├── .drone.yml ├── .github_changelog_generator ├── .golangci.yml ├── CHANGELOG.md ├── LICENSE ├── NOTICE ├── README.md ├── drone ├── client.go ├── client_test.go ├── const.go ├── interface.go ├── testdata │ ├── build.json │ ├── build.json.golden │ ├── builds.json │ ├── builds.json.golden │ ├── cron.json │ ├── cron.json.golden │ ├── crons.json │ ├── crons.json.golden │ ├── logs.json │ ├── logs.json.golden │ ├── repo.json │ ├── repo.json.golden │ ├── repos.json │ ├── repos.json.golden │ ├── user.json │ ├── user.json.golden │ ├── users.json │ └── users.json.golden └── types.go ├── go.mod ├── go.sum └── plugin ├── admission ├── admission.go ├── client.go ├── handler.go └── handler_test.go ├── config ├── client.go ├── config.go ├── handler.go └── handler_test.go ├── converter ├── client.go ├── converter.go ├── handler.go └── handler_test.go ├── environ ├── client.go ├── environ.go ├── handler.go ├── handler_test.go ├── util.go └── util_test.go ├── internal ├── aesgcm │ ├── aesgcm.go │ └── aesgcm_test.go ├── client │ └── client.go └── internal.go ├── logger └── log.go ├── registry ├── client.go ├── handler.go ├── handler_test.go └── registry.go ├── secret ├── client.go ├── handler.go ├── handler_test.go └── secret.go ├── validator ├── client.go ├── client_test.go ├── handler.go ├── handler_test.go └── validater.go └── webhook ├── client.go ├── handler.go ├── handler_test.go └── webhook.go /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | type: docker 4 | name: default 5 | 6 | platform: 7 | os: linux 8 | arch: amd64 9 | 10 | steps: 11 | - name: vet 12 | image: golang:1.15 13 | commands: 14 | - go vet ./... 15 | volumes: 16 | - name: gopath 17 | path: /go 18 | 19 | - name: test 20 | image: golang:1.15 21 | commands: 22 | - go test -cover ./... 23 | volumes: 24 | - name: gopath 25 | path: /go 26 | 27 | volumes: 28 | - name: gopath 29 | temp: {} 30 | 31 | ... 32 | -------------------------------------------------------------------------------- /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | since-tag=v1.6.0 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | dupl: 3 | threshold: 100 4 | funlen: 5 | lines: 100 6 | statements: 50 7 | gci: 8 | local-prefixes: github.com/golangci/golangci-lint 9 | goconst: 10 | min-len: 3 11 | min-occurrences: 3 12 | gocritic: 13 | enabled-tags: 14 | - diagnostic 15 | - experimental 16 | - opinionated 17 | - performance 18 | - style 19 | disabled-checks: 20 | - dupImport # https://github.com/go-critic/go-critic/issues/845 21 | - ifElseChain 22 | - octalLiteral 23 | - whyNoLint 24 | - wrapperFunc 25 | gocyclo: 26 | min-complexity: 15 27 | goimports: 28 | local-prefixes: github.com/golangci/golangci-lint 29 | gomnd: 30 | settings: 31 | mnd: 32 | # don't include the "operation" and "assign" 33 | checks: argument,case,condition,return 34 | govet: 35 | check-shadowing: true 36 | settings: 37 | printf: 38 | funcs: 39 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 40 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 41 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 42 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 43 | lll: 44 | line-length: 200 45 | maligned: 46 | suggest-new: true 47 | misspell: 48 | locale: US 49 | nolintlint: 50 | allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space) 51 | allow-unused: false # report any unused nolint directives 52 | require-explanation: false # don't require an explanation for nolint directives 53 | require-specific: false # don't require nolint directives to be specific about which linter is being skipped 54 | 55 | linters: 56 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 57 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 58 | disable-all: true 59 | enable: 60 | - bodyclose 61 | - deadcode 62 | - depguard 63 | - dogsled 64 | - errcheck 65 | - exportloopref 66 | - exhaustive 67 | - funlen 68 | - gochecknoinits 69 | - goconst 70 | - gocritic 71 | - gocyclo 72 | - gofmt 73 | - goimports 74 | - gomnd 75 | - goprintffuncname 76 | - gosec 77 | - gosimple 78 | - govet 79 | - ineffassign 80 | - lll 81 | - misspell 82 | - nakedret 83 | - noctx 84 | - nolintlint 85 | - revive 86 | - rowserrcheck 87 | - staticcheck 88 | - structcheck 89 | - stylecheck 90 | - typecheck 91 | - unconvert 92 | - unparam 93 | - unused 94 | - varcheck 95 | - whitespace 96 | 97 | # don't enable: 98 | # - asciicheck 99 | # - dupl 100 | # - scopelint 101 | # - gochecknoglobals 102 | # - gocognit 103 | # - godot 104 | # - godox 105 | # - goerr113 106 | # - interfacer 107 | # - maligned 108 | # - nestif 109 | # - prealloc 110 | # - testpackage 111 | # - revive 112 | # - wsl 113 | 114 | issues: 115 | # Excluding configuration per-path, per-linter, per-text and per-source 116 | exclude-rules: 117 | - path: _test\.go 118 | linters: 119 | - gomnd 120 | 121 | # https://github.com/go-critic/go-critic/issues/926 122 | - linters: 123 | - gocritic 124 | text: "unnecessaryDefer:" 125 | 126 | run: 127 | skip-files: 128 | - _gen\.go 129 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.7.1](https://github.com/drone/drone-go/tree/v1.7.1) (2021-11-04) 4 | 5 | [Full Changelog](https://github.com/drone/drone-go/compare/v1.7.0...v1.7.1) 6 | 7 | **Implemented enhancements:** 8 | 9 | - add new type for cardInput [\#70](https://github.com/drone/drone-go/pull/70) ([eoinmcafee00](https://github.com/eoinmcafee00)) 10 | 11 | **Merged pull requests:** 12 | 13 | - fix for various lint warnings [\#68](https://github.com/drone/drone-go/pull/68) ([marko-gacesa](https://github.com/marko-gacesa)) 14 | 15 | ## [v1.7.0](https://github.com/drone/drone-go/tree/v1.7.0) (2021-10-04) 16 | 17 | [Full Changelog](https://github.com/drone/drone-go/compare/v1.6.2...v1.7.0) 18 | 19 | **Implemented enhancements:** 20 | 21 | - \(DRON-124\) add ReposRunningStatus [\#65](https://github.com/drone/drone-go/pull/65) ([tphoney](https://github.com/tphoney)) 22 | 23 | **Merged pull requests:** 24 | 25 | - \(maint\) release\_v1.7.0 prep [\#67](https://github.com/drone/drone-go/pull/67) ([tphoney](https://github.com/tphoney)) 26 | 27 | ## [v1.6.2](https://github.com/drone/drone-go/tree/v1.6.2) (2021-08-27) 28 | 29 | [Full Changelog](https://github.com/drone/drone-go/compare/v1.6.1...v1.6.2) 30 | 31 | **Implemented enhancements:** 32 | 33 | - add new variable auto-cancel-running to repo struct [\#64](https://github.com/drone/drone-go/pull/64) ([eoinmcafee00](https://github.com/eoinmcafee00)) 34 | 35 | ## [v1.6.1](https://github.com/drone/drone-go/tree/v1.6.1) (2021-08-19) 36 | 37 | [Full Changelog](https://github.com/drone/drone-go/compare/v1.6.0...v1.6.1) 38 | 39 | **Implemented enhancements:** 40 | 41 | - Template type for supporting CLI commands [\#61](https://github.com/drone/drone-go/pull/61) ([eoinmcafee00](https://github.com/eoinmcafee00)) 42 | 43 | **Fixed bugs:** 44 | 45 | - update create template api to access namespace as a param [\#62](https://github.com/drone/drone-go/pull/62) ([eoinmcafee00](https://github.com/eoinmcafee00)) 46 | 47 | **Merged pull requests:** 48 | 49 | - Release/1.6.1 [\#63](https://github.com/drone/drone-go/pull/63) ([eoinmcafee00](https://github.com/eoinmcafee00)) 50 | - Add a vet step to drone config [\#57](https://github.com/drone/drone-go/pull/57) ([tboerger](https://github.com/tboerger)) 51 | 52 | 53 | 54 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Drone 2 | Copyright 2019 Drone.IO, Inc 3 | 4 | This product includes software developed at Drone.IO, Inc. 5 | (http://drone.io/). 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # drone-go 2 | 3 | [![Go.dev](https://pkg.go.dev/badge/github.com/drone/drone-go)](https://pkg.go.dev/github.com/drone/drone-go?tab=doc) 4 | 5 | ```Go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/drone/drone-go/drone" 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | const ( 16 | token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" 17 | host = "http://drone.company.com" 18 | ) 19 | 20 | func main() { 21 | // create an http client with oauth authentication. 22 | config := new(oauth2.Config) 23 | auther := config.Client( 24 | oauth2.NoContext, 25 | &oauth2.Token{ 26 | AccessToken: token, 27 | }, 28 | ) 29 | 30 | // create the drone client with authenticator 31 | client := drone.NewClient(host, auther) 32 | 33 | // gets the current user 34 | user, err := client.Self() 35 | fmt.Println(user, err) 36 | 37 | // gets the named repository information 38 | repo, err := client.Repo("drone", "drone-go") 39 | fmt.Println(repo, err) 40 | } 41 | ``` 42 | ## Release procedure 43 | 44 | Run the changelog generator. 45 | 46 | ```BASH 47 | docker run -it --rm -v "$(pwd)":/usr/local/src/your-app githubchangeloggenerator/github-changelog-generator -u drone -p drone-go -t 48 | ``` 49 | 50 | You can generate a token by logging into your GitHub account and going to Settings -> Personal access tokens. 51 | 52 | Next we tag the PR's with the fixes or enhancements labels. If the PR does not fufil the requirements, do not add a label. 53 | 54 | Run the changelog generator again with the future version according to semver. 55 | 56 | ```BASH 57 | docker run -it --rm -v "$(pwd)":/usr/local/src/your-app githubchangeloggenerator/github-changelog-generator -u drone -p drone-go -t --future-release v1.0.0 58 | ``` 59 | 60 | Create your pull request for the release. Get it merged then tag the release. -------------------------------------------------------------------------------- /drone/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package drone 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "fmt" 21 | "io" 22 | "io/ioutil" 23 | "net/http" 24 | "net/url" 25 | "strconv" 26 | "strings" 27 | ) 28 | 29 | const ( 30 | pathSelf = "%s/api/user" 31 | pathRepos = "%s/api/user/repos" 32 | pathIncomplete = "%s/api/builds/incomplete" 33 | pathIncompleteV2 = "%s/api/builds/incomplete/v2" 34 | pathReposAll = "%s/api/repos" 35 | pathRepo = "%s/api/repos/%s/%s" 36 | pathChown = "%s/api/repos/%s/%s/chown" 37 | pathRepair = "%s/api/repos/%s/%s/repair" 38 | pathBuilds = "%s/api/repos/%s/%s/builds?%s" 39 | pathBuild = "%s/api/repos/%s/%s/builds/%v" 40 | pathApprove = "%s/api/repos/%s/%s/builds/%d/approve/%d" 41 | pathDecline = "%s/api/repos/%s/%s/builds/%d/decline/%d" 42 | pathPromote = "%s/api/repos/%s/%s/builds/%d/promote?%s" 43 | pathRollback = "%s/api/repos/%s/%s/builds/%d/rollback?%s" 44 | pathLog = "%s/api/repos/%s/%s/builds/%d/logs/%d/%d" 45 | pathRepoSecrets = "%s/api/repos/%s/%s/secrets" 46 | pathRepoSecret = "%s/api/repos/%s/%s/secrets/%s" 47 | pathEncryptSecret = "%s/api/repos/%s/%s/encrypt/secret" 48 | pathSign = "%s/api/repos/%s/%s/sign" 49 | pathVerify = "%s/api/repos/%s/%s/verify" 50 | pathCrons = "%s/api/repos/%s/%s/cron" 51 | pathCron = "%s/api/repos/%s/%s/cron/%s" 52 | pathSecrets = "%s/api/secrets" 53 | pathSecretsNamespace = "%s/api/secrets/%s" 54 | pathSecretsName = "%s/api/secrets/%s/%s" 55 | pathUsers = "%s/api/users" 56 | pathUser = "%s/api/users/%s" 57 | pathQueue = "%s/api/queue" 58 | pathServers = "%s/api/servers" 59 | pathServer = "%s/api/servers/%s" 60 | pathScalerPause = "%s/api/pause" 61 | pathScalerResume = "%s/api/resume" 62 | pathNodes = "%s/api/nodes" 63 | pathNode = "%s/api/nodes/%s" 64 | pathVersion = "%s/version" 65 | pathTemplates = "%s/api/templates" 66 | pathTemplateName = "%s/api/templates/%s/%s" 67 | pathTemplateNamespace = "%s/api/templates/%s" 68 | ) 69 | 70 | type client struct { 71 | client *http.Client 72 | addr string 73 | } 74 | 75 | type ListOptions struct { 76 | Page int 77 | Size int 78 | } 79 | 80 | func encodeListOptions(opts ListOptions) string { 81 | params := url.Values{} 82 | if opts.Page != 0 { 83 | params.Set("page", strconv.Itoa(opts.Page)) 84 | } 85 | if opts.Size != 0 { 86 | params.Set("per_page", strconv.Itoa(opts.Size)) 87 | } 88 | return params.Encode() 89 | } 90 | 91 | // New returns a client at the specified url. 92 | func New(uri string) Client { 93 | return &client{http.DefaultClient, strings.TrimSuffix(uri, "/")} 94 | } 95 | 96 | // NewClient returns a client at the specified url. 97 | func NewClient(uri string, cli *http.Client) Client { 98 | return &client{cli, strings.TrimSuffix(uri, "/")} 99 | } 100 | 101 | // SetClient sets the http.Client. 102 | func (c *client) SetClient(client *http.Client) { 103 | c.client = client 104 | } 105 | 106 | // SetAddress sets the server address. 107 | func (c *client) SetAddress(addr string) { 108 | c.addr = addr 109 | } 110 | 111 | // Self returns the currently authenticated user. 112 | func (c *client) Self() (*User, error) { 113 | out := new(User) 114 | uri := fmt.Sprintf(pathSelf, c.addr) 115 | err := c.get(uri, out) 116 | return out, err 117 | } 118 | 119 | // User returns a user by login. 120 | func (c *client) User(login string) (*User, error) { 121 | out := new(User) 122 | uri := fmt.Sprintf(pathUser, c.addr, login) 123 | err := c.get(uri, out) 124 | return out, err 125 | } 126 | 127 | // UserList returns a list of all registered users. 128 | func (c *client) UserList() ([]*User, error) { 129 | var out []*User 130 | uri := fmt.Sprintf(pathUsers, c.addr) 131 | err := c.get(uri, &out) 132 | return out, err 133 | } 134 | 135 | // UserCreate creates a new user account. 136 | func (c *client) UserCreate(in *User) (*User, error) { 137 | out := new(User) 138 | uri := fmt.Sprintf(pathUsers, c.addr) 139 | err := c.post(uri, in, out) 140 | return out, err 141 | } 142 | 143 | // UserUpdate updates a user account. 144 | func (c *client) UserUpdate(login string, in *UserPatch) (*User, error) { 145 | out := new(User) 146 | uri := fmt.Sprintf(pathUser, c.addr, login) 147 | err := c.patch(uri, in, out) 148 | return out, err 149 | } 150 | 151 | // UserDelete deletes a user account. 152 | func (c *client) UserDelete(login string) error { 153 | uri := fmt.Sprintf(pathUser, c.addr, login) 154 | err := c.delete(uri) 155 | return err 156 | } 157 | 158 | // Incomplete returns a list of incomplete builds. 159 | func (c *client) Incomplete() ([]*Repo, error) { 160 | var out []*Repo 161 | uri := fmt.Sprintf(pathIncomplete, c.addr) 162 | err := c.get(uri, &out) 163 | return out, err 164 | } 165 | 166 | // IncompleteV2 returns a list of builds repos and any stages that are running/pending. 167 | func (c *client) IncompleteV2() ([]*RepoBuildStage, error) { 168 | var out []*RepoBuildStage 169 | uri := fmt.Sprintf(pathIncompleteV2, c.addr) 170 | err := c.get(uri, &out) 171 | return out, err 172 | } 173 | 174 | // Repo returns a repository by name. 175 | func (c *client) Repo(owner, name string) (*Repo, error) { 176 | out := new(Repo) 177 | uri := fmt.Sprintf(pathRepo, c.addr, owner, name) 178 | err := c.get(uri, out) 179 | return out, err 180 | } 181 | 182 | // RepoList returns a list of all repositories to which 183 | // the user has explicit access in the host system. 184 | func (c *client) RepoList() ([]*Repo, error) { 185 | var out []*Repo 186 | uri := fmt.Sprintf(pathRepos, c.addr) 187 | err := c.get(uri, &out) 188 | return out, err 189 | } 190 | 191 | // RepoListSync returns a list of all repositories to which 192 | // the user has explicit access in the host system. 193 | func (c *client) RepoListSync() ([]*Repo, error) { 194 | var out []*Repo 195 | uri := fmt.Sprintf(pathRepos, c.addr) 196 | err := c.post(uri, nil, &out) 197 | return out, err 198 | } 199 | 200 | // RepoListAll returns a paginated list of all repositories 201 | // stored in the database. 202 | func (c *client) RepoListAll(opts ListOptions) ([]*Repo, error) { 203 | var out []*Repo 204 | uri := fmt.Sprintf(pathReposAll, c.addr) 205 | if opt := encodeListOptions(opts); opt != "" { 206 | uri = uri + "?" + opt 207 | } 208 | err := c.get(uri, &out) 209 | return out, err 210 | } 211 | 212 | // RepoEnable activates a repository. 213 | func (c *client) RepoEnable(owner, name string) (*Repo, error) { 214 | out := new(Repo) 215 | uri := fmt.Sprintf(pathRepo, c.addr, owner, name) 216 | err := c.post(uri, nil, out) 217 | return out, err 218 | } 219 | 220 | // RepoDisable disables a repository. 221 | func (c *client) RepoDisable(owner, name string) error { 222 | uri := fmt.Sprintf(pathRepo, c.addr, owner, name) 223 | err := c.delete(uri) 224 | return err 225 | } 226 | 227 | // RepoDelete permanently deletes a repository. 228 | func (c *client) RepoDelete(owner, name string) error { 229 | uri := fmt.Sprintf(pathRepo+"?remove=true", c.addr, owner, name) 230 | err := c.delete(uri) 231 | return err 232 | } 233 | 234 | // RepoUpdate updates a repository. 235 | func (c *client) RepoUpdate(owner, name string, in *RepoPatch) (*Repo, error) { 236 | out := new(Repo) 237 | uri := fmt.Sprintf(pathRepo, c.addr, owner, name) 238 | err := c.patch(uri, in, out) 239 | return out, err 240 | } 241 | 242 | // RepoChown updates a repository owner. 243 | func (c *client) RepoChown(owner, name string) (*Repo, error) { 244 | out := new(Repo) 245 | uri := fmt.Sprintf(pathChown, c.addr, owner, name) 246 | err := c.post(uri, nil, out) 247 | return out, err 248 | } 249 | 250 | // RepoRepair repais the repository hooks. 251 | func (c *client) RepoRepair(owner, name string) error { 252 | uri := fmt.Sprintf(pathRepair, c.addr, owner, name) 253 | return c.post(uri, nil, nil) 254 | } 255 | 256 | // Build returns a repository build by number. 257 | func (c *client) Build(owner, name string, num int) (*Build, error) { 258 | out := new(Build) 259 | uri := fmt.Sprintf(pathBuild, c.addr, owner, name, num) 260 | err := c.get(uri, out) 261 | return out, err 262 | } 263 | 264 | // Build returns the latest repository build by branch. 265 | func (c *client) BuildLast(owner, name, branch string) (*Build, error) { 266 | out := new(Build) 267 | uri := fmt.Sprintf(pathBuild, c.addr, owner, name, "latest") 268 | if branch != "" { 269 | uri += "?branch=" + branch 270 | } 271 | err := c.get(uri, out) 272 | return out, err 273 | } 274 | 275 | // BuildList returns a list of recent builds for the 276 | // the specified repository. 277 | func (c *client) BuildList(owner, name string, opts ListOptions) ([]*Build, error) { 278 | var out []*Build 279 | uri := fmt.Sprintf(pathBuilds, c.addr, owner, name, encodeListOptions(opts)) 280 | err := c.get(uri, &out) 281 | return out, err 282 | } 283 | 284 | // BuildCreate creates a new build by branch or commit. 285 | func (c *client) BuildCreate(owner, name, commit, branch string, params map[string]string) (*Build, error) { 286 | out := new(Build) 287 | val := mapValues(params) 288 | if commit != "" { 289 | val.Set("commit", commit) 290 | } 291 | if branch != "" { 292 | val.Set("branch", branch) 293 | } 294 | uri := fmt.Sprintf(pathBuilds, c.addr, owner, name, val.Encode()) 295 | 296 | err := c.post(uri, nil, out) 297 | return out, err 298 | } 299 | 300 | // BuildRestart re-starts a stopped build. 301 | func (c *client) BuildRestart(owner, name string, build int, params map[string]string) (*Build, error) { 302 | out := new(Build) 303 | val := mapValues(params) 304 | uri := fmt.Sprintf(pathBuild, c.addr, owner, name, build) 305 | if len(params) > 0 { 306 | uri = uri + "?" + val.Encode() 307 | } 308 | err := c.post(uri, nil, out) 309 | return out, err 310 | } 311 | 312 | // BuildCancel cancels the running job. 313 | func (c *client) BuildCancel(owner, name string, build int) error { 314 | uri := fmt.Sprintf(pathBuild, c.addr, owner, name, build) 315 | err := c.delete(uri) 316 | return err 317 | } 318 | 319 | // BuildPurge purges the build history. 320 | func (c *client) BuildPurge(owner, name string, before int) error { 321 | param := fmt.Sprintf("before=%d", before) 322 | uri := fmt.Sprintf(pathBuilds, c.addr, owner, name, param) 323 | err := c.delete(uri) 324 | return err 325 | } 326 | 327 | // Promote promotes a build to the target environment. 328 | func (c *client) Promote(namespace, name string, build int, target string, params map[string]string) (*Build, error) { 329 | out := new(Build) 330 | val := mapValues(params) 331 | val.Set("target", target) 332 | uri := fmt.Sprintf(pathPromote, c.addr, namespace, name, build, val.Encode()) 333 | err := c.post(uri, nil, out) 334 | return out, err 335 | } 336 | 337 | // Roolback reverts the target environment to an previous build. 338 | func (c *client) Rollback(namespace, name string, build int, target string, params map[string]string) (*Build, error) { 339 | out := new(Build) 340 | val := mapValues(params) 341 | val.Set("target", target) 342 | uri := fmt.Sprintf(pathRollback, c.addr, namespace, name, build, val.Encode()) 343 | err := c.post(uri, nil, out) 344 | return out, err 345 | } 346 | 347 | // Approve approves a blocked build stage. 348 | func (c *client) Approve(namespace, name string, build, stage int) error { 349 | uri := fmt.Sprintf(pathApprove, c.addr, namespace, name, build, stage) 350 | err := c.post(uri, nil, nil) 351 | return err 352 | } 353 | 354 | // Decline declines a blocked build stage. 355 | func (c *client) Decline(namespace, name string, build, stage int) error { 356 | uri := fmt.Sprintf(pathDecline, c.addr, namespace, name, build, stage) 357 | err := c.post(uri, nil, nil) 358 | return err 359 | } 360 | 361 | // BuildLogs returns the build logs for the specified job. 362 | func (c *client) Logs(owner, name string, build, stage, step int) ([]*Line, error) { 363 | var out []*Line 364 | uri := fmt.Sprintf(pathLog, c.addr, owner, name, build, stage, step) 365 | err := c.get(uri, &out) 366 | return out, err 367 | } 368 | 369 | // LogsPurge purges the build logs for the specified build. 370 | func (c *client) LogsPurge(owner, name string, build, stage, step int) error { 371 | uri := fmt.Sprintf(pathLog, c.addr, owner, name, build, stage, step) 372 | err := c.delete(uri) 373 | return err 374 | } 375 | 376 | // Sign signs the yaml file. 377 | func (c *client) Sign(owner, name, file string) (string, error) { 378 | in := struct { 379 | Data string `json:"data"` 380 | }{Data: file} 381 | out := struct { 382 | Data string `json:"data"` 383 | }{} 384 | uri := fmt.Sprintf(pathSign, c.addr, owner, name) 385 | err := c.post(uri, &in, &out) 386 | return out.Data, err 387 | } 388 | 389 | // Verify verifies the yaml signature. 390 | func (c *client) Verify(owner, name, file string) error { 391 | in := struct { 392 | Data string `json:"data"` 393 | }{Data: file} 394 | uri := fmt.Sprintf(pathVerify, c.addr, owner, name) 395 | return c.post(uri, &in, nil) 396 | } 397 | 398 | // Encrypt returns an encrypted secret. 399 | func (c *client) Encrypt(owner, name string, secret *Secret) (string, error) { 400 | out := struct { 401 | Data string `json:"data"` 402 | }{} 403 | uri := fmt.Sprintf(pathEncryptSecret, c.addr, owner, name) 404 | err := c.post(uri, secret, &out) 405 | return out.Data, err 406 | } 407 | 408 | // Secret returns a secret by name. 409 | func (c *client) Secret(owner, name, secret string) (*Secret, error) { 410 | out := new(Secret) 411 | uri := fmt.Sprintf(pathRepoSecret, c.addr, owner, name, secret) 412 | err := c.get(uri, out) 413 | return out, err 414 | } 415 | 416 | // SecretList returns a list of all repository secrets. 417 | func (c *client) SecretList(owner, name string) ([]*Secret, error) { 418 | var out []*Secret 419 | uri := fmt.Sprintf(pathRepoSecrets, c.addr, owner, name) 420 | err := c.get(uri, &out) 421 | return out, err 422 | } 423 | 424 | // SecretCreate creates a secret. 425 | func (c *client) SecretCreate(owner, name string, in *Secret) (*Secret, error) { 426 | out := new(Secret) 427 | uri := fmt.Sprintf(pathRepoSecrets, c.addr, owner, name) 428 | err := c.post(uri, in, out) 429 | return out, err 430 | } 431 | 432 | // SecretUpdate updates a secret. 433 | func (c *client) SecretUpdate(owner, name string, in *Secret) (*Secret, error) { 434 | out := new(Secret) 435 | uri := fmt.Sprintf(pathRepoSecret, c.addr, owner, name, in.Name) 436 | err := c.patch(uri, in, out) 437 | return out, err 438 | } 439 | 440 | // SecretDelete deletes a secret. 441 | func (c *client) SecretDelete(owner, name, secret string) error { 442 | uri := fmt.Sprintf(pathRepoSecret, c.addr, owner, name, secret) 443 | return c.delete(uri) 444 | } 445 | 446 | // OrgSecret returns a secret by name. 447 | func (c *client) OrgSecret(namespace, name string) (*Secret, error) { 448 | out := new(Secret) 449 | uri := fmt.Sprintf(pathSecretsName, c.addr, namespace, name) 450 | err := c.get(uri, &out) 451 | return out, err 452 | } 453 | 454 | // OrgSecretList returns a list of all repository secrets. 455 | func (c *client) OrgSecretList(namespace string) ([]*Secret, error) { 456 | var out []*Secret 457 | uri := fmt.Sprintf(pathSecretsNamespace, c.addr, namespace) 458 | err := c.get(uri, &out) 459 | return out, err 460 | } 461 | 462 | // OrgSecretListAll returns a list of all repository secrets. 463 | func (c *client) OrgSecretListAll() ([]*Secret, error) { 464 | var out []*Secret 465 | uri := fmt.Sprintf(pathSecrets, c.addr) 466 | err := c.get(uri, &out) 467 | return out, err 468 | } 469 | 470 | // OrgSecretCreate creates a registry. 471 | func (c *client) OrgSecretCreate(namespace string, in *Secret) (*Secret, error) { 472 | out := new(Secret) 473 | uri := fmt.Sprintf(pathSecretsNamespace, c.addr, namespace) 474 | err := c.post(uri, in, out) 475 | return out, err 476 | } 477 | 478 | // OrgSecretUpdate updates a registry. 479 | func (c *client) OrgSecretUpdate(namespace string, in *Secret) (*Secret, error) { 480 | out := new(Secret) 481 | uri := fmt.Sprintf(pathSecretsName, c.addr, namespace, in.Name) 482 | err := c.patch(uri, in, out) 483 | return out, err 484 | } 485 | 486 | // OrgSecretDelete deletes a secret. 487 | func (c *client) OrgSecretDelete(namespace, name string) error { 488 | uri := fmt.Sprintf(pathSecretsName, c.addr, namespace, name) 489 | return c.delete(uri) 490 | } 491 | 492 | // Cron returns a cronjob by name. 493 | func (c *client) Cron(owner, name, cron string) (*Cron, error) { 494 | out := new(Cron) 495 | uri := fmt.Sprintf(pathCron, c.addr, owner, name, cron) 496 | err := c.get(uri, out) 497 | return out, err 498 | } 499 | 500 | // CronList returns a list of all repository cronjobs. 501 | func (c *client) CronList(owner, name string) ([]*Cron, error) { 502 | var out []*Cron 503 | uri := fmt.Sprintf(pathCrons, c.addr, owner, name) 504 | err := c.get(uri, &out) 505 | return out, err 506 | } 507 | 508 | // CronCreate creates a cronjob. 509 | func (c *client) CronCreate(owner, name string, in *Cron) (*Cron, error) { 510 | out := new(Cron) 511 | uri := fmt.Sprintf(pathCrons, c.addr, owner, name) 512 | err := c.post(uri, in, out) 513 | return out, err 514 | } 515 | 516 | // CronUpdate disables a cronjob. 517 | func (c *client) CronUpdate(owner, name, cron string, in *CronPatch) (*Cron, error) { 518 | out := new(Cron) 519 | uri := fmt.Sprintf(pathCron, c.addr, owner, name, cron) 520 | err := c.patch(uri, in, out) 521 | return out, err 522 | } 523 | 524 | // CronDelete deletes a cronjob. 525 | func (c *client) CronDelete(owner, name, cron string) error { 526 | uri := fmt.Sprintf(pathCron, c.addr, owner, name, cron) 527 | return c.delete(uri) 528 | } 529 | 530 | // CronExec executes a cronjob. 531 | func (c *client) CronExec(owner, name, cron string) error { 532 | uri := fmt.Sprintf(pathCron, c.addr, owner, name, cron) 533 | err := c.post(uri, nil, nil) 534 | return err 535 | } 536 | 537 | // Queue returns a list of enqueued builds. 538 | func (c *client) Queue() ([]*Stage, error) { 539 | var out []*Stage 540 | uri := fmt.Sprintf(pathQueue, c.addr) 541 | err := c.get(uri, &out) 542 | return out, err 543 | } 544 | 545 | // QueueResume resumes queue operations. 546 | func (c *client) QueueResume() error { 547 | uri := fmt.Sprintf(pathQueue, c.addr) 548 | err := c.post(uri, nil, nil) 549 | return err 550 | } 551 | 552 | // QueuePause pauses queue operations. 553 | func (c *client) QueuePause() error { 554 | uri := fmt.Sprintf(pathQueue, c.addr) 555 | err := c.delete(uri) 556 | return err 557 | } 558 | 559 | // Node returns a node by name. 560 | func (c *client) Node(name string) (*Node, error) { 561 | out := new(Node) 562 | uri := fmt.Sprintf(pathNode, c.addr, name) 563 | err := c.get(uri, out) 564 | return out, err 565 | } 566 | 567 | // NodeList returns a list of all nodes. 568 | func (c *client) NodeList() ([]*Node, error) { 569 | var out []*Node 570 | uri := fmt.Sprintf(pathNodes, c.addr) 571 | err := c.get(uri, &out) 572 | return out, err 573 | } 574 | 575 | // NodeCreate creates a node. 576 | func (c *client) NodeCreate(in *Node) (*Node, error) { 577 | out := new(Node) 578 | uri := fmt.Sprintf(pathNodes, c.addr) 579 | err := c.post(uri, in, out) 580 | return out, err 581 | } 582 | 583 | // NodeDelete deletes a node. 584 | func (c *client) NodeDelete(name string) error { 585 | uri := fmt.Sprintf(pathNode, c.addr, name) 586 | return c.delete(uri) 587 | } 588 | 589 | // NodeUpdate updates a node. 590 | func (c *client) NodeUpdate(name string, in *NodePatch) (*Node, error) { 591 | out := new(Node) 592 | uri := fmt.Sprintf(pathNode, c.addr, name) 593 | err := c.patch(uri, in, out) 594 | return out, err 595 | } 596 | 597 | // 598 | // autoscaler 599 | // 600 | 601 | // Server returns the named servers details. 602 | func (c *client) Server(name string) (*Server, error) { 603 | out := new(Server) 604 | uri := fmt.Sprintf(pathServer, c.addr, name) 605 | err := c.get(uri, &out) 606 | return out, err 607 | } 608 | 609 | // ServerList returns a list of all active build servers. 610 | func (c *client) ServerList() ([]*Server, error) { 611 | var out []*Server 612 | uri := fmt.Sprintf(pathServers, c.addr) 613 | err := c.get(uri, &out) 614 | return out, err 615 | } 616 | 617 | // ServerCreate creates a new server. 618 | func (c *client) ServerCreate() (*Server, error) { 619 | out := new(Server) 620 | uri := fmt.Sprintf(pathServers, c.addr) 621 | err := c.post(uri, nil, out) 622 | return out, err 623 | } 624 | 625 | // ServerDelete terminates a server. 626 | func (c *client) ServerDelete(name string, force bool) error { 627 | uri := fmt.Sprintf(pathServer, c.addr, name) 628 | if force { 629 | uri += "?force=true" 630 | } 631 | return c.delete(uri) 632 | } 633 | 634 | // AutoscalePause pauses the autoscaler. 635 | func (c *client) AutoscalePause() error { 636 | uri := fmt.Sprintf(pathScalerPause, c.addr) 637 | return c.post(uri, nil, nil) 638 | } 639 | 640 | // AutoscaleResume resumes the autoscaler. 641 | func (c *client) AutoscaleResume() error { 642 | uri := fmt.Sprintf(pathScalerResume, c.addr) 643 | return c.post(uri, nil, nil) 644 | } 645 | 646 | // AutoscaleVersion resumes the autoscaler. 647 | func (c *client) AutoscaleVersion() (*Version, error) { 648 | out := new(Version) 649 | uri := fmt.Sprintf(pathVersion, c.addr) 650 | err := c.get(uri, out) 651 | return out, err 652 | } 653 | 654 | // Template returns a template by name. 655 | func (c *client) Template(namespace, name string) (*Template, error) { 656 | out := new(Template) 657 | uri := fmt.Sprintf(pathTemplateName, c.addr, namespace, name) 658 | err := c.get(uri, out) 659 | return out, err 660 | } 661 | 662 | // TemplateListAll returns a list of all templates. 663 | func (c *client) TemplateListAll() ([]*Template, error) { 664 | var out []*Template 665 | uri := fmt.Sprintf(pathTemplates, c.addr) 666 | err := c.get(uri, &out) 667 | return out, err 668 | } 669 | 670 | // TemplateList returns a list of all templates by namespace 671 | func (c *client) TemplateList(namespace string) ([]*Template, error) { 672 | var out []*Template 673 | uri := fmt.Sprintf(pathTemplateNamespace, c.addr, namespace) 674 | err := c.get(uri, &out) 675 | return out, err 676 | } 677 | 678 | // TemplateCreate creates a template. 679 | func (c *client) TemplateCreate(namespace string, in *Template) (*Template, error) { 680 | out := new(Template) 681 | uri := fmt.Sprintf(pathTemplateNamespace, c.addr, namespace) 682 | err := c.post(uri, in, out) 683 | return out, err 684 | } 685 | 686 | // TemplateUpdate updates a template. 687 | func (c *client) TemplateUpdate(namespace, name string, in *Template) (*Template, error) { 688 | out := new(Template) 689 | uri := fmt.Sprintf(pathTemplateName, c.addr, namespace, name) 690 | err := c.patch(uri, in, out) 691 | return out, err 692 | } 693 | 694 | // TemplateDelete deletes a template. 695 | func (c *client) TemplateDelete(namespace, name string) error { 696 | uri := fmt.Sprintf(pathTemplateName, c.addr, namespace, name) 697 | return c.delete(uri) 698 | } 699 | 700 | // 701 | // http request helper functions 702 | // 703 | 704 | // helper function for making an http GET request. 705 | func (c *client) get(rawurl string, out interface{}) error { 706 | return c.do(rawurl, "GET", nil, out) 707 | } 708 | 709 | // helper function for making an http POST request. 710 | func (c *client) post(rawurl string, in, out interface{}) error { 711 | return c.do(rawurl, "POST", in, out) 712 | } 713 | 714 | // helper function for making an http PATCH request. 715 | func (c *client) patch(rawurl string, in, out interface{}) error { 716 | return c.do(rawurl, "PATCH", in, out) 717 | } 718 | 719 | // helper function for making an http DELETE request. 720 | func (c *client) delete(rawurl string) error { 721 | return c.do(rawurl, "DELETE", nil, nil) 722 | } 723 | 724 | // helper function to make an http request 725 | func (c *client) do(rawurl, method string, in, out interface{}) error { 726 | body, err := c.open(rawurl, method, in, out) 727 | if err != nil { 728 | return err 729 | } 730 | defer body.Close() 731 | if out != nil { 732 | return json.NewDecoder(body).Decode(out) 733 | } 734 | return nil 735 | } 736 | 737 | // helper function to open an http request 738 | func (c *client) open(rawurl, method string, in, out interface{}) (io.ReadCloser, error) { 739 | uri, err := url.Parse(rawurl) 740 | if err != nil { 741 | return nil, err 742 | } 743 | req, err := http.NewRequest(method, uri.String(), nil) 744 | if err != nil { 745 | return nil, err 746 | } 747 | if in != nil { 748 | decoded, derr := json.Marshal(in) 749 | if derr != nil { 750 | return nil, derr 751 | } 752 | buf := bytes.NewBuffer(decoded) 753 | req.Body = ioutil.NopCloser(buf) 754 | req.ContentLength = int64(len(decoded)) 755 | req.Header.Set("Content-Length", strconv.Itoa(len(decoded))) 756 | req.Header.Set("Content-Type", "application/json") 757 | } 758 | resp, err := c.client.Do(req) 759 | if err != nil { 760 | return nil, err 761 | } 762 | if resp.StatusCode > 299 { 763 | defer resp.Body.Close() 764 | out, _ := ioutil.ReadAll(resp.Body) 765 | return nil, fmt.Errorf("client error %d: %s", resp.StatusCode, string(out)) 766 | } 767 | return resp.Body, nil 768 | } 769 | 770 | // mapValues converts a map to url.Values 771 | func mapValues(params map[string]string) url.Values { 772 | values := url.Values{} 773 | for key, val := range params { 774 | values.Add(key, val) 775 | } 776 | return values 777 | } 778 | -------------------------------------------------------------------------------- /drone/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package drone 16 | 17 | import ( 18 | "encoding/json" 19 | "io/ioutil" 20 | "net/http" 21 | "net/http/httptest" 22 | "testing" 23 | 24 | "github.com/google/go-cmp/cmp" 25 | ) 26 | 27 | // 28 | // user tests. 29 | // 30 | 31 | func TestSelf(t *testing.T) { 32 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 33 | defer ts.Close() 34 | 35 | client := New(ts.URL) 36 | got, err := client.Self() 37 | if err != nil { 38 | t.Error(err) 39 | return 40 | } 41 | 42 | in, err := ioutil.ReadFile("testdata/user.json.golden") 43 | if err != nil { 44 | t.Error(err) 45 | return 46 | } 47 | want := new(User) 48 | err = json.Unmarshal(in, want) 49 | if err != nil { 50 | t.Error(err) 51 | return 52 | } 53 | if diff := cmp.Diff(got, want); diff != "" { 54 | t.Errorf("Unexpected response") 55 | t.Log(diff) 56 | } 57 | } 58 | 59 | func TestUser(t *testing.T) { 60 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 61 | defer ts.Close() 62 | 63 | client := New(ts.URL) 64 | got, err := client.User("octocat") 65 | if err != nil { 66 | t.Error(err) 67 | return 68 | } 69 | 70 | in, err := ioutil.ReadFile("testdata/user.json.golden") 71 | if err != nil { 72 | t.Error(err) 73 | return 74 | } 75 | want := new(User) 76 | err = json.Unmarshal(in, want) 77 | if err != nil { 78 | t.Error(err) 79 | return 80 | } 81 | if diff := cmp.Diff(got, want); diff != "" { 82 | t.Errorf("Unexpected response") 83 | t.Log(diff) 84 | } 85 | } 86 | 87 | func TestUserList(t *testing.T) { 88 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 89 | defer ts.Close() 90 | 91 | client := New(ts.URL) 92 | got, err := client.UserList() 93 | if err != nil { 94 | t.Error(err) 95 | return 96 | } 97 | 98 | in, err := ioutil.ReadFile("testdata/users.json.golden") 99 | if err != nil { 100 | t.Error(err) 101 | return 102 | } 103 | want := []*User{} 104 | err = json.Unmarshal(in, &want) 105 | if err != nil { 106 | t.Error(err) 107 | return 108 | } 109 | if diff := cmp.Diff(got, want); diff != "" { 110 | t.Errorf("Unexpected response") 111 | t.Log(diff) 112 | } 113 | } 114 | 115 | func TestUserDelete(t *testing.T) { 116 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 117 | defer ts.Close() 118 | 119 | client := New(ts.URL) 120 | err := client.UserDelete("octocat") 121 | if err != nil { 122 | t.Error(err) 123 | return 124 | } 125 | } 126 | 127 | func TestUserCreate(t *testing.T) { 128 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 129 | defer ts.Close() 130 | 131 | client := New(ts.URL) 132 | got, err := client.UserCreate(&User{}) 133 | if err != nil { 134 | t.Error(err) 135 | return 136 | } 137 | 138 | in, err := ioutil.ReadFile("testdata/user.json.golden") 139 | if err != nil { 140 | t.Error(err) 141 | return 142 | } 143 | want := new(User) 144 | err = json.Unmarshal(in, want) 145 | if err != nil { 146 | t.Error(err) 147 | return 148 | } 149 | if diff := cmp.Diff(got, want); diff != "" { 150 | t.Errorf("Unexpected response") 151 | t.Log(diff) 152 | } 153 | } 154 | 155 | func TestUserUpdate(t *testing.T) { 156 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 157 | defer ts.Close() 158 | 159 | client := New(ts.URL) 160 | got, err := client.UserUpdate("octocat", &UserPatch{}) 161 | if err != nil { 162 | t.Error(err) 163 | return 164 | } 165 | 166 | in, err := ioutil.ReadFile("testdata/user.json.golden") 167 | if err != nil { 168 | t.Error(err) 169 | return 170 | } 171 | want := new(User) 172 | err = json.Unmarshal(in, want) 173 | if err != nil { 174 | t.Error(err) 175 | return 176 | } 177 | if diff := cmp.Diff(got, want); diff != "" { 178 | t.Errorf("Unexpected response") 179 | t.Log(diff) 180 | } 181 | } 182 | 183 | // 184 | // repos 185 | // 186 | 187 | func TestRepo(t *testing.T) { 188 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 189 | defer ts.Close() 190 | 191 | client := New(ts.URL) 192 | got, err := client.Repo("octocat", "hello-world") 193 | if err != nil { 194 | t.Error(err) 195 | return 196 | } 197 | 198 | in, err := ioutil.ReadFile("testdata/repo.json.golden") 199 | if err != nil { 200 | t.Error(err) 201 | return 202 | } 203 | want := new(Repo) 204 | err = json.Unmarshal(in, want) 205 | if err != nil { 206 | t.Error(err) 207 | return 208 | } 209 | if diff := cmp.Diff(got, want); diff != "" { 210 | t.Errorf("Unexpected response") 211 | t.Log(diff) 212 | } 213 | } 214 | 215 | func TestRepoList(t *testing.T) { 216 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 217 | defer ts.Close() 218 | 219 | client := New(ts.URL) 220 | got, err := client.RepoList() 221 | if err != nil { 222 | t.Error(err) 223 | return 224 | } 225 | 226 | in, err := ioutil.ReadFile("testdata/repos.json.golden") 227 | if err != nil { 228 | t.Error(err) 229 | return 230 | } 231 | want := []*Repo{} 232 | err = json.Unmarshal(in, &want) 233 | if err != nil { 234 | t.Error(err) 235 | return 236 | } 237 | if diff := cmp.Diff(got, want); diff != "" { 238 | t.Errorf("Unexpected response") 239 | t.Log(diff) 240 | } 241 | } 242 | 243 | func TestRepoListSync(t *testing.T) { 244 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 245 | defer ts.Close() 246 | 247 | client := New(ts.URL) 248 | got, err := client.RepoListSync() 249 | if err != nil { 250 | t.Error(err) 251 | return 252 | } 253 | 254 | in, err := ioutil.ReadFile("testdata/repos.json.golden") 255 | if err != nil { 256 | t.Error(err) 257 | return 258 | } 259 | want := []*Repo{} 260 | err = json.Unmarshal(in, &want) 261 | if err != nil { 262 | t.Error(err) 263 | return 264 | } 265 | if diff := cmp.Diff(got, want); diff != "" { 266 | t.Errorf("Unexpected response") 267 | t.Log(diff) 268 | } 269 | } 270 | 271 | func TestRepoEnable(t *testing.T) { 272 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 273 | defer ts.Close() 274 | 275 | client := New(ts.URL) 276 | got, err := client.RepoEnable("octocat", "hello-world") 277 | if err != nil { 278 | t.Error(err) 279 | return 280 | } 281 | 282 | in, err := ioutil.ReadFile("testdata/repo.json.golden") 283 | if err != nil { 284 | t.Error(err) 285 | return 286 | } 287 | want := new(Repo) 288 | err = json.Unmarshal(in, want) 289 | if err != nil { 290 | t.Error(err) 291 | return 292 | } 293 | if diff := cmp.Diff(got, want); diff != "" { 294 | t.Errorf("Unexpected response") 295 | t.Log(diff) 296 | } 297 | } 298 | 299 | func TestRepoDisable(t *testing.T) { 300 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 301 | defer ts.Close() 302 | 303 | client := New(ts.URL) 304 | err := client.RepoDisable("octocat", "hello-world") 305 | if err != nil { 306 | t.Error(err) 307 | return 308 | } 309 | } 310 | 311 | func TestRepoRepair(t *testing.T) { 312 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 313 | defer ts.Close() 314 | 315 | client := New(ts.URL) 316 | err := client.RepoRepair("octocat", "hello-world") 317 | if err != nil { 318 | t.Error(err) 319 | return 320 | } 321 | } 322 | 323 | func TestRepoChown(t *testing.T) { 324 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 325 | defer ts.Close() 326 | 327 | client := New(ts.URL) 328 | got, err := client.RepoChown("octocat", "hello-world") 329 | if err != nil { 330 | t.Error(err) 331 | return 332 | } 333 | 334 | in, err := ioutil.ReadFile("testdata/repo.json.golden") 335 | if err != nil { 336 | t.Error(err) 337 | return 338 | } 339 | want := new(Repo) 340 | err = json.Unmarshal(in, want) 341 | if err != nil { 342 | t.Error(err) 343 | return 344 | } 345 | if diff := cmp.Diff(got, want); diff != "" { 346 | t.Errorf("Unexpected response") 347 | t.Log(diff) 348 | } 349 | } 350 | 351 | func TestRepoUpdate(t *testing.T) { 352 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 353 | defer ts.Close() 354 | 355 | client := New(ts.URL) 356 | got, err := client.RepoUpdate("octocat", "hello-world", &RepoPatch{}) 357 | if err != nil { 358 | t.Error(err) 359 | return 360 | } 361 | 362 | in, err := ioutil.ReadFile("testdata/repo.json.golden") 363 | if err != nil { 364 | t.Error(err) 365 | return 366 | } 367 | want := new(Repo) 368 | err = json.Unmarshal(in, want) 369 | if err != nil { 370 | t.Error(err) 371 | return 372 | } 373 | if diff := cmp.Diff(got, want); diff != "" { 374 | t.Errorf("Unexpected response") 375 | t.Log(diff) 376 | } 377 | } 378 | 379 | // 380 | // cron jobs 381 | // 382 | 383 | func TestCron(t *testing.T) { 384 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 385 | defer ts.Close() 386 | 387 | client := New(ts.URL) 388 | got, err := client.Cron("octocat", "hello-world", "nightly") 389 | if err != nil { 390 | t.Error(err) 391 | return 392 | } 393 | 394 | in, err := ioutil.ReadFile("testdata/cron.json.golden") 395 | if err != nil { 396 | t.Error(err) 397 | return 398 | } 399 | want := new(Cron) 400 | err = json.Unmarshal(in, want) 401 | if err != nil { 402 | t.Error(err) 403 | return 404 | } 405 | if diff := cmp.Diff(got, want); diff != "" { 406 | t.Errorf("Unexpected response") 407 | t.Log(diff) 408 | } 409 | } 410 | 411 | func TestCronList(t *testing.T) { 412 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 413 | defer ts.Close() 414 | 415 | client := New(ts.URL) 416 | got, err := client.CronList("octocat", "hello-world") 417 | if err != nil { 418 | t.Error(err) 419 | return 420 | } 421 | 422 | in, err := ioutil.ReadFile("testdata/crons.json.golden") 423 | if err != nil { 424 | t.Error(err) 425 | return 426 | } 427 | want := []*Cron{} 428 | err = json.Unmarshal(in, &want) 429 | if err != nil { 430 | t.Error(err) 431 | return 432 | } 433 | if diff := cmp.Diff(got, want); diff != "" { 434 | t.Errorf("Unexpected response") 435 | t.Log(diff) 436 | } 437 | } 438 | 439 | // func TestCronDisable(t *testing.T) { 440 | // ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 441 | // defer ts.Close() 442 | 443 | // client := New(ts.URL) 444 | // err := client.CronDisable("octocat", "hello-world", "nightly") 445 | // if err != nil { 446 | // t.Error(err) 447 | // return 448 | // } 449 | // } 450 | 451 | // func TestCronEnable(t *testing.T) { 452 | // ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 453 | // defer ts.Close() 454 | 455 | // client := New(ts.URL) 456 | // err := client.CronEnable("octocat", "hello-world", "nightly") 457 | // if err != nil { 458 | // t.Error(err) 459 | // return 460 | // } 461 | // } 462 | 463 | // 464 | // builds 465 | // 466 | 467 | func TestBuild(t *testing.T) { 468 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 469 | defer ts.Close() 470 | 471 | client := New(ts.URL) 472 | got, err := client.Build("octocat", "hello-world", 1) 473 | if err != nil { 474 | t.Error(err) 475 | return 476 | } 477 | 478 | in, err := ioutil.ReadFile("testdata/build.json.golden") 479 | if err != nil { 480 | t.Error(err) 481 | return 482 | } 483 | want := new(Build) 484 | err = json.Unmarshal(in, want) 485 | if err != nil { 486 | t.Error(err) 487 | return 488 | } 489 | if diff := cmp.Diff(got, want); diff != "" { 490 | t.Errorf("Unexpected response") 491 | t.Log(diff) 492 | } 493 | } 494 | 495 | func TestBuildLast(t *testing.T) { 496 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 497 | defer ts.Close() 498 | 499 | client := New(ts.URL) 500 | got, err := client.BuildLast("octocat", "hello-world", "master") 501 | if err != nil { 502 | t.Error(err) 503 | return 504 | } 505 | 506 | in, err := ioutil.ReadFile("testdata/build.json.golden") 507 | if err != nil { 508 | t.Error(err) 509 | return 510 | } 511 | want := new(Build) 512 | err = json.Unmarshal(in, want) 513 | if err != nil { 514 | t.Error(err) 515 | return 516 | } 517 | if diff := cmp.Diff(got, want); diff != "" { 518 | t.Errorf("Unexpected response") 519 | t.Log(diff) 520 | } 521 | } 522 | 523 | func TestBuildList(t *testing.T) { 524 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 525 | defer ts.Close() 526 | 527 | client := New(ts.URL) 528 | got, err := client.BuildList("octocat", "hello-world", ListOptions{}) 529 | if err != nil { 530 | t.Error(err) 531 | return 532 | } 533 | 534 | in, err := ioutil.ReadFile("testdata/builds.json.golden") 535 | if err != nil { 536 | t.Error(err) 537 | return 538 | } 539 | want := []*Build{} 540 | err = json.Unmarshal(in, &want) 541 | if err != nil { 542 | t.Error(err) 543 | return 544 | } 545 | if diff := cmp.Diff(got, want); diff != "" { 546 | t.Errorf("Unexpected response") 547 | t.Log(diff) 548 | } 549 | } 550 | 551 | // func TestBuildQueue(t *testing.T) { 552 | // ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 553 | // defer ts.Close() 554 | 555 | // client := New(ts.URL) 556 | // got, err := client.BuildQueue() 557 | // if err != nil { 558 | // t.Error(err) 559 | // return 560 | // } 561 | 562 | // in, err := ioutil.ReadFile("testdata/builds.json.golden") 563 | // if err != nil { 564 | // t.Error(err) 565 | // return 566 | // } 567 | // want := []*Build{} 568 | // err = json.Unmarshal(in, &want) 569 | // if err != nil { 570 | // t.Error(err) 571 | // return 572 | // } 573 | // if diff := cmp.Diff(got, want); diff != "" { 574 | // t.Errorf("Unexpected response") 575 | // t.Log(diff) 576 | // } 577 | // } 578 | 579 | func TestBuildRestart(t *testing.T) { 580 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 581 | defer ts.Close() 582 | 583 | client := New(ts.URL) 584 | got, err := client.BuildRestart("octocat", "hello-world", 99, nil) 585 | if err != nil { 586 | t.Error(err) 587 | return 588 | } 589 | 590 | in, err := ioutil.ReadFile("testdata/build.json.golden") 591 | if err != nil { 592 | t.Error(err) 593 | return 594 | } 595 | want := new(Build) 596 | err = json.Unmarshal(in, want) 597 | if err != nil { 598 | t.Error(err) 599 | return 600 | } 601 | if diff := cmp.Diff(got, want); diff != "" { 602 | t.Errorf("Unexpected response") 603 | t.Log(diff) 604 | } 605 | } 606 | 607 | func TestBuildCancel(t *testing.T) { 608 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 609 | defer ts.Close() 610 | 611 | client := New(ts.URL) 612 | err := client.BuildCancel("octocat", "hello-world", 1) 613 | if err != nil { 614 | t.Error(err) 615 | } 616 | } 617 | 618 | func TestApprove(t *testing.T) { 619 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 620 | defer ts.Close() 621 | 622 | client := New(ts.URL) 623 | err := client.Approve("octocat", "hello-world", 1, 2) 624 | if err != nil { 625 | t.Error(err) 626 | } 627 | } 628 | 629 | func TestDecline(t *testing.T) { 630 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 631 | defer ts.Close() 632 | 633 | client := New(ts.URL) 634 | err := client.Decline("octocat", "hello-world", 1, 3) 635 | if err != nil { 636 | t.Error(err) 637 | } 638 | } 639 | 640 | // 641 | // logs 642 | // 643 | 644 | func TestLogs(t *testing.T) { 645 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 646 | defer ts.Close() 647 | 648 | client := New(ts.URL) 649 | got, err := client.Logs("octocat", "hello-world", 1, 2, 3) 650 | if err != nil { 651 | t.Error(err) 652 | return 653 | } 654 | 655 | in, err := ioutil.ReadFile("testdata/logs.json.golden") 656 | if err != nil { 657 | t.Error(err) 658 | return 659 | } 660 | want := []*Line{} 661 | err = json.Unmarshal(in, &want) 662 | if err != nil { 663 | t.Error(err) 664 | return 665 | } 666 | if diff := cmp.Diff(got, want); diff != "" { 667 | t.Errorf("Unexpected response") 668 | t.Log(diff) 669 | } 670 | } 671 | 672 | func TestLogsPurge(t *testing.T) { 673 | ts := httptest.NewServer(http.HandlerFunc(mockHandler)) 674 | defer ts.Close() 675 | 676 | client := New(ts.URL) 677 | err := client.LogsPurge("octocat", "hello-world", 1, 2, 3) 678 | if err != nil { 679 | t.Error(err) 680 | return 681 | } 682 | } 683 | 684 | // 685 | // mock server and testdata. 686 | // 687 | func mockHandler(w http.ResponseWriter, r *http.Request) { 688 | routes := []struct { 689 | verb string 690 | path string 691 | body string 692 | code int 693 | }{ 694 | // 695 | // users 696 | // 697 | { 698 | verb: "GET", 699 | path: "/api/user", 700 | body: "testdata/user.json", 701 | code: 200, 702 | }, 703 | { 704 | verb: "GET", 705 | path: "/api/users/octocat", 706 | body: "testdata/user.json", 707 | code: 200, 708 | }, 709 | { 710 | verb: "DELETE", 711 | path: "/api/users/octocat", 712 | code: 204, 713 | }, 714 | { 715 | verb: "POST", 716 | path: "/api/users", 717 | body: "testdata/user.json", 718 | code: 200, 719 | }, 720 | { 721 | verb: "PATCH", 722 | path: "/api/users/octocat", 723 | body: "testdata/user.json", 724 | code: 200, 725 | }, 726 | { 727 | verb: "GET", 728 | path: "/api/users", 729 | body: "testdata/users.json", 730 | code: 200, 731 | }, 732 | // 733 | // repos 734 | // 735 | { 736 | verb: "GET", 737 | path: "/api/repos/octocat/hello-world", 738 | body: "testdata/repo.json", 739 | code: 200, 740 | }, 741 | { 742 | verb: "GET", 743 | path: "/api/user/repos", 744 | body: "testdata/repos.json", 745 | code: 200, 746 | }, 747 | { 748 | verb: "POST", 749 | path: "/api/user/repos", 750 | body: "testdata/repos.json", 751 | code: 200, 752 | }, 753 | { 754 | verb: "POST", 755 | path: "/api/repos/octocat/hello-world/repair", 756 | code: 204, 757 | }, 758 | { 759 | verb: "POST", 760 | path: "/api/repos/octocat/hello-world/chown", 761 | body: "testdata/repo.json", 762 | code: 200, 763 | }, 764 | { 765 | verb: "PATCH", 766 | path: "/api/repos/octocat/hello-world", 767 | body: "testdata/repo.json", 768 | code: 200, 769 | }, 770 | { 771 | verb: "POST", 772 | path: "/api/repos/octocat/hello-world", 773 | body: "testdata/repo.json", 774 | code: 200, 775 | }, 776 | { 777 | verb: "DELETE", 778 | path: "/api/repos/octocat/hello-world", 779 | code: 204, 780 | }, 781 | // 782 | // crons 783 | // 784 | { 785 | verb: "GET", 786 | path: "/api/repos/octocat/hello-world/cron/nightly", 787 | body: "testdata/cron.json", 788 | code: 200, 789 | }, 790 | { 791 | verb: "GET", 792 | path: "/api/repos/octocat/hello-world/cron", 793 | body: "testdata/crons.json", 794 | code: 200, 795 | }, 796 | { 797 | verb: "POST", 798 | path: "/api/repos/octocat/hello-world/cron/nightly", 799 | code: 204, 800 | }, 801 | { 802 | verb: "DELETE", 803 | path: "/api/repos/octocat/hello-world/cron/nightly", 804 | code: 204, 805 | }, 806 | // 807 | // builds 808 | // 809 | { 810 | verb: "GET", 811 | path: "/api/system/builds", 812 | body: "testdata/builds.json", 813 | code: 200, 814 | }, 815 | { 816 | verb: "GET", 817 | path: "/api/repos/octocat/hello-world/builds", 818 | body: "testdata/builds.json", 819 | code: 200, 820 | }, 821 | { 822 | verb: "GET", 823 | path: "/api/repos/octocat/hello-world/builds/1", 824 | body: "testdata/build.json", 825 | code: 200, 826 | }, 827 | { 828 | verb: "GET", 829 | path: "/api/repos/octocat/hello-world/builds/latest", 830 | body: "testdata/build.json", 831 | code: 200, 832 | }, 833 | { 834 | verb: "POST", 835 | path: "/api/repos/octocat/hello-world/builds/99", 836 | body: "testdata/build.json", 837 | code: 200, 838 | }, 839 | { 840 | verb: "DELETE", 841 | path: "/api/repos/octocat/hello-world/builds/1", 842 | code: 204, 843 | }, 844 | { 845 | verb: "POST", 846 | path: "/api/repos/octocat/hello-world/builds/1/approve/2", 847 | code: 204, 848 | }, 849 | { 850 | verb: "POST", 851 | path: "/api/repos/octocat/hello-world/builds/1/decline/3", 852 | code: 204, 853 | }, 854 | // 855 | // logs 856 | // 857 | { 858 | verb: "GET", 859 | path: "/api/repos/octocat/hello-world/builds/1/logs/2/3", 860 | body: "testdata/logs.json", 861 | code: 200, 862 | }, 863 | { 864 | verb: "DELETE", 865 | path: "/api/repos/octocat/hello-world/builds/1/logs/2/3", 866 | code: 204, 867 | }, 868 | } 869 | 870 | path := r.URL.Path 871 | verb := r.Method 872 | for _, route := range routes { 873 | if route.verb != verb { 874 | continue 875 | } 876 | if route.path != path { 877 | continue 878 | } 879 | if route.code == 204 { 880 | w.WriteHeader(204) 881 | return 882 | } 883 | body, err := ioutil.ReadFile(route.body) 884 | if err != nil { 885 | break 886 | } 887 | w.WriteHeader(route.code) 888 | _, _ = w.Write(body) 889 | return 890 | } 891 | w.WriteHeader(404) 892 | } 893 | -------------------------------------------------------------------------------- /drone/const.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package drone 16 | 17 | // Event values. 18 | const ( 19 | EventPush = "push" 20 | EventPullRequest = "pull_request" 21 | EventTag = "tag" 22 | EventPromote = "promote" 23 | EventRollback = "rollback" 24 | ) 25 | 26 | // Status values. 27 | const ( 28 | StatusSkipped = "skipped" 29 | StatusBlocked = "blocked" 30 | StatusDeclined = "declined" 31 | StatusWaiting = "waiting_on_dependencies" 32 | StatusPending = "pending" 33 | StatusRunning = "running" 34 | StatusPassing = "success" 35 | StatusFailing = "failure" 36 | StatusKilled = "killed" 37 | StatusError = "error" 38 | ) 39 | -------------------------------------------------------------------------------- /drone/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package drone 16 | 17 | import ( 18 | "net/http" 19 | ) 20 | 21 | // TODO(bradrydzewski) add repo + latest build endpoint 22 | // TODO(bradrydzewski) add queue endpoint 23 | // TDOO(bradrydzewski) add stats endpoint 24 | // TODO(bradrydzewski) add version endpoint 25 | 26 | // Client is used to communicate with a Drone server. 27 | type Client interface { 28 | // SetClient sets the http.Client. 29 | SetClient(*http.Client) 30 | 31 | // SetAddress sets the server address. 32 | SetAddress(string) 33 | 34 | // Self returns the currently authenticated user. 35 | Self() (*User, error) 36 | 37 | // User returns a user by login. 38 | User(login string) (*User, error) 39 | 40 | // UserList returns a list of all registered users. 41 | UserList() ([]*User, error) 42 | 43 | // UserCreate creates a new user account. 44 | UserCreate(user *User) (*User, error) 45 | 46 | // UserUpdate updates a user account. 47 | UserUpdate(login string, user *UserPatch) (*User, error) 48 | 49 | // UserDelete deletes a user account. 50 | UserDelete(login string) error 51 | 52 | // Incomplete returns a list of incomplete builds. 53 | Incomplete() ([]*Repo, error) 54 | 55 | // IncompleteV2 returns a list of builds/repos/stages that are running/pending. 56 | IncompleteV2() ([]*RepoBuildStage, error) 57 | 58 | // Repo returns a repository by name. 59 | Repo(namespace, name string) (*Repo, error) 60 | 61 | // RepoList returns a list of all repositories to which 62 | // the user has explicit access in the host system. 63 | RepoList() ([]*Repo, error) 64 | 65 | // RepoListSync returns a list of all repositories to which 66 | // the user has explicit access in the host system. 67 | RepoListSync() ([]*Repo, error) 68 | 69 | // RepoListAll returns a list of all repositories in 70 | // the database. This is only available to system admins. 71 | RepoListAll(opts ListOptions) ([]*Repo, error) 72 | 73 | // RepoEnable activates a repository. 74 | RepoEnable(namespace, name string) (*Repo, error) 75 | 76 | // RepoUpdate updates a repository. 77 | RepoUpdate(namespace, name string, repo *RepoPatch) (*Repo, error) 78 | 79 | // RepoChown updates a repository owner. 80 | RepoChown(namespace, name string) (*Repo, error) 81 | 82 | // RepoRepair repairs the repository hooks. 83 | RepoRepair(namespace, name string) error 84 | 85 | // RepoDisable disables a repository. 86 | RepoDisable(namespace, name string) error 87 | 88 | // RepoDelete permanetnly deletes a repository. 89 | RepoDelete(namespace, name string) error 90 | 91 | // Build returns a repository build by number. 92 | Build(namespace, name string, build int) (*Build, error) 93 | 94 | // BuildLast returns the latest build by branch. An 95 | // empty branch will result in the default branch. 96 | BuildLast(namespace, name, branch string) (*Build, error) 97 | 98 | // BuildList returns a list of recent builds for the 99 | // the specified repository. 100 | BuildList(namespace, name string, opts ListOptions) ([]*Build, error) 101 | 102 | // BuildCreate creates a new build by branch or commit. 103 | BuildCreate(owner, name, commit, branch string, params map[string]string) (*Build, error) 104 | 105 | // BuildRestart re-starts a build. 106 | BuildRestart(namespace, name string, build int, params map[string]string) (*Build, error) 107 | 108 | // BuildCancel stops the specified running job for 109 | // given build. 110 | BuildCancel(namespace, name string, build int) error 111 | 112 | // BuildPurge purges the build history. 113 | BuildPurge(namespace, name string, before int) error 114 | 115 | // Approve approves a blocked build stage. 116 | Approve(namespace, name string, build, stage int) error 117 | 118 | // Decline declines a blocked build stage. 119 | Decline(namespace, name string, build, stage int) error 120 | 121 | // Promote promotes a build to the target environment. 122 | Promote(namespace, name string, build int, target string, params map[string]string) (*Build, error) 123 | 124 | // Rollback reverts the target environment to an previous build. 125 | Rollback(namespace, name string, build int, target string, params map[string]string) (*Build, error) 126 | 127 | // Logs gets the logs for the specified step. 128 | Logs(owner, name string, build, stage, step int) ([]*Line, error) 129 | 130 | // LogsPurge purges the build logs for the specified step. 131 | LogsPurge(owner, name string, build, stage, step int) error 132 | 133 | // Secret returns a secret by name. 134 | Secret(owner, name, secret string) (*Secret, error) 135 | 136 | // SecretList returns a list of all repository secrets. 137 | SecretList(owner, name string) ([]*Secret, error) 138 | 139 | // SecretCreate creates a registry. 140 | SecretCreate(owner, name string, secret *Secret) (*Secret, error) 141 | 142 | // SecretUpdate updates a registry. 143 | SecretUpdate(owner, name string, secret *Secret) (*Secret, error) 144 | 145 | // SecretDelete deletes a secret. 146 | SecretDelete(owner, name, secret string) error 147 | 148 | // OrgSecret returns a secret by name. 149 | OrgSecret(namespace, secret string) (*Secret, error) 150 | 151 | // OrgSecretList returns a list of all repository secrets. 152 | OrgSecretList(namespace string) ([]*Secret, error) 153 | 154 | // OrgSecretListAll returns a list of all repository secrets. 155 | OrgSecretListAll() ([]*Secret, error) 156 | 157 | // OrgSecretCreate creates a registry. 158 | OrgSecretCreate(namespace string, secret *Secret) (*Secret, error) 159 | 160 | // OrgSecretUpdate updates a registry. 161 | OrgSecretUpdate(namespace string, secret *Secret) (*Secret, error) 162 | 163 | // OrgSecretDelete deletes a secret. 164 | OrgSecretDelete(namespace, name string) error 165 | 166 | // Cron returns a cronjob by name. 167 | Cron(owner, name, cron string) (*Cron, error) 168 | 169 | // CronList returns a list of all repository cronjobs. 170 | CronList(owner string, name string) ([]*Cron, error) 171 | 172 | // CronCreate creates a cronjob. 173 | CronCreate(owner, name string, in *Cron) (*Cron, error) 174 | 175 | // CronDelete deletes a cronjob. 176 | CronDelete(owner, name, cron string) error 177 | 178 | // CronUpdate enables a cronjob. 179 | CronUpdate(owner, name, cron string, in *CronPatch) (*Cron, error) 180 | 181 | // CronExec executes a cronjob. 182 | CronExec(owner, name, cron string) error 183 | 184 | // Sign signs the yaml file. 185 | Sign(owner, name, file string) (string, error) 186 | 187 | // Verify verifies the yaml signature. 188 | Verify(owner, name, file string) error 189 | 190 | // Encrypt returns an encrypted secret 191 | Encrypt(owner, name string, secret *Secret) (string, error) 192 | 193 | // Queue returns a list of queue items. 194 | Queue() ([]*Stage, error) 195 | 196 | // QueuePause pauses queue operations. 197 | QueuePause() error 198 | 199 | // QueueResume resumes queue operations. 200 | QueueResume() error 201 | 202 | // Node returns a node by name. 203 | Node(name string) (*Node, error) 204 | 205 | // NodeList returns a list of all nodes. 206 | NodeList() ([]*Node, error) 207 | 208 | // NodeCreate creates a node. 209 | NodeCreate(in *Node) (*Node, error) 210 | 211 | // NodeDelete deletes a node. 212 | NodeDelete(name string) error 213 | 214 | // NodeUpdate updates a node. 215 | NodeUpdate(name string, in *NodePatch) (*Node, error) 216 | 217 | // 218 | // Move to autoscaler-go 219 | // 220 | 221 | // Server returns the named servers details. 222 | Server(name string) (*Server, error) 223 | 224 | // ServerList returns a list of all active build servers. 225 | ServerList() ([]*Server, error) 226 | 227 | // ServerCreate creates a new server. 228 | ServerCreate() (*Server, error) 229 | 230 | // ServerDelete terminates a server. 231 | ServerDelete(name string, force bool) error 232 | 233 | // AutoscalePause pauses the autoscaler. 234 | AutoscalePause() error 235 | 236 | // AutoscaleResume resumes the autoscaler. 237 | AutoscaleResume() error 238 | 239 | // AutoscaleVersion returns the autoscaler version. 240 | AutoscaleVersion() (*Version, error) 241 | 242 | // Template returns a template by name. 243 | Template(namespace string, name string) (*Template, error) 244 | 245 | // TemplateListAll returns a list of all templates. 246 | TemplateListAll() ([]*Template, error) 247 | 248 | // TemplateList returns a list of all templates by namespace 249 | TemplateList(namespace string) ([]*Template, error) 250 | 251 | // TemplateCreate creates a template. 252 | TemplateCreate(namespace string, template *Template) (*Template, error) 253 | 254 | // TemplateUpdate updates template data. 255 | TemplateUpdate(namespace string, name string, template *Template) (*Template, error) 256 | 257 | // TemplateDelete deletes a template. 258 | TemplateDelete(namespace string, name string) error 259 | } 260 | -------------------------------------------------------------------------------- /drone/testdata/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "repo_id": 168, 4 | "trigger": "@hook", 5 | "number": 1, 6 | "status": "success", 7 | "event": "push", 8 | "action": "", 9 | "link": "https://github.com/drone/drone-git/compare/61e0cb11bbb1...6783f72b40ea", 10 | "timestamp": 0, 11 | "message": "format yaml", 12 | "before": "", 13 | "after": "6783f72b40ea0af776d2ec40ef7e01c58b4794c3", 14 | "ref": "refs/heads/master", 15 | "source_repo": "", 16 | "source": "master", 17 | "target": "master", 18 | "author_login": "octocat", 19 | "author_name": "The Octocat", 20 | "author_email": "octocat@github.com", 21 | "author_avatar": "https://avatars1.githubusercontent.com/u/817538?v=4", 22 | "sender": "octocat", 23 | "started": 1537221915, 24 | "finished": 1537222041, 25 | "created": 1537221914, 26 | "updated": 1537221915, 27 | "version": 3, 28 | "stages": [ 29 | { 30 | "id": 1, 31 | "build_id": 1, 32 | "number": 1, 33 | "name": "linux-amd64", 34 | "status": "success", 35 | "errignore": false, 36 | "exit_code": 0, 37 | "machine": "2e0d5b2c7ff4", 38 | "os": "linux", 39 | "arch": "amd64", 40 | "started": 1537221916, 41 | "stopped": 1537221969, 42 | "created": 1537221914, 43 | "updated": 1537221969, 44 | "version": 4, 45 | "on_success": true, 46 | "on_failure": false, 47 | "steps": [ 48 | { 49 | "id": 4, 50 | "step_id": 1, 51 | "number": 1, 52 | "name": "clone", 53 | "status": "success", 54 | "exit_code": 0, 55 | "started": 1537221916, 56 | "stopped": 1537221918, 57 | "version": 4 58 | }, 59 | { 60 | "id": 5, 61 | "step_id": 1, 62 | "number": 2, 63 | "name": "test", 64 | "status": "success", 65 | "exit_code": 0, 66 | "started": 1537221918, 67 | "stopped": 1537221929, 68 | "version": 4 69 | }, 70 | { 71 | "id": 6, 72 | "step_id": 1, 73 | "number": 3, 74 | "name": "push", 75 | "status": "success", 76 | "exit_code": 0, 77 | "started": 1537221929, 78 | "stopped": 1537221969, 79 | "version": 4 80 | } 81 | ] 82 | }, 83 | { 84 | "id": 2, 85 | "build_id": 1, 86 | "number": 2, 87 | "name": "linux-arm64", 88 | "status": "success", 89 | "errignore": false, 90 | "exit_code": 0, 91 | "machine": "56bcfb7f1207", 92 | "os": "linux", 93 | "arch": "arm64", 94 | "started": 1537221915, 95 | "stopped": 1537221969, 96 | "created": 1537221914, 97 | "updated": 1537221969, 98 | "version": 4, 99 | "on_success": true, 100 | "on_failure": false, 101 | "steps": [ 102 | { 103 | "id": 1, 104 | "step_id": 2, 105 | "number": 1, 106 | "name": "clone", 107 | "status": "success", 108 | "exit_code": 0, 109 | "started": 1537221916, 110 | "stopped": 1537221918, 111 | "version": 4 112 | }, 113 | { 114 | "id": 2, 115 | "step_id": 2, 116 | "number": 2, 117 | "name": "test", 118 | "status": "success", 119 | "exit_code": 0, 120 | "started": 1537221918, 121 | "stopped": 1537221930, 122 | "version": 4 123 | }, 124 | { 125 | "id": 3, 126 | "step_id": 2, 127 | "number": 3, 128 | "name": "push", 129 | "status": "success", 130 | "exit_code": 0, 131 | "started": 1537221930, 132 | "stopped": 1537221968, 133 | "version": 4 134 | } 135 | ] 136 | }, 137 | { 138 | "id": 3, 139 | "build_id": 1, 140 | "number": 3, 141 | "name": "linux-arm", 142 | "status": "success", 143 | "errignore": false, 144 | "exit_code": 0, 145 | "machine": "f01265229b89", 146 | "os": "linux", 147 | "arch": "arm", 148 | "started": 1537221917, 149 | "stopped": 1537222024, 150 | "created": 1537221914, 151 | "updated": 1537222024, 152 | "version": 4, 153 | "on_success": true, 154 | "on_failure": false, 155 | "steps": [ 156 | { 157 | "id": 7, 158 | "step_id": 3, 159 | "number": 1, 160 | "name": "clone", 161 | "status": "success", 162 | "exit_code": 0, 163 | "started": 1537221918, 164 | "stopped": 1537221922, 165 | "version": 4 166 | }, 167 | { 168 | "id": 8, 169 | "step_id": 3, 170 | "number": 2, 171 | "name": "test", 172 | "status": "success", 173 | "exit_code": 0, 174 | "started": 1537221922, 175 | "stopped": 1537221937, 176 | "version": 4 177 | }, 178 | { 179 | "id": 9, 180 | "step_id": 3, 181 | "number": 3, 182 | "name": "push", 183 | "status": "success", 184 | "exit_code": 0, 185 | "started": 1537221937, 186 | "stopped": 1537222017, 187 | "version": 4 188 | } 189 | ] 190 | }, 191 | { 192 | "id": 4, 193 | "build_id": 1, 194 | "number": 4, 195 | "name": "after", 196 | "status": "success", 197 | "errignore": false, 198 | "exit_code": 0, 199 | "machine": "2e0d5b2c7ff4", 200 | "os": "linux", 201 | "arch": "amd64", 202 | "started": 1537222025, 203 | "stopped": 1537222041, 204 | "created": 1537221914, 205 | "updated": 1537222041, 206 | "version": 5, 207 | "on_success": true, 208 | "on_failure": false, 209 | "depends_on": [ 210 | "linux-arm", 211 | "linux-arm64", 212 | "linux-amd64" 213 | ], 214 | "steps": [ 215 | { 216 | "id": 10, 217 | "step_id": 4, 218 | "number": 1, 219 | "name": "clone", 220 | "status": "success", 221 | "exit_code": 0, 222 | "started": 1537222025, 223 | "stopped": 1537222027, 224 | "version": 4 225 | }, 226 | { 227 | "id": 11, 228 | "step_id": 4, 229 | "number": 2, 230 | "name": "manifest", 231 | "status": "success", 232 | "exit_code": 0, 233 | "started": 1537222027, 234 | "stopped": 1537222040, 235 | "version": 4 236 | } 237 | ] 238 | } 239 | ] 240 | } -------------------------------------------------------------------------------- /drone/testdata/build.json.golden: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "repo_id": 168, 4 | "trigger": "@hook", 5 | "number": 1, 6 | "status": "success", 7 | "event": "push", 8 | "action": "", 9 | "link": "https://github.com/drone/drone-git/compare/61e0cb11bbb1...6783f72b40ea", 10 | "timestamp": 0, 11 | "message": "format yaml", 12 | "before": "", 13 | "after": "6783f72b40ea0af776d2ec40ef7e01c58b4794c3", 14 | "ref": "refs/heads/master", 15 | "source_repo": "", 16 | "source": "master", 17 | "target": "master", 18 | "author_login": "octocat", 19 | "author_name": "The Octocat", 20 | "author_email": "octocat@github.com", 21 | "author_avatar": "https://avatars1.githubusercontent.com/u/817538?v=4", 22 | "sender": "octocat", 23 | "started": 1537221915, 24 | "finished": 1537222041, 25 | "created": 1537221914, 26 | "updated": 1537221915, 27 | "version": 3, 28 | "stages": [ 29 | { 30 | "id": 1, 31 | "build_id": 1, 32 | "number": 1, 33 | "name": "linux-amd64", 34 | "status": "success", 35 | "errignore": false, 36 | "exit_code": 0, 37 | "machine": "2e0d5b2c7ff4", 38 | "os": "linux", 39 | "arch": "amd64", 40 | "started": 1537221916, 41 | "stopped": 1537221969, 42 | "created": 1537221914, 43 | "updated": 1537221969, 44 | "version": 4, 45 | "on_success": true, 46 | "on_failure": false, 47 | "steps": [ 48 | { 49 | "id": 4, 50 | "step_id": 1, 51 | "number": 1, 52 | "name": "clone", 53 | "status": "success", 54 | "exit_code": 0, 55 | "started": 1537221916, 56 | "stopped": 1537221918, 57 | "version": 4 58 | }, 59 | { 60 | "id": 5, 61 | "step_id": 1, 62 | "number": 2, 63 | "name": "test", 64 | "status": "success", 65 | "exit_code": 0, 66 | "started": 1537221918, 67 | "stopped": 1537221929, 68 | "version": 4 69 | }, 70 | { 71 | "id": 6, 72 | "step_id": 1, 73 | "number": 3, 74 | "name": "push", 75 | "status": "success", 76 | "exit_code": 0, 77 | "started": 1537221929, 78 | "stopped": 1537221969, 79 | "version": 4 80 | } 81 | ] 82 | }, 83 | { 84 | "id": 2, 85 | "build_id": 1, 86 | "number": 2, 87 | "name": "linux-arm64", 88 | "status": "success", 89 | "errignore": false, 90 | "exit_code": 0, 91 | "machine": "56bcfb7f1207", 92 | "os": "linux", 93 | "arch": "arm64", 94 | "started": 1537221915, 95 | "stopped": 1537221969, 96 | "created": 1537221914, 97 | "updated": 1537221969, 98 | "version": 4, 99 | "on_success": true, 100 | "on_failure": false, 101 | "steps": [ 102 | { 103 | "id": 1, 104 | "step_id": 2, 105 | "number": 1, 106 | "name": "clone", 107 | "status": "success", 108 | "exit_code": 0, 109 | "started": 1537221916, 110 | "stopped": 1537221918, 111 | "version": 4 112 | }, 113 | { 114 | "id": 2, 115 | "step_id": 2, 116 | "number": 2, 117 | "name": "test", 118 | "status": "success", 119 | "exit_code": 0, 120 | "started": 1537221918, 121 | "stopped": 1537221930, 122 | "version": 4 123 | }, 124 | { 125 | "id": 3, 126 | "step_id": 2, 127 | "number": 3, 128 | "name": "push", 129 | "status": "success", 130 | "exit_code": 0, 131 | "started": 1537221930, 132 | "stopped": 1537221968, 133 | "version": 4 134 | } 135 | ] 136 | }, 137 | { 138 | "id": 3, 139 | "build_id": 1, 140 | "number": 3, 141 | "name": "linux-arm", 142 | "status": "success", 143 | "errignore": false, 144 | "exit_code": 0, 145 | "machine": "f01265229b89", 146 | "os": "linux", 147 | "arch": "arm", 148 | "started": 1537221917, 149 | "stopped": 1537222024, 150 | "created": 1537221914, 151 | "updated": 1537222024, 152 | "version": 4, 153 | "on_success": true, 154 | "on_failure": false, 155 | "steps": [ 156 | { 157 | "id": 7, 158 | "step_id": 3, 159 | "number": 1, 160 | "name": "clone", 161 | "status": "success", 162 | "exit_code": 0, 163 | "started": 1537221918, 164 | "stopped": 1537221922, 165 | "version": 4 166 | }, 167 | { 168 | "id": 8, 169 | "step_id": 3, 170 | "number": 2, 171 | "name": "test", 172 | "status": "success", 173 | "exit_code": 0, 174 | "started": 1537221922, 175 | "stopped": 1537221937, 176 | "version": 4 177 | }, 178 | { 179 | "id": 9, 180 | "step_id": 3, 181 | "number": 3, 182 | "name": "push", 183 | "status": "success", 184 | "exit_code": 0, 185 | "started": 1537221937, 186 | "stopped": 1537222017, 187 | "version": 4 188 | } 189 | ] 190 | }, 191 | { 192 | "id": 4, 193 | "build_id": 1, 194 | "number": 4, 195 | "name": "after", 196 | "status": "success", 197 | "errignore": false, 198 | "exit_code": 0, 199 | "machine": "2e0d5b2c7ff4", 200 | "os": "linux", 201 | "arch": "amd64", 202 | "started": 1537222025, 203 | "stopped": 1537222041, 204 | "created": 1537221914, 205 | "updated": 1537222041, 206 | "version": 5, 207 | "on_success": true, 208 | "on_failure": false, 209 | "depends_on": [ 210 | "linux-arm", 211 | "linux-arm64", 212 | "linux-amd64" 213 | ], 214 | "steps": [ 215 | { 216 | "id": 10, 217 | "step_id": 4, 218 | "number": 1, 219 | "name": "clone", 220 | "status": "success", 221 | "exit_code": 0, 222 | "started": 1537222025, 223 | "stopped": 1537222027, 224 | "version": 4 225 | }, 226 | { 227 | "id": 11, 228 | "step_id": 4, 229 | "number": 2, 230 | "name": "manifest", 231 | "status": "success", 232 | "exit_code": 0, 233 | "started": 1537222027, 234 | "stopped": 1537222040, 235 | "version": 4 236 | } 237 | ] 238 | } 239 | ] 240 | } -------------------------------------------------------------------------------- /drone/testdata/builds.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "repo_id": 168, 5 | "trigger": "@hook", 6 | "number": 1, 7 | "status": "success", 8 | "event": "push", 9 | "action": "", 10 | "link": "https://github.com/drone/drone-git/compare/61e0cb11bbb1...6783f72b40ea", 11 | "timestamp": 0, 12 | "message": "format yaml", 13 | "before": "", 14 | "after": "6783f72b40ea0af776d2ec40ef7e01c58b4794c3", 15 | "ref": "refs/heads/master", 16 | "source_repo": "", 17 | "source": "master", 18 | "target": "master", 19 | "author_login": "octocat", 20 | "author_name": "The Octocat", 21 | "author_email": "octocat@github.com", 22 | "author_avatar": "https://avatars1.githubusercontent.com/u/817538?v=4", 23 | "sender": "octocat", 24 | "started": 1537221915, 25 | "finished": 1537222041, 26 | "created": 1537221914, 27 | "updated": 1537221915, 28 | "version": 3 29 | } 30 | ] -------------------------------------------------------------------------------- /drone/testdata/builds.json.golden: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "repo_id": 168, 5 | "trigger": "@hook", 6 | "number": 1, 7 | "status": "success", 8 | "event": "push", 9 | "action": "", 10 | "link": "https://github.com/drone/drone-git/compare/61e0cb11bbb1...6783f72b40ea", 11 | "timestamp": 0, 12 | "message": "format yaml", 13 | "before": "", 14 | "after": "6783f72b40ea0af776d2ec40ef7e01c58b4794c3", 15 | "ref": "refs/heads/master", 16 | "source_repo": "", 17 | "source": "master", 18 | "target": "master", 19 | "author_login": "octocat", 20 | "author_name": "The Octocat", 21 | "author_email": "octocat@github.com", 22 | "author_avatar": "https://avatars1.githubusercontent.com/u/817538?v=4", 23 | "sender": "octocat", 24 | "started": 1537221915, 25 | "finished": 1537222041, 26 | "created": 1537221914, 27 | "updated": 1537221915, 28 | "version": 3 29 | } 30 | ] -------------------------------------------------------------------------------- /drone/testdata/cron.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "repo_id": 47, 4 | "name": "every-10-minutes", 5 | "expr": "0 */5 * * * *", 6 | "next": 1537463700, 7 | "prev": 1537463400, 8 | "event": "push", 9 | "branch": "master", 10 | "disabled": false, 11 | "created": 1537302981, 12 | "updated": 1537302981, 13 | "version": 0 14 | } -------------------------------------------------------------------------------- /drone/testdata/cron.json.golden: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "repo_id": 47, 4 | "name": "every-10-minutes", 5 | "expr": "0 */5 * * * *", 6 | "next": 1537463700, 7 | "prev": 1537463400, 8 | "event": "push", 9 | "branch": "master", 10 | "disabled": false, 11 | "created": 1537302981, 12 | "updated": 1537302981, 13 | "version": 0 14 | } -------------------------------------------------------------------------------- /drone/testdata/crons.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 3, 4 | "repo_id": 47, 5 | "name": "every-10-minutes", 6 | "expr": "0 */5 * * * *", 7 | "next": 1537463700, 8 | "prev": 1537463400, 9 | "event": "push", 10 | "branch": "master", 11 | "disabled": false, 12 | "created": 1537302981, 13 | "updated": 1537302981, 14 | "version": 0 15 | } 16 | ] -------------------------------------------------------------------------------- /drone/testdata/crons.json.golden: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 3, 4 | "repo_id": 47, 5 | "name": "every-10-minutes", 6 | "expr": "0 */5 * * * *", 7 | "next": 1537463700, 8 | "prev": 1537463400, 9 | "event": "push", 10 | "branch": "master", 11 | "disabled": false, 12 | "created": 1537302981, 13 | "updated": 1537302981, 14 | "version": 0 15 | } 16 | ] -------------------------------------------------------------------------------- /drone/testdata/logs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pos": 0, 4 | "out": "Initialized empty Git repository in /drone/src/github.com/octocat/hello-world/.git/\n", 5 | "time": 0 6 | }, 7 | { 8 | "pos": 1, 9 | "out": "+ git fetch origin +refs/heads/master:\n", 10 | "time": 0 11 | }, 12 | { 13 | "pos": 2, 14 | "out": "From https://github.com/octocat/hello-world\n", 15 | "time": 0 16 | }, 17 | { 18 | "pos": 3, 19 | "out": " * branch master -\u003e FETCH_HEAD\n", 20 | "time": 0 21 | }, 22 | { 23 | "pos": 4, 24 | "out": " * [new branch] master -\u003e origin/master\n", 25 | "time": 0 26 | }, 27 | { 28 | "pos": 5, 29 | "out": "+ git checkout 6783f72b40ea0af776d2ec40ef7e01c58b4794c3 -b master\n", 30 | "time": 1 31 | }, 32 | { 33 | "pos": 6, 34 | "out": "Already on 'master'\n", 35 | "time": 1 36 | } 37 | ] -------------------------------------------------------------------------------- /drone/testdata/logs.json.golden: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pos": 0, 4 | "out": "Initialized empty Git repository in /drone/src/github.com/octocat/hello-world/.git/\n", 5 | "time": 0 6 | }, 7 | { 8 | "pos": 1, 9 | "out": "+ git fetch origin +refs/heads/master:\n", 10 | "time": 0 11 | }, 12 | { 13 | "pos": 2, 14 | "out": "From https://github.com/octocat/hello-world\n", 15 | "time": 0 16 | }, 17 | { 18 | "pos": 3, 19 | "out": " * branch master -\u003e FETCH_HEAD\n", 20 | "time": 0 21 | }, 22 | { 23 | "pos": 4, 24 | "out": " * [new branch] master -\u003e origin/master\n", 25 | "time": 0 26 | }, 27 | { 28 | "pos": 5, 29 | "out": "+ git checkout 6783f72b40ea0af776d2ec40ef7e01c58b4794c3 -b master\n", 30 | "time": 1 31 | }, 32 | { 33 | "pos": 6, 34 | "out": "Already on 'master'\n", 35 | "time": 1 36 | } 37 | ] -------------------------------------------------------------------------------- /drone/testdata/repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 181, 3 | "uid": "13933572", 4 | "user_id": 2, 5 | "namespace": "octocat", 6 | "name": "hello-world", 7 | "slug": "octocat/hello-world", 8 | "scm": "", 9 | "git_http_url": "https://github.com/octocat/hello-world.git", 10 | "git_ssh_url": "git@github.com:octocat/hello-world.git", 11 | "link": "https://github.com/octocat/hello-world", 12 | "default_branch": "master", 13 | "private": true, 14 | "visibility": "private", 15 | "active": true, 16 | "config_path": ".drone.yml", 17 | "trusted": false, 18 | "protected": false, 19 | "timeout": 60, 20 | "counter": 1, 21 | "synced": 1537221753, 22 | "created": 1537221753, 23 | "updated": 1537221753, 24 | "version": 3 25 | } -------------------------------------------------------------------------------- /drone/testdata/repo.json.golden: -------------------------------------------------------------------------------- 1 | { 2 | "id": 181, 3 | "uid": "13933572", 4 | "user_id": 2, 5 | "namespace": "octocat", 6 | "name": "hello-world", 7 | "slug": "octocat/hello-world", 8 | "scm": "", 9 | "git_http_url": "https://github.com/octocat/hello-world.git", 10 | "git_ssh_url": "git@github.com:octocat/hello-world.git", 11 | "link": "https://github.com/octocat/hello-world", 12 | "default_branch": "master", 13 | "private": true, 14 | "visibility": "private", 15 | "active": true, 16 | "config_path": ".drone.yml", 17 | "trusted": false, 18 | "protected": false, 19 | "timeout": 60, 20 | "counter": 1, 21 | "synced": 1537221753, 22 | "created": 1537221753, 23 | "updated": 1537221753, 24 | "version": 3 25 | } -------------------------------------------------------------------------------- /drone/testdata/repos.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 181, 4 | "uid": "13933572", 5 | "user_id": 2, 6 | "namespace": "octocat", 7 | "name": "hello-world", 8 | "slug": "octocat/hello-world", 9 | "scm": "", 10 | "git_http_url": "https://github.com/octocat/hello-world.git", 11 | "git_ssh_url": "git@github.com:octocat/hello-world.git", 12 | "link": "https://github.com/octocat/hello-world", 13 | "default_branch": "master", 14 | "private": true, 15 | "visibility": "private", 16 | "active": true, 17 | "config_path": ".drone.yml", 18 | "trusted": false, 19 | "protected": false, 20 | "timeout": 60, 21 | "counter": 1, 22 | "synced": 1537221753, 23 | "created": 1537221753, 24 | "updated": 1537221753, 25 | "version": 3 26 | } 27 | ] -------------------------------------------------------------------------------- /drone/testdata/repos.json.golden: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 181, 4 | "uid": "13933572", 5 | "user_id": 2, 6 | "namespace": "octocat", 7 | "name": "hello-world", 8 | "slug": "octocat/hello-world", 9 | "scm": "", 10 | "git_http_url": "https://github.com/octocat/hello-world.git", 11 | "git_ssh_url": "git@github.com:octocat/hello-world.git", 12 | "link": "https://github.com/octocat/hello-world", 13 | "default_branch": "master", 14 | "private": true, 15 | "visibility": "private", 16 | "active": true, 17 | "config_path": ".drone.yml", 18 | "trusted": false, 19 | "protected": false, 20 | "timeout": 60, 21 | "counter": 1, 22 | "synced": 1537221753, 23 | "created": 1537221753, 24 | "updated": 1537221753, 25 | "version": 3 26 | } 27 | ] -------------------------------------------------------------------------------- /drone/testdata/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "login": "octocat", 4 | "email": "octocat@github.com", 5 | "machine": false, 6 | "admin": true, 7 | "active": true, 8 | "avatar": "https://avatars1.githubusercontent.com/u/817538?v=4", 9 | "syncing": false, 10 | "synced": 1537375999, 11 | "created": 1537221751, 12 | "updated": 1537221751, 13 | "last_login": 1537221751 14 | } -------------------------------------------------------------------------------- /drone/testdata/user.json.golden: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "login": "octocat", 4 | "email": "octocat@github.com", 5 | "machine": false, 6 | "admin": true, 7 | "active": true, 8 | "avatar": "https://avatars1.githubusercontent.com/u/817538?v=4", 9 | "syncing": false, 10 | "synced": 1537375999, 11 | "created": 1537221751, 12 | "updated": 1537221751, 13 | "last_login": 1537221751 14 | } -------------------------------------------------------------------------------- /drone/testdata/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 2, 4 | "login": "bradrydzewski", 5 | "email": "", 6 | "machine": false, 7 | "admin": true, 8 | "active": true, 9 | "avatar": "https://avatars1.githubusercontent.com/u/817538?v=4", 10 | "syncing": false, 11 | "synced": 1537375999, 12 | "created": 1537221751, 13 | "updated": 1537221751, 14 | "last_login": 1537221751 15 | } 16 | ] -------------------------------------------------------------------------------- /drone/testdata/users.json.golden: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 2, 4 | "login": "bradrydzewski", 5 | "email": "", 6 | "machine": false, 7 | "admin": true, 8 | "active": true, 9 | "avatar": "https://avatars1.githubusercontent.com/u/817538?v=4", 10 | "syncing": false, 11 | "synced": 1537375999, 12 | "created": 1537221751, 13 | "updated": 1537221751, 14 | "last_login": 1537221751 15 | } 16 | ] -------------------------------------------------------------------------------- /drone/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package drone 16 | 17 | import "encoding/json" 18 | 19 | type ( 20 | // User represents a user account. 21 | User struct { 22 | ID int64 `json:"id"` 23 | Login string `json:"login"` 24 | Email string `json:"email"` 25 | Avatar string `json:"avatar_url"` 26 | Active bool `json:"active"` 27 | Admin bool `json:"admin"` 28 | Machine bool `json:"machine"` 29 | Syncing bool `json:"syncing"` 30 | Synced int64 `json:"synced"` 31 | Created int64 `json:"created"` 32 | Updated int64 `json:"updated"` 33 | LastLogin int64 `json:"last_login"` 34 | Token string `json:"token"` 35 | } 36 | 37 | // UserPatch defines a user patch request. 38 | UserPatch struct { 39 | Active *bool `json:"active,omitempty"` 40 | Admin *bool `json:"admin,omitempty"` 41 | Machine *bool `json:"machine,omitempty"` 42 | Token *string `json:"token,omitempty"` 43 | } 44 | 45 | // Repo represents a repository. 46 | Repo struct { 47 | ID int64 `json:"id"` 48 | UID string `json:"uid"` 49 | UserID int64 `json:"user_id"` 50 | Namespace string `json:"namespace"` 51 | Name string `json:"name"` 52 | Slug string `json:"slug"` 53 | SCM string `json:"scm"` 54 | HTTPURL string `json:"git_http_url"` 55 | SSHURL string `json:"git_ssh_url"` 56 | Link string `json:"link"` 57 | Branch string `json:"default_branch"` 58 | Private bool `json:"private"` 59 | Visibility string `json:"visibility"` 60 | Active bool `json:"active"` 61 | Config string `json:"config_path"` 62 | Trusted bool `json:"trusted"` 63 | Protected bool `json:"protected"` 64 | IgnoreForks bool `json:"ignore_forks"` 65 | IgnorePulls bool `json:"ignore_pull_requests"` 66 | CancelPulls bool `json:"auto_cancel_pull_requests"` 67 | CancelPush bool `json:"auto_cancel_pushes"` 68 | CancelRunning bool `json:"auto_cancel_running"` 69 | Throttle int64 `json:"throttle"` 70 | Timeout int64 `json:"timeout"` 71 | Counter int64 `json:"counter"` 72 | Synced int64 `json:"synced"` 73 | Created int64 `json:"created"` 74 | Updated int64 `json:"updated"` 75 | Version int64 `json:"version"` 76 | Signer string `json:"signer,omitempty"` 77 | Secret string `json:"secret,omitempty"` 78 | Build Build `json:"build,omitempty"` 79 | } 80 | 81 | RepoBuildStage struct { 82 | RepoNamespace string `json:"repo_namespace"` 83 | RepoName string `json:"repo_name"` 84 | RepoSlug string `json:"repo_slug"` 85 | BuildNumber int64 `json:"build_number"` 86 | BuildAuthor string `json:"build_author"` 87 | BuildAuthorName string `json:"build_author_name"` 88 | BuildAuthorEmail string `json:"build_author_email"` 89 | BuildAuthorAvatar string `json:"build_author_avatar"` 90 | BuildSender string `json:"build_sender"` 91 | BuildStarted int64 `json:"build_started"` 92 | BuildFinished int64 `json:"build_finished"` 93 | BuildCreated int64 `json:"build_created"` 94 | BuildUpdated int64 `json:"build_updated"` 95 | StageName string `json:"stage_name"` 96 | StageKind string `json:"stage_kind"` 97 | StageType string `json:"stage_type"` 98 | StageStatus string `json:"stage_status"` 99 | StageMachine string `json:"stage_machine"` 100 | StageOS string `json:"stage_os"` 101 | StageArch string `json:"stage_arch"` 102 | StageVariant string `json:"stage_variant"` 103 | StageKernel string `json:"stage_kernel"` 104 | StageLimit string `json:"stage_limit"` 105 | StageLimitRepo string `json:"stage_limit_repo"` 106 | StageStarted int64 `json:"stage_started"` 107 | StageStopped int64 `json:"stage_stopped"` 108 | } 109 | 110 | // RepoPatch defines a repository patch request. 111 | RepoPatch struct { 112 | Config *string `json:"config_path,omitempty"` 113 | Protected *bool `json:"protected,omitempty"` 114 | Trusted *bool `json:"trusted,omitempty"` 115 | Throttle *int64 `json:"throttle,omitempty"` 116 | Timeout *int64 `json:"timeout,omitempty"` 117 | Visibility *string `json:"visibility,omitempty"` 118 | IgnoreForks *bool `json:"ignore_forks"` 119 | IgnorePulls *bool `json:"ignore_pull_requests"` 120 | CancelPulls *bool `json:"auto_cancel_pull_requests"` 121 | CancelPush *bool `json:"auto_cancel_pushes"` 122 | CancelRunning *bool `json:"auto_cancel_running"` 123 | Counter *int64 `json:"counter,omitempty"` 124 | } 125 | 126 | // Build defines a build object. 127 | Build struct { 128 | ID int64 `json:"id"` 129 | RepoID int64 `json:"repo_id"` 130 | Trigger string `json:"trigger"` 131 | Number int64 `json:"number"` 132 | Parent int64 `json:"parent,omitempty"` 133 | Status string `json:"status"` 134 | Error string `json:"error,omitempty"` 135 | Event string `json:"event"` 136 | Action string `json:"action"` 137 | Link string `json:"link"` 138 | Timestamp int64 `json:"timestamp"` 139 | Title string `json:"title,omitempty"` 140 | Message string `json:"message"` 141 | Before string `json:"before"` 142 | After string `json:"after"` 143 | Ref string `json:"ref"` 144 | Fork string `json:"source_repo"` 145 | Source string `json:"source"` 146 | Target string `json:"target"` 147 | Author string `json:"author_login"` 148 | AuthorName string `json:"author_name"` 149 | AuthorEmail string `json:"author_email"` 150 | AuthorAvatar string `json:"author_avatar"` 151 | Sender string `json:"sender"` 152 | Params map[string]string `json:"params,omitempty"` 153 | Cron string `json:"cron,omitempty"` 154 | Deploy string `json:"deploy_to,omitempty"` 155 | DeployID int64 `json:"deploy_id,omitempty"` 156 | Debug bool `json:"debug"` 157 | Started int64 `json:"started"` 158 | Finished int64 `json:"finished"` 159 | Created int64 `json:"created"` 160 | Updated int64 `json:"updated"` 161 | Version int64 `json:"version"` 162 | Stages []*Stage `json:"stages,omitempty"` 163 | } 164 | 165 | // Stage represents a stage of build execution. 166 | Stage struct { 167 | ID int64 `json:"id"` 168 | BuildID int64 `json:"build_id"` 169 | Number int `json:"number"` 170 | Name string `json:"name"` 171 | Kind string `json:"kind,omitempty"` 172 | Type string `json:"type,omitempty"` 173 | Status string `json:"status"` 174 | Error string `json:"error,omitempty"` 175 | ErrIgnore bool `json:"errignore"` 176 | ExitCode int `json:"exit_code"` 177 | Machine string `json:"machine,omitempty"` 178 | OS string `json:"os"` 179 | Arch string `json:"arch"` 180 | Variant string `json:"variant,omitempty"` 181 | Kernel string `json:"kernel,omitempty"` 182 | Limit int `json:"limit,omitempty"` 183 | LimitRepo int `json:"throttle,omitempty"` 184 | Started int64 `json:"started"` 185 | Stopped int64 `json:"stopped"` 186 | Created int64 `json:"created"` 187 | Updated int64 `json:"updated"` 188 | Version int64 `json:"version"` 189 | OnSuccess bool `json:"on_success"` 190 | OnFailure bool `json:"on_failure"` 191 | DependsOn []string `json:"depends_on,omitempty"` 192 | Labels map[string]string `json:"labels,omitempty"` 193 | Steps []*Step `json:"steps,omitempty"` 194 | } 195 | 196 | // Step represents an individual step in the stage. 197 | Step struct { 198 | ID int64 `json:"id"` 199 | StageID int64 `json:"step_id"` 200 | Number int `json:"number"` 201 | Name string `json:"name"` 202 | Status string `json:"status"` 203 | Error string `json:"error,omitempty"` 204 | ErrIgnore bool `json:"errignore,omitempty"` 205 | ExitCode int `json:"exit_code"` 206 | Started int64 `json:"started,omitempty"` 207 | Stopped int64 `json:"stopped,omitempty"` 208 | Version int64 `json:"version"` 209 | DependsOn []string `json:"depends_on,omitempty"` 210 | Image string `json:"image,omitempty"` 211 | Detached bool `json:"detached,omitempty"` 212 | Schema string `json:"schema,omitempty"` 213 | } 214 | 215 | // Registry represents a docker registry with credentials. 216 | // DEPRECATED 217 | Registry struct { 218 | Address string `json:"address"` 219 | Username string `json:"username"` 220 | Password string `json:"password,omitempty"` 221 | Email string `json:"email"` 222 | Token string `json:"token"` 223 | Policy string `json:"policy,omitempty"` 224 | } 225 | 226 | // Secret represents a secret variable, such as a password or token. 227 | Secret struct { 228 | Namespace string `json:"namespace,omitempty"` 229 | Name string `json:"name,omitempty"` 230 | Data string `json:"data,omitempty"` 231 | PullRequest bool `json:"pull_request,omitempty"` 232 | PullRequestPush bool `json:"pull_request_push,omitempty"` 233 | 234 | // Deprecated. 235 | Pull bool `json:"pull,omitempty"` 236 | Fork bool `json:"fork,omitempty"` 237 | } 238 | 239 | Template struct { 240 | Name string `json:"name,omitempty"` 241 | Data string `json:"data,omitempty"` 242 | } 243 | 244 | // Server represents a server node. 245 | Server struct { 246 | ID string `json:"id"` 247 | Provider string `json:"provider"` 248 | State string `json:"state"` 249 | Name string `json:"name"` 250 | Image string `json:"image"` 251 | Region string `json:"region"` 252 | Size string `json:"size"` 253 | Address string `json:"address"` 254 | Capacity int `json:"capacity"` 255 | Secret string `json:"secret"` 256 | Error string `json:"error"` 257 | CAKey []byte `json:"ca_key"` 258 | CACert []byte `json:"ca_cert"` 259 | TLSKey []byte `json:"tls_key"` 260 | TLSCert []byte `json:"tls_cert"` 261 | Created int64 `json:"created"` 262 | Updated int64 `json:"updated"` 263 | Started int64 `json:"started"` 264 | Stopped int64 `json:"stopped"` 265 | } 266 | 267 | // Cron represents a cron job. 268 | Cron struct { 269 | ID int64 `json:"id"` 270 | RepoID int64 `json:"repo_id"` 271 | Name string `json:"name"` 272 | Expr string `json:"expr"` 273 | Next int64 `json:"next"` 274 | Prev int64 `json:"prev"` 275 | Event string `json:"event"` 276 | Branch string `json:"branch"` 277 | Target string `json:"target"` 278 | Disabled bool `json:"disabled"` 279 | Created int64 `json:"created"` 280 | Updated int64 `json:"updated"` 281 | } 282 | 283 | // CronPatch defines a cron patch request. 284 | CronPatch struct { 285 | Event *string `json:"event"` 286 | Branch *string `json:"branch"` 287 | Target *string `json:"target"` 288 | Disabled *bool `json:"disabled"` 289 | } 290 | 291 | // Line represents a line of container logs. 292 | Line struct { 293 | Number int `json:"pos"` 294 | Message string `json:"out"` 295 | Timestamp int64 `json:"time"` 296 | } 297 | 298 | // Config represents a config file. 299 | Config struct { 300 | Data string `json:"data"` 301 | Kind string `json:"kind"` 302 | } 303 | 304 | // Version provides system version details. 305 | Version struct { 306 | Source string `json:"source,omitempty"` 307 | Version string `json:"version,omitempty"` 308 | Commit string `json:"commit,omitempty"` 309 | } 310 | 311 | // System stores system information. 312 | System struct { 313 | Proto string `json:"proto,omitempty"` 314 | Host string `json:"host,omitempty"` 315 | Link string `json:"link,omitempty"` 316 | Version string `json:"version,omitempty"` 317 | } 318 | 319 | // Node provides node details. 320 | Node struct { 321 | ID int64 `json:"id"` 322 | UID string `json:"uid"` 323 | Provider string `json:"provider"` 324 | State string `json:"state"` 325 | Name string `json:"name"` 326 | Image string `json:"image"` 327 | Region string `json:"region"` 328 | Size string `json:"size"` 329 | OS string `json:"os"` 330 | Arch string `json:"arch"` 331 | Kernel string `json:"kernel"` 332 | Variant string `json:"variant"` 333 | Address string `json:"address"` 334 | Capacity int `json:"capacity"` 335 | Filters []string `json:"filters"` 336 | Labels map[string]string `json:"labels"` 337 | Error string `json:"error"` 338 | CAKey []byte `json:"ca_key"` 339 | CACert []byte `json:"ca_cert"` 340 | TLSKey []byte `json:"tls_key"` 341 | TLSCert []byte `json:"tls_cert"` 342 | TLSName string `json:"tls_name"` 343 | Paused bool `json:"paused"` 344 | Protected bool `json:"protected"` 345 | Created int64 `json:"created"` 346 | Updated int64 `json:"updated"` 347 | } 348 | 349 | // NodePatch defines a node patch request. 350 | NodePatch struct { 351 | UID *string `json:"uid"` 352 | Provider *string `json:"provider"` 353 | State *string `json:"state"` 354 | Image *string `json:"image"` 355 | Region *string `json:"region"` 356 | Size *string `json:"size"` 357 | Address *string `json:"address"` 358 | Capacity *int `json:"capacity"` 359 | Filters *[]string `json:"filters"` 360 | Labels *map[string]string `json:"labels"` 361 | Error *string `json:"error"` 362 | CAKey *[]byte `json:"ca_key"` 363 | CACert *[]byte `json:"ca_cert"` 364 | TLSKey *[]byte `json:"tls_key"` 365 | TLSCert *[]byte `json:"tls_cert"` 366 | Paused *bool `json:"paused"` 367 | Protected *bool `json:"protected"` 368 | } 369 | 370 | // Netrc contains login and initialization information used 371 | // by an automated login process. 372 | Netrc struct { 373 | Machine string `json:"machine"` 374 | Login string `json:"login"` 375 | Password string `json:"password"` 376 | } 377 | 378 | // Token provides an access and refresh token. 379 | Token struct { 380 | Access string `json:"access"` 381 | Refresh string `json:"refresh"` 382 | } 383 | 384 | // CardInput provides adaptive card schema and data. 385 | CardInput struct { 386 | Schema string `json:"schema"` 387 | Data json.RawMessage `json:"data"` 388 | } 389 | ) 390 | 391 | // Error represents a json-encoded API error. 392 | type Error struct { 393 | Code int `json:"code"` 394 | Message string `json:"message"` 395 | } 396 | 397 | func (e *Error) Error() string { 398 | return e.Message 399 | } 400 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/drone/drone-go 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e 7 | github.com/google/go-cmp v0.2.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc= 2 | github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY= 3 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 4 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 5 | -------------------------------------------------------------------------------- /plugin/admission/admission.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package admission 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | ) 22 | 23 | // V1 is version 1 of the admission API 24 | const V1 = "application/vnd.drone.admission.v1+json" 25 | 26 | // Admission event types. 27 | const ( 28 | EventLogin = "login" 29 | EventRegister = "register" 30 | ) 31 | 32 | type ( 33 | // Request defines an admission request. 34 | Request struct { 35 | Event string `json:"event,omitempty"` 36 | User drone.User `json:"user,omitempty"` 37 | Token drone.Token `json:"token,omitempty"` 38 | } 39 | 40 | // Plugin responds to a admission request. 41 | Plugin interface { 42 | Admit(context.Context, *Request) (*drone.User, error) 43 | } 44 | ) 45 | -------------------------------------------------------------------------------- /plugin/admission/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package admission 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | "github.com/drone/drone-go/plugin/internal/client" 22 | ) 23 | 24 | // Client returns a new plugin client. 25 | func Client(endpoint, secret string, skipverify bool) Plugin { 26 | client := client.New(endpoint, secret, skipverify) 27 | client.Accept = V1 28 | return &pluginClient{ 29 | client: client, 30 | } 31 | } 32 | 33 | type pluginClient struct { 34 | client *client.Client 35 | } 36 | 37 | func (c *pluginClient) Admit(ctx context.Context, in *Request) (*drone.User, error) { 38 | res := new(drone.User) 39 | err := c.client.Do(ctx, in, res) 40 | return res, err 41 | } 42 | -------------------------------------------------------------------------------- /plugin/admission/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package admission 16 | 17 | import ( 18 | "encoding/json" 19 | "io/ioutil" 20 | "net/http" 21 | 22 | "github.com/drone/drone-go/plugin/logger" 23 | 24 | "github.com/99designs/httpsignatures-go" 25 | ) 26 | 27 | // Handler returns a http.Handler that accepts JSON-encoded 28 | // HTTP requests for a user, invokes the underlying admission 29 | // plugin, and writes the JSON-encoded config to the HTTP response. 30 | // 31 | // The handler verifies the authenticity of the HTTP request 32 | // using the http-signature, and returns a 400 Bad Request if 33 | // the signature is missing or invalid. 34 | // 35 | // The handler can optionally encrypt the response body using 36 | // aesgcm if the HTTP request includes the Accept-Encoding header 37 | // set to aesgcm. 38 | func Handler(plugin Plugin, secret string, logs logger.Logger) http.Handler { 39 | handler := &handler{ 40 | secret: secret, 41 | plugin: plugin, 42 | logger: logs, 43 | } 44 | if handler.logger == nil { 45 | handler.logger = logger.Discard() 46 | } 47 | return handler 48 | } 49 | 50 | type handler struct { 51 | secret string 52 | plugin Plugin 53 | logger logger.Logger 54 | } 55 | 56 | func (p *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 57 | signature, err := httpsignatures.FromRequest(r) 58 | if err != nil { 59 | p.logger.Debugf("admission: invalid or missing signature in http.Request") 60 | http.Error(w, "Invalid or Missing Signature", http.StatusBadRequest) 61 | return 62 | } 63 | if !signature.IsValid(p.secret, r) { 64 | p.logger.Debugf("admission: invalid signature in http.Request") 65 | http.Error(w, "Invalid Signature", http.StatusBadRequest) 66 | return 67 | } 68 | 69 | body, err := ioutil.ReadAll(r.Body) 70 | if err != nil { 71 | p.logger.Debugf("admission: cannot read http.Request body") 72 | w.WriteHeader(http.StatusBadRequest) 73 | return 74 | } 75 | 76 | req := &Request{} 77 | err = json.Unmarshal(body, req) 78 | if err != nil { 79 | p.logger.Debugf("admission: cannot unmarshal http.Request body") 80 | http.Error(w, "Invalid Input", http.StatusBadRequest) 81 | return 82 | } 83 | 84 | res, err := p.plugin.Admit(r.Context(), req) 85 | if err != nil { 86 | p.logger.Debugf("admission: denied: %s: %s", 87 | req.User.Login, 88 | err, 89 | ) 90 | http.Error(w, err.Error(), http.StatusForbidden) 91 | return 92 | } 93 | if res == nil { 94 | w.WriteHeader(http.StatusNoContent) 95 | return 96 | } 97 | out, _ := json.Marshal(res) 98 | w.Header().Set("Content-Type", "application/json") 99 | w.Write(out) 100 | } 101 | -------------------------------------------------------------------------------- /plugin/admission/handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package admission 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "errors" 22 | "net/http" 23 | "net/http/httptest" 24 | "strings" 25 | "testing" 26 | "time" 27 | 28 | "github.com/drone/drone-go/drone" 29 | 30 | "github.com/99designs/httpsignatures-go" 31 | ) 32 | 33 | func TestHandler(t *testing.T) { 34 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 35 | 36 | buf := new(bytes.Buffer) 37 | json.NewEncoder(buf).Encode(&Request{}) 38 | 39 | res := httptest.NewRecorder() 40 | req := httptest.NewRequest("GET", "/", buf) 41 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 42 | 43 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 44 | if err != nil { 45 | t.Error(err) 46 | return 47 | } 48 | 49 | want := &drone.User{ 50 | Login: "octocat", 51 | Admin: false, 52 | } 53 | plugin := &mockPlugin{ 54 | res: want, 55 | err: nil, 56 | } 57 | 58 | handler := Handler(plugin, key, nil) 59 | handler.ServeHTTP(res, req) 60 | 61 | if got, want := res.Code, 200; got != want { 62 | t.Errorf("Want status code %d, got %d", want, got) 63 | } 64 | 65 | resp := &drone.User{} 66 | json.Unmarshal(res.Body.Bytes(), resp) 67 | if got, want := resp.Login, want.Login; got != want { 68 | t.Errorf("Want user data %q, got %q", want, got) 69 | } 70 | } 71 | 72 | func TestHandler_NoContent(t *testing.T) { 73 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 74 | 75 | buf := new(bytes.Buffer) 76 | json.NewEncoder(buf).Encode(&Request{}) 77 | 78 | res := httptest.NewRecorder() 79 | req := httptest.NewRequest("GET", "/", buf) 80 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 81 | 82 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 83 | if err != nil { 84 | t.Error(err) 85 | return 86 | } 87 | 88 | plugin := &mockPlugin{ 89 | res: nil, 90 | err: nil, 91 | } 92 | 93 | handler := Handler(plugin, key, nil) 94 | handler.ServeHTTP(res, req) 95 | 96 | if got, want := res.Code, http.StatusNoContent; got != want { 97 | t.Errorf("Want status code %d, got %d", want, got) 98 | } 99 | } 100 | 101 | func TestHandler_AccessDenied(t *testing.T) { 102 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 103 | 104 | buf := new(bytes.Buffer) 105 | json.NewEncoder(buf).Encode(&Request{}) 106 | 107 | res := httptest.NewRecorder() 108 | req := httptest.NewRequest("GET", "/", buf) 109 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 110 | 111 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 112 | if err != nil { 113 | t.Error(err) 114 | return 115 | } 116 | 117 | plugin := &mockPlugin{ 118 | res: nil, 119 | err: errors.New("access denied"), 120 | } 121 | 122 | handler := Handler(plugin, key, nil) 123 | handler.ServeHTTP(res, req) 124 | 125 | if got, want := res.Code, 403; got != want { 126 | t.Errorf("Want status code %d, got %d", want, got) 127 | } 128 | 129 | got, want := strings.TrimSpace(res.Body.String()), plugin.err.Error() 130 | if got != want { 131 | t.Errorf("Want error %q, got %q", want, got) 132 | } 133 | } 134 | 135 | func TestHandler_MissingSignature(t *testing.T) { 136 | res := httptest.NewRecorder() 137 | req := httptest.NewRequest("GET", "/", nil) 138 | 139 | handler := Handler(nil, "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil) 140 | handler.ServeHTTP(res, req) 141 | 142 | got, want := res.Body.String(), "Invalid or Missing Signature\n" 143 | if got != want { 144 | t.Errorf("Want response body %q, got %q", want, got) 145 | } 146 | } 147 | 148 | func TestHandler_InvalidSignature(t *testing.T) { 149 | sig := `keyId="hmac-key",algorithm="hmac-sha256",signature="QrS16+RlRsFjXn5IVW8tWz+3ZRAypjpNgzehEuvJksk=",headers="(request-target) accept accept-encoding content-type date digest"` 150 | res := httptest.NewRecorder() 151 | req := httptest.NewRequest("GET", "/", nil) 152 | req.Header.Set("Signature", sig) 153 | 154 | handler := Handler(nil, "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil) 155 | handler.ServeHTTP(res, req) 156 | 157 | got, want := res.Body.String(), "Invalid Signature\n" 158 | if got != want { 159 | t.Errorf("Want response body %q, got %q", want, got) 160 | } 161 | } 162 | 163 | type mockPlugin struct { 164 | res *drone.User 165 | err error 166 | } 167 | 168 | func (m *mockPlugin) Admit(ctx context.Context, req *Request) (*drone.User, error) { 169 | return m.res, m.err 170 | } 171 | -------------------------------------------------------------------------------- /plugin/config/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | "github.com/drone/drone-go/plugin/internal/client" 22 | ) 23 | 24 | // Client returns a new plugin client. 25 | func Client(endpoint, secret string, skipverify bool) Plugin { 26 | client := client.New(endpoint, secret, skipverify) 27 | client.Accept = V1 28 | return &pluginClient{ 29 | client: client, 30 | } 31 | } 32 | 33 | type pluginClient struct { 34 | client *client.Client 35 | } 36 | 37 | func (c *pluginClient) Find(ctx context.Context, in *Request) (*drone.Config, error) { 38 | res := new(drone.Config) 39 | err := c.client.Do(ctx, in, res) 40 | return res, err 41 | } 42 | -------------------------------------------------------------------------------- /plugin/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | ) 22 | 23 | // V1 is version 1 of the configuration API 24 | const V1 = "application/vnd.drone.config.v1+json" 25 | 26 | type ( 27 | // Request defines a configuration request. 28 | Request struct { 29 | Build drone.Build `json:"build,omitempty"` 30 | Repo drone.Repo `json:"repo,omitempty"` 31 | Token drone.Token `json:"token,omitempty"` 32 | } 33 | 34 | // Plugin responds to a configuration request. 35 | Plugin interface { 36 | Find(context.Context, *Request) (*drone.Config, error) 37 | } 38 | ) 39 | -------------------------------------------------------------------------------- /plugin/config/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "encoding/json" 19 | "io/ioutil" 20 | "net/http" 21 | 22 | "github.com/drone/drone-go/plugin/logger" 23 | 24 | "github.com/99designs/httpsignatures-go" 25 | ) 26 | 27 | // Handler returns a http.Handler that accepts JSON-encoded 28 | // HTTP requests for a config file, invokes the underlying config 29 | // plugin, and writes the JSON-encoded config to the HTTP response. 30 | // 31 | // The handler verifies the authenticity of the HTTP request 32 | // using the http-signature, and returns a 400 Bad Request if 33 | // the signature is missing or invalid. 34 | // 35 | // The handler can optionally encrypt the response body using 36 | // aesgcm if the HTTP request includes the Accept-Encoding header 37 | // set to aesgcm. 38 | func Handler(plugin Plugin, secret string, logs logger.Logger) http.Handler { 39 | handler := &handler{ 40 | secret: secret, 41 | plugin: plugin, 42 | logger: logs, 43 | } 44 | if handler.logger == nil { 45 | handler.logger = logger.Discard() 46 | } 47 | return handler 48 | } 49 | 50 | type handler struct { 51 | secret string 52 | plugin Plugin 53 | logger logger.Logger 54 | } 55 | 56 | func (p *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 57 | signature, err := httpsignatures.FromRequest(r) 58 | if err != nil { 59 | p.logger.Debugf("config: invalid or missing signature in http.Request") 60 | http.Error(w, "Invalid or Missing Signature", http.StatusBadRequest) 61 | return 62 | } 63 | if !signature.IsValid(p.secret, r) { 64 | p.logger.Debugf("config: invalid signature in http.Request") 65 | http.Error(w, "Invalid Signature", http.StatusBadRequest) 66 | return 67 | } 68 | 69 | body, err := ioutil.ReadAll(r.Body) 70 | if err != nil { 71 | p.logger.Debugf("config: cannot read http.Request body") 72 | w.WriteHeader(http.StatusBadRequest) 73 | return 74 | } 75 | 76 | req := &Request{} 77 | err = json.Unmarshal(body, req) 78 | if err != nil { 79 | p.logger.Debugf("config: cannot unmarshal http.Request body") 80 | http.Error(w, "Invalid Input", http.StatusBadRequest) 81 | return 82 | } 83 | 84 | res, err := p.plugin.Find(r.Context(), req) 85 | if err != nil { 86 | p.logger.Debugf("config: cannot find configuration: %s: %s: %s", 87 | req.Repo.Slug, 88 | req.Build.Target, 89 | err, 90 | ) 91 | http.Error(w, err.Error(), http.StatusNotFound) 92 | return 93 | } 94 | if res == nil { 95 | w.WriteHeader(http.StatusNoContent) 96 | return 97 | } 98 | out, _ := json.Marshal(res) 99 | w.Header().Set("Content-Type", "application/json") 100 | _, _ = w.Write(out) 101 | } 102 | -------------------------------------------------------------------------------- /plugin/config/handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "net/http" 22 | "net/http/httptest" 23 | "testing" 24 | "time" 25 | 26 | "github.com/drone/drone-go/drone" 27 | 28 | "github.com/99designs/httpsignatures-go" 29 | ) 30 | 31 | func TestHandler(t *testing.T) { 32 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 33 | 34 | buf := new(bytes.Buffer) 35 | json.NewEncoder(buf).Encode(&Request{}) 36 | 37 | res := httptest.NewRecorder() 38 | req := httptest.NewRequest("GET", "/", buf) 39 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 40 | 41 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 42 | if err != nil { 43 | t.Error(err) 44 | return 45 | } 46 | 47 | want := &drone.Config{ 48 | Kind: "drone.v1.yaml", 49 | Data: "pipeilne: []", 50 | } 51 | plugin := &mockPlugin{ 52 | res: want, 53 | err: nil, 54 | } 55 | 56 | handler := Handler(plugin, key, nil) 57 | handler.ServeHTTP(res, req) 58 | 59 | if got, want := res.Code, 200; got != want { 60 | t.Errorf("Want status code %d, got %d", want, got) 61 | } 62 | 63 | resp := &drone.Config{} 64 | json.Unmarshal(res.Body.Bytes(), resp) 65 | if got, want := resp.Data, want.Data; got != want { 66 | t.Errorf("Want configuration data %s, got %s", want, got) 67 | } 68 | } 69 | 70 | func TestHandler_MissingSignature(t *testing.T) { 71 | res := httptest.NewRecorder() 72 | req := httptest.NewRequest("GET", "/", nil) 73 | 74 | handler := Handler(nil, "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil) 75 | handler.ServeHTTP(res, req) 76 | 77 | got, want := res.Body.String(), "Invalid or Missing Signature\n" 78 | if got != want { 79 | t.Errorf("Want response body %q, got %q", want, got) 80 | } 81 | } 82 | 83 | func TestHandler_InvalidSignature(t *testing.T) { 84 | sig := `keyId="hmac-key",algorithm="hmac-sha256",signature="QrS16+RlRsFjXn5IVW8tWz+3ZRAypjpNgzehEuvJksk=",headers="(request-target) accept accept-encoding content-type date digest"` 85 | res := httptest.NewRecorder() 86 | req := httptest.NewRequest("GET", "/", nil) 87 | req.Header.Set("Signature", sig) 88 | 89 | handler := Handler(nil, "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil) 90 | handler.ServeHTTP(res, req) 91 | 92 | got, want := res.Body.String(), "Invalid Signature\n" 93 | if got != want { 94 | t.Errorf("Want response body %q, got %q", want, got) 95 | } 96 | } 97 | 98 | type mockPlugin struct { 99 | res *drone.Config 100 | err error 101 | } 102 | 103 | func (m *mockPlugin) Find(ctx context.Context, req *Request) (*drone.Config, error) { 104 | return m.res, m.err 105 | } 106 | -------------------------------------------------------------------------------- /plugin/converter/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package converter 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | "github.com/drone/drone-go/plugin/internal/client" 22 | ) 23 | 24 | // Client returns a new plugin client. 25 | func Client(endpoint, secret string, skipverify bool) Plugin { 26 | client := client.New(endpoint, secret, skipverify) 27 | client.Accept = V1 28 | return &pluginClient{ 29 | client: client, 30 | } 31 | } 32 | 33 | type pluginClient struct { 34 | client *client.Client 35 | } 36 | 37 | func (c *pluginClient) Convert(ctx context.Context, in *Request) (*drone.Config, error) { 38 | res := new(drone.Config) 39 | err := c.client.Do(ctx, in, res) 40 | return res, err 41 | } 42 | -------------------------------------------------------------------------------- /plugin/converter/converter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package converter 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | ) 22 | 23 | // V1 is version 1 of the converter API 24 | const V1 = "application/vnd.drone.convert.v1+json" 25 | 26 | type ( 27 | // Request defines a converter request. 28 | Request struct { 29 | Build drone.Build `json:"build,omitempty"` 30 | Config drone.Config `json:"config,omitempty"` 31 | Repo drone.Repo `json:"repo,omitempty"` 32 | Token drone.Token `json:"token,omitempty"` 33 | } 34 | 35 | // Plugin responds to a converter request. 36 | Plugin interface { 37 | Convert(context.Context, *Request) (*drone.Config, error) 38 | } 39 | ) 40 | -------------------------------------------------------------------------------- /plugin/converter/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package converter 16 | 17 | import ( 18 | "encoding/json" 19 | "io/ioutil" 20 | "net/http" 21 | 22 | "github.com/drone/drone-go/plugin/logger" 23 | 24 | "github.com/99designs/httpsignatures-go" 25 | ) 26 | 27 | // Handler returns a http.Handler that accepts JSON-encoded 28 | // HTTP requests to convert the raw format to a yaml configuration 29 | // file, invokes the underlying plugin, and writes the 30 | // JSON-encoded config to the HTTP response. 31 | // 32 | // The handler verifies the authenticity of the HTTP request 33 | // using the http-signature, and returns a 400 Bad Request if 34 | // the signature is missing or invalid. 35 | // 36 | // The handler can optionally encrypt the response body using 37 | // aesgcm if the HTTP request includes the Accept-Encoding header 38 | // set to aesgcm. 39 | func Handler(plugin Plugin, secret string, logs logger.Logger) http.Handler { 40 | handler := &handler{ 41 | secret: secret, 42 | plugin: plugin, 43 | logger: logs, 44 | } 45 | if handler.logger == nil { 46 | handler.logger = logger.Discard() 47 | } 48 | return handler 49 | } 50 | 51 | type handler struct { 52 | secret string 53 | plugin Plugin 54 | logger logger.Logger 55 | } 56 | 57 | func (p *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 58 | signature, err := httpsignatures.FromRequest(r) 59 | if err != nil { 60 | p.logger.Debugf("converter: invalid or missing signature in http.Request") 61 | http.Error(w, "Invalid or Missing Signature", http.StatusBadRequest) 62 | return 63 | } 64 | if !signature.IsValid(p.secret, r) { 65 | p.logger.Debugf("converter: invalid signature in http.Request") 66 | http.Error(w, "Invalid Signature", http.StatusBadRequest) 67 | return 68 | } 69 | 70 | body, err := ioutil.ReadAll(r.Body) 71 | if err != nil { 72 | p.logger.Debugf("converter: cannot read http.Request body") 73 | w.WriteHeader(http.StatusBadRequest) 74 | return 75 | } 76 | 77 | req := &Request{} 78 | err = json.Unmarshal(body, req) 79 | if err != nil { 80 | p.logger.Debugf("converter: cannot unmarshal http.Request body") 81 | http.Error(w, "Invalid Input", http.StatusBadRequest) 82 | return 83 | } 84 | 85 | res, err := p.plugin.Convert(r.Context(), req) 86 | if err != nil { 87 | p.logger.Debugf("converter: cannot convert configuration: %s: %s: %s", 88 | req.Repo.Slug, 89 | req.Build.Target, 90 | err, 91 | ) 92 | http.Error(w, err.Error(), http.StatusNotFound) 93 | return 94 | } 95 | if res == nil { 96 | w.WriteHeader(http.StatusNoContent) 97 | return 98 | } 99 | out, _ := json.Marshal(res) 100 | w.Header().Set("Content-Type", "application/json") 101 | _, _ = w.Write(out) 102 | } 103 | -------------------------------------------------------------------------------- /plugin/converter/handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package converter 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "net/http" 22 | "net/http/httptest" 23 | "testing" 24 | "time" 25 | 26 | "github.com/drone/drone-go/drone" 27 | 28 | "github.com/99designs/httpsignatures-go" 29 | ) 30 | 31 | func TestHandler(t *testing.T) { 32 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 33 | 34 | buf := new(bytes.Buffer) 35 | json.NewEncoder(buf).Encode(&Request{}) 36 | 37 | res := httptest.NewRecorder() 38 | req := httptest.NewRequest("GET", "/", buf) 39 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 40 | 41 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 42 | if err != nil { 43 | t.Error(err) 44 | return 45 | } 46 | 47 | want := &drone.Config{ 48 | Kind: "drone.v1.yaml", 49 | Data: "pipeline: []", 50 | } 51 | plugin := &mockPlugin{ 52 | res: want, 53 | err: nil, 54 | } 55 | 56 | handler := Handler(plugin, key, nil) 57 | handler.ServeHTTP(res, req) 58 | 59 | if got, want := res.Code, 200; got != want { 60 | t.Errorf("Want status code %d, got %d", want, got) 61 | } 62 | 63 | resp := &drone.Config{} 64 | json.Unmarshal(res.Body.Bytes(), resp) 65 | if got, want := resp.Data, want.Data; got != want { 66 | t.Errorf("Want configuration data %s, got %s", want, got) 67 | } 68 | } 69 | 70 | func TestHandler_MissingSignature(t *testing.T) { 71 | res := httptest.NewRecorder() 72 | req := httptest.NewRequest("GET", "/", nil) 73 | 74 | handler := Handler(nil, "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil) 75 | handler.ServeHTTP(res, req) 76 | 77 | got, want := res.Body.String(), "Invalid or Missing Signature\n" 78 | if got != want { 79 | t.Errorf("Want response body %q, got %q", want, got) 80 | } 81 | } 82 | 83 | func TestHandler_InvalidSignature(t *testing.T) { 84 | sig := `keyId="hmac-key",algorithm="hmac-sha256",signature="QrS16+RlRsFjXn5IVW8tWz+3ZRAypjpNgzehEuvJksk=",headers="(request-target) accept accept-encoding content-type date digest"` 85 | res := httptest.NewRecorder() 86 | req := httptest.NewRequest("GET", "/", nil) 87 | req.Header.Set("Signature", sig) 88 | 89 | handler := Handler(nil, "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil) 90 | handler.ServeHTTP(res, req) 91 | 92 | got, want := res.Body.String(), "Invalid Signature\n" 93 | if got != want { 94 | t.Errorf("Want response body %q, got %q", want, got) 95 | } 96 | } 97 | 98 | type mockPlugin struct { 99 | res *drone.Config 100 | err error 101 | } 102 | 103 | func (m *mockPlugin) Convert(ctx context.Context, req *Request) (*drone.Config, error) { 104 | return m.res, m.err 105 | } 106 | -------------------------------------------------------------------------------- /plugin/environ/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package environ 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/plugin/internal/client" 21 | ) 22 | 23 | // Client returns a new plugin client. 24 | func Client(endpoint, secret string, skipverify bool) Plugin { 25 | client := client.New(endpoint, secret, skipverify) 26 | client.Accept = V2 27 | return &pluginClient{ 28 | client: client, 29 | } 30 | } 31 | 32 | type pluginClient struct { 33 | client *client.Client 34 | } 35 | 36 | func (c *pluginClient) List(ctx context.Context, in *Request) ([]*Variable, error) { 37 | res := []*Variable{} 38 | err := c.client.Do(ctx, in, &res) 39 | return res, err 40 | } 41 | -------------------------------------------------------------------------------- /plugin/environ/environ.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package environ 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | ) 22 | 23 | // V1 is version 1 of the env API 24 | const V1 = "application/vnd.drone.env.v1+json" 25 | 26 | // V2 is version 2 of the env API 27 | const V2 = "application/vnd.drone.env.v2+json" 28 | 29 | type ( 30 | // Request defines a environment request. 31 | Request struct { 32 | Repo drone.Repo `json:"repo,omitempty"` 33 | Build drone.Build `json:"build,omitempty"` 34 | } 35 | 36 | // Variable defines an environment variable. 37 | Variable struct { 38 | Name string `json:"name,omitempty"` 39 | Data string `json:"data,omitempty"` 40 | Mask bool `json:"mask,omitempty"` 41 | } 42 | 43 | // Plugin responds to a registry request. 44 | Plugin interface { 45 | List(context.Context, *Request) ([]*Variable, error) 46 | } 47 | ) 48 | -------------------------------------------------------------------------------- /plugin/environ/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package environ 16 | 17 | import ( 18 | "encoding/json" 19 | "io/ioutil" 20 | "net/http" 21 | 22 | "github.com/drone/drone-go/plugin/internal/aesgcm" 23 | "github.com/drone/drone-go/plugin/logger" 24 | 25 | "github.com/99designs/httpsignatures-go" 26 | ) 27 | 28 | // Handler returns a http.Handler that accepts JSON-encoded 29 | // HTTP requests for environment variables, invokes the underlying 30 | // plugin, and writes the JSON-encoded secret to the HTTP response. 31 | // 32 | // The handler verifies the authenticity of the HTTP request 33 | // using the http-signature, and returns a 400 Bad Request if 34 | // the signature is missing or invalid. 35 | // 36 | // The handler can optionally encrypt the response body using 37 | // aesgcm if the HTTP request includes the Accept-Encoding header 38 | // set to aesgcm. 39 | func Handler(secret string, plugin Plugin, logs logger.Logger) http.Handler { 40 | handler := &handler{ 41 | secret: secret, 42 | plugin: plugin, 43 | logger: logs, 44 | } 45 | if handler.logger == nil { 46 | handler.logger = logger.Discard() 47 | } 48 | return handler 49 | } 50 | 51 | type handler struct { 52 | secret string 53 | plugin Plugin 54 | logger logger.Logger 55 | } 56 | 57 | func (p *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 58 | signature, err := httpsignatures.FromRequest(r) 59 | if err != nil { 60 | p.logger.Debugf("environment: invalid or missing signature in http.Request") 61 | http.Error(w, "Invalid or Missing Signature", http.StatusBadRequest) 62 | return 63 | } 64 | if !signature.IsValid(p.secret, r) { 65 | p.logger.Debugf("environment: invalid signature in http.Request") 66 | http.Error(w, "Invalid Signature", http.StatusBadRequest) 67 | return 68 | } 69 | 70 | body, err := ioutil.ReadAll(r.Body) 71 | if err != nil { 72 | p.logger.Debugf("environment: cannot read http.Request body") 73 | w.WriteHeader(http.StatusBadRequest) 74 | return 75 | } 76 | 77 | req := &Request{} 78 | err = json.Unmarshal(body, req) 79 | if err != nil { 80 | p.logger.Debugf("environment: cannot unmarshal http.Request body") 81 | http.Error(w, "Invalid Input", http.StatusBadRequest) 82 | return 83 | } 84 | 85 | res, err := p.plugin.List(r.Context(), req) 86 | if err != nil { 87 | p.logger.Debugf("environment: cannot list registries: %s", err) 88 | http.Error(w, err.Error(), http.StatusNotFound) 89 | return 90 | } 91 | 92 | out, _ := json.Marshal(res) 93 | 94 | // If the client is a legacy V1 format we convert the 95 | // output from V2 output to V1 output. 96 | if r.Header.Get("Accept") == V1 { 97 | out, _ = json.Marshal(toMap(res)) 98 | } 99 | 100 | // If the client can optionally accept an encrypted 101 | // response, we encrypt the payload body using secretbox. 102 | if r.Header.Get("Accept-Encoding") == "aesgcm" { 103 | key, err := aesgcm.Key(p.secret) 104 | if err != nil { 105 | p.logger.Errorf("environment: invalid encryption key: %s", err) 106 | http.Error(w, err.Error(), http.StatusInternalServerError) 107 | return 108 | } 109 | out, err = aesgcm.Encrypt(out, key) 110 | if err != nil { 111 | p.logger.Errorf("environment: cannot encrypt message: %s", err) 112 | http.Error(w, err.Error(), http.StatusInternalServerError) 113 | return 114 | } 115 | w.Header().Set("Content-Encoding", "aesgcm") 116 | w.Header().Set("Content-Type", "application/octet-stream") 117 | } 118 | 119 | w.Write(out) 120 | } 121 | -------------------------------------------------------------------------------- /plugin/environ/handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package environ 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "net/http" 22 | "net/http/httptest" 23 | "testing" 24 | "time" 25 | 26 | "github.com/drone/drone-go/plugin/internal/aesgcm" 27 | 28 | "github.com/99designs/httpsignatures-go" 29 | "github.com/google/go-cmp/cmp" 30 | ) 31 | 32 | func TestHandler(t *testing.T) { 33 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 34 | 35 | buf := new(bytes.Buffer) 36 | json.NewEncoder(buf).Encode(&Request{}) 37 | 38 | res := httptest.NewRecorder() 39 | req := httptest.NewRequest("GET", "/", buf) 40 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 41 | 42 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 43 | if err != nil { 44 | t.Error(err) 45 | return 46 | } 47 | 48 | want := []*Variable{{Name: "PATH", Data: "/bin:/usr/bin"}} 49 | plugin := &mockPlugin{ 50 | res: want, 51 | err: nil, 52 | } 53 | 54 | handler := Handler(key, plugin, nil) 55 | handler.ServeHTTP(res, req) 56 | 57 | if got, want := res.Code, 200; got != want { 58 | t.Errorf("Want status code %d, got %d", want, got) 59 | } 60 | 61 | resp := []*Variable{} 62 | json.Unmarshal(res.Body.Bytes(), &resp) 63 | if got, want := len(resp), len(want); got != want { 64 | t.Errorf("Want %d environment variables, got %d", want, got) 65 | return 66 | } 67 | 68 | if diff := cmp.Diff(want, resp); diff != "" { 69 | t.Log(diff) 70 | t.Errorf("Unexpected response") 71 | } 72 | } 73 | 74 | func TestHandler_Encrypted(t *testing.T) { 75 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 76 | 77 | buf := new(bytes.Buffer) 78 | json.NewEncoder(buf).Encode(&Request{}) 79 | 80 | res := httptest.NewRecorder() 81 | req := httptest.NewRequest("GET", "/", buf) 82 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 83 | req.Header.Add("Accept-Encoding", "aesgcm") 84 | 85 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 86 | if err != nil { 87 | t.Error(err) 88 | return 89 | } 90 | 91 | want := []*Variable{{Name: "PATH", Data: "/bin:/usr/bin"}} 92 | plugin := &mockPlugin{ 93 | res: want, 94 | err: nil, 95 | } 96 | 97 | handler := Handler(key, plugin, nil) 98 | handler.ServeHTTP(res, req) 99 | 100 | if got, want := res.Code, 200; got != want { 101 | t.Errorf("Want status code %d, got %d", want, got) 102 | } 103 | if got, want := res.Header().Get("Content-Encoding"), "aesgcm"; got != want { 104 | t.Errorf("Want Content-Encoding %s, got %s", want, got) 105 | } 106 | if got, want := res.Header().Get("Content-Type"), "application/octet-stream"; got != want { 107 | t.Errorf("Want Content-Type %s, got %s", want, got) 108 | } 109 | 110 | keyb, err := aesgcm.Key(key) 111 | if err != nil { 112 | t.Error(err) 113 | return 114 | } 115 | body, err := aesgcm.Decrypt(res.Body.Bytes(), keyb) 116 | if err != nil { 117 | t.Error(err) 118 | return 119 | } 120 | 121 | resp := []*Variable{} 122 | json.Unmarshal(body, &resp) 123 | if got, want := len(resp), len(want); got != want { 124 | t.Errorf("Want %d environment variables, got %d", want, got) 125 | return 126 | } 127 | if diff := cmp.Diff(want, resp); diff != "" { 128 | t.Log(diff) 129 | t.Errorf("Unexpected response") 130 | } 131 | } 132 | 133 | func TestHandler_MissingSignature(t *testing.T) { 134 | res := httptest.NewRecorder() 135 | req := httptest.NewRequest("GET", "/", nil) 136 | 137 | handler := Handler("xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil, nil) 138 | handler.ServeHTTP(res, req) 139 | 140 | got, want := res.Body.String(), "Invalid or Missing Signature\n" 141 | if got != want { 142 | t.Errorf("Want response body %q, got %q", want, got) 143 | } 144 | } 145 | 146 | func TestHandler_InvalidSignature(t *testing.T) { 147 | sig := `keyId="hmac-key",algorithm="hmac-sha256",signature="QrS16+RlRsFjXn5IVW8tWz+3ZRAypjpNgzehEuvJksk=",headers="(request-target) accept accept-encoding content-type date digest"` 148 | res := httptest.NewRecorder() 149 | req := httptest.NewRequest("GET", "/", nil) 150 | req.Header.Set("Signature", sig) 151 | 152 | handler := Handler("xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil, nil) 153 | handler.ServeHTTP(res, req) 154 | 155 | got, want := res.Body.String(), "Invalid Signature\n" 156 | if got != want { 157 | t.Errorf("Want response body %q, got %q", want, got) 158 | } 159 | } 160 | 161 | type mockPlugin struct { 162 | res []*Variable 163 | err error 164 | } 165 | 166 | func (m *mockPlugin) List(ctx context.Context, req *Request) ([]*Variable, error) { 167 | return m.res, m.err 168 | } 169 | -------------------------------------------------------------------------------- /plugin/environ/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package environ 16 | 17 | // toMap is a helper function that converts a list of 18 | // variables to a map. 19 | func toMap(src []*Variable) map[string]string { 20 | dst := map[string]string{} 21 | for _, v := range src { 22 | dst[v.Name] = v.Data 23 | } 24 | return dst 25 | } 26 | -------------------------------------------------------------------------------- /plugin/environ/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package environ 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/google/go-cmp/cmp" 21 | ) 22 | 23 | func TestToMap(t *testing.T) { 24 | in := []*Variable{ 25 | { 26 | Name: "foo", 27 | Data: "bar", 28 | }, 29 | } 30 | want := map[string]string{ 31 | "foo": "bar", 32 | } 33 | got := toMap(in) 34 | if diff := cmp.Diff(want, got); diff != "" { 35 | t.Log(diff) 36 | t.Errorf("Unexpected map value") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugin/internal/aesgcm/aesgcm.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aesgcm 16 | 17 | import ( 18 | "crypto/aes" 19 | "crypto/cipher" 20 | "crypto/rand" 21 | "errors" 22 | "io" 23 | ) 24 | 25 | // error returned when the key length is less than the 26 | // required size of 32 bytes. 27 | var errInvalidKeyLength = errors.New("aesgcm: invalid key length") 28 | 29 | // Encrypt encrypts the plaintext with the provided key and 30 | // returns the raw, unencoded bytes. 31 | func Encrypt(plaintext []byte, key *[32]byte) (ciphertext []byte, err error) { 32 | block, err := aes.NewCipher(key[:]) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | gcm, err := cipher.NewGCM(block) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | nonce := make([]byte, gcm.NonceSize()) 43 | _, err = io.ReadFull(rand.Reader, nonce) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return gcm.Seal(nonce, nonce, plaintext, nil), nil 49 | } 50 | 51 | // Decrypt decrypts the raw, unencoded cihpertext with the provided key. 52 | func Decrypt(ciphertext []byte, key *[32]byte) (plaintext []byte, err error) { 53 | block, err := aes.NewCipher(key[:]) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | gcm, err := cipher.NewGCM(block) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | if len(ciphertext) < gcm.NonceSize() { 64 | return nil, errors.New("malformed ciphertext") 65 | } 66 | 67 | return gcm.Open(nil, 68 | ciphertext[:gcm.NonceSize()], 69 | ciphertext[gcm.NonceSize():], 70 | nil, 71 | ) 72 | } 73 | 74 | // Key returns an encryption key. 75 | func Key(s string) (*[32]byte, error) { 76 | if len(s) < 32 { 77 | return nil, errInvalidKeyLength 78 | } 79 | var key [32]byte 80 | copy(key[:], s) 81 | return &key, nil 82 | } 83 | -------------------------------------------------------------------------------- /plugin/internal/aesgcm/aesgcm_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aesgcm 16 | 17 | import ( 18 | "bytes" 19 | "testing" 20 | ) 21 | 22 | func TestEncryptDecrypt(t *testing.T) { 23 | key, err := Key("xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh") 24 | if err != nil { 25 | t.Error(err) 26 | return 27 | } 28 | 29 | message := []byte("top-secret") 30 | ciphertext, err := Encrypt(message, key) 31 | if err != nil { 32 | t.Error(err) 33 | return 34 | } 35 | 36 | plaintext, err := Decrypt(ciphertext, key) 37 | if err != nil { 38 | t.Error(err) 39 | return 40 | } 41 | 42 | if !bytes.Equal(message, plaintext) { 43 | t.Errorf("Expect secret encrypted and decrypted") 44 | } 45 | } 46 | 47 | func TestInvalidKey(t *testing.T) { 48 | _, err := Key("xVKAGlWQiY3sOp8J") 49 | if err != errInvalidKeyLength { 50 | t.Errorf("Want Invalid Key Length error") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /plugin/internal/client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package client 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "crypto/sha256" 21 | "crypto/tls" 22 | "encoding/base64" 23 | "encoding/json" 24 | "io" 25 | "io/ioutil" 26 | "net/http" 27 | "time" 28 | 29 | "github.com/drone/drone-go/drone" 30 | "github.com/drone/drone-go/plugin/internal/aesgcm" 31 | 32 | httpsignatures "github.com/99designs/httpsignatures-go" 33 | ) 34 | 35 | // DefaultClient is the default http.Client. 36 | var DefaultClient = &http.Client{ 37 | CheckRedirect: func(*http.Request, []*http.Request) error { 38 | return http.ErrUseLastResponse 39 | }, 40 | } 41 | 42 | // required http headers 43 | // note that (request-target) is disabled because reverse proxies, 44 | // including aws lambda with api gateway, fail verification. 45 | var headers = []string{ 46 | "accept", 47 | "accept-encoding", 48 | "content-type", 49 | "date", 50 | "digest", 51 | } 52 | 53 | var signer = httpsignatures.NewSigner( 54 | httpsignatures.AlgorithmHmacSha256, 55 | headers..., 56 | ) 57 | 58 | // New returns a new http.Client with signature verification. 59 | func New(endpoint, secret string, skipverify bool) *Client { 60 | client := &Client{ 61 | Accept: "application/json", 62 | Encoding: "identity", 63 | Endpoint: endpoint, 64 | Secret: secret, 65 | } 66 | if skipverify { 67 | client.Client = &http.Client{ 68 | CheckRedirect: func(*http.Request, []*http.Request) error { 69 | return http.ErrUseLastResponse 70 | }, 71 | Transport: &http.Transport{ 72 | Proxy: http.ProxyFromEnvironment, 73 | TLSClientConfig: &tls.Config{ 74 | InsecureSkipVerify: true, // user needs to explicitly enable this with skipverify=true 75 | }, 76 | }, 77 | } 78 | } 79 | return client 80 | } 81 | 82 | // Client wraps an http.Client and applies retry logic and 83 | // http signature verification. 84 | type Client struct { 85 | Client *http.Client 86 | Accept string 87 | Encoding string 88 | Endpoint string 89 | Secret string 90 | SkipVerify bool 91 | } 92 | 93 | // Do makes an http.Request to the target endpoint using the context provided. 94 | func (s *Client) Do(ctx context.Context, in, out interface{}) error { 95 | data, err := json.Marshal(in) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | buf := bytes.NewBuffer(data) 101 | req, err := http.NewRequest("POST", s.Endpoint, buf) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | req = req.WithContext(ctx) 107 | req.Header.Add("Accept", s.Accept) 108 | req.Header.Add("Accept-Encoding", s.Encoding) 109 | req.Header.Add("Content-Type", "application/json") 110 | req.Header.Add("Digest", "SHA-256="+digest(data)) 111 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 112 | err = signer.SignRequest("hmac-key", s.Secret, req) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | res, err := s.client().Do(req) 118 | if res != nil && res.Body != nil { 119 | defer func() { 120 | // drain the response body so we can reuse this connection. 121 | _, _ = io.Copy(ioutil.Discard, io.LimitReader(res.Body, 4096)) 122 | _ = res.Body.Close() 123 | }() 124 | } 125 | if err != nil { 126 | return err 127 | } 128 | 129 | body, err := ioutil.ReadAll(res.Body) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | if res.StatusCode > 299 { 135 | err := new(drone.Error) 136 | err.Code = res.StatusCode 137 | err.Message = string(body) 138 | 139 | // if the response body is empty we should return 140 | // the default status code text. 141 | if len(body) == 0 { 142 | err.Message = http.StatusText(res.StatusCode) 143 | } 144 | return err 145 | } 146 | 147 | // if the response body return no content we exit 148 | // immediately. We do not read or unmarshal the response 149 | // and we do not return an error. 150 | if res.StatusCode == http.StatusNoContent { 151 | return nil 152 | } 153 | 154 | // the response body may be optionally encrypted 155 | // using the aesgcm algorithm. If encrypted, 156 | // decrypt using the shared secret. 157 | if res.Header.Get("Content-Encoding") == "aesgcm" { 158 | secret, err := aesgcm.Key(s.Secret) 159 | if err != nil { 160 | return err 161 | } 162 | plaintext, err := aesgcm.Decrypt(body, secret) 163 | if err != nil { 164 | return err 165 | } 166 | body = plaintext 167 | } 168 | 169 | if out == nil { 170 | return nil 171 | } 172 | return json.Unmarshal(body, out) 173 | } 174 | 175 | func (s *Client) client() *http.Client { 176 | if s.Client == nil { 177 | return DefaultClient 178 | } 179 | return s.Client 180 | } 181 | 182 | func digest(data []byte) string { 183 | h := sha256.New() 184 | h.Write(data) 185 | return base64.StdEncoding.EncodeToString(h.Sum(nil)) 186 | } 187 | -------------------------------------------------------------------------------- /plugin/internal/internal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package internal 16 | -------------------------------------------------------------------------------- /plugin/logger/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logger 16 | 17 | // A Logger represents an active logging object that generates 18 | // lines of output to an io.Writer. 19 | type Logger interface { 20 | Debug(args ...interface{}) 21 | Debugf(format string, args ...interface{}) 22 | Debugln(args ...interface{}) 23 | 24 | Error(args ...interface{}) 25 | Errorf(format string, args ...interface{}) 26 | Errorln(args ...interface{}) 27 | 28 | Info(args ...interface{}) 29 | Infof(format string, args ...interface{}) 30 | Infoln(args ...interface{}) 31 | 32 | Warn(args ...interface{}) 33 | Warnf(format string, args ...interface{}) 34 | Warnln(args ...interface{}) 35 | } 36 | 37 | // Discard returns a no-op logger 38 | func Discard() Logger { 39 | return &discard{} 40 | } 41 | 42 | type discard struct{} 43 | 44 | func (*discard) Debug(args ...interface{}) {} 45 | func (*discard) Debugf(format string, args ...interface{}) {} 46 | func (*discard) Debugln(args ...interface{}) {} 47 | func (*discard) Error(args ...interface{}) {} 48 | func (*discard) Errorf(format string, args ...interface{}) {} 49 | func (*discard) Errorln(args ...interface{}) {} 50 | func (*discard) Info(args ...interface{}) {} 51 | func (*discard) Infof(format string, args ...interface{}) {} 52 | func (*discard) Infoln(args ...interface{}) {} 53 | func (*discard) Warn(args ...interface{}) {} 54 | func (*discard) Warnf(format string, args ...interface{}) {} 55 | func (*discard) Warnln(args ...interface{}) {} 56 | -------------------------------------------------------------------------------- /plugin/registry/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package registry 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | "github.com/drone/drone-go/plugin/internal/client" 22 | ) 23 | 24 | // Client returns a new plugin client. 25 | func Client(endpoint, secret string, skipverify bool) Plugin { 26 | client := client.New(endpoint, secret, skipverify) 27 | client.Accept = V1 28 | return &pluginClient{ 29 | client: client, 30 | } 31 | } 32 | 33 | type pluginClient struct { 34 | client *client.Client 35 | } 36 | 37 | func (c *pluginClient) List(ctx context.Context, in *Request) ([]*drone.Registry, error) { 38 | res := []*drone.Registry{} 39 | err := c.client.Do(ctx, in, &res) 40 | return res, err 41 | } 42 | -------------------------------------------------------------------------------- /plugin/registry/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package registry 16 | 17 | import ( 18 | "encoding/json" 19 | "io/ioutil" 20 | "net/http" 21 | 22 | "github.com/drone/drone-go/plugin/internal/aesgcm" 23 | "github.com/drone/drone-go/plugin/logger" 24 | 25 | "github.com/99designs/httpsignatures-go" 26 | ) 27 | 28 | // Handler returns a http.Handler that accepts JSON-encoded 29 | // HTTP requests for a secret, invokes the underlying secret 30 | // plugin, and writes the JSON-encoded secret to the HTTP response. 31 | // 32 | // The handler verifies the authenticity of the HTTP request 33 | // using the http-signature, and returns a 400 Bad Request if 34 | // the signature is missing or invalid. 35 | // 36 | // The handler can optionally encrypt the response body using 37 | // aesgcm if the HTTP request includes the Accept-Encoding header 38 | // set to aesgcm. 39 | func Handler(secret string, plugin Plugin, logs logger.Logger) http.Handler { 40 | handler := &handler{ 41 | secret: secret, 42 | plugin: plugin, 43 | logger: logs, 44 | } 45 | if handler.logger == nil { 46 | handler.logger = logger.Discard() 47 | } 48 | return handler 49 | } 50 | 51 | type handler struct { 52 | secret string 53 | plugin Plugin 54 | logger logger.Logger 55 | } 56 | 57 | func (p *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 58 | signature, err := httpsignatures.FromRequest(r) 59 | if err != nil { 60 | p.logger.Debugf("registry: invalid or missing signature in http.Request") 61 | http.Error(w, "Invalid or Missing Signature", http.StatusBadRequest) 62 | return 63 | } 64 | if !signature.IsValid(p.secret, r) { 65 | p.logger.Debugf("registry: invalid signature in http.Request") 66 | http.Error(w, "Invalid Signature", http.StatusBadRequest) 67 | return 68 | } 69 | 70 | body, err := ioutil.ReadAll(r.Body) 71 | if err != nil { 72 | p.logger.Debugf("registry: cannot read http.Request body") 73 | w.WriteHeader(http.StatusBadRequest) 74 | return 75 | } 76 | 77 | req := &Request{} 78 | err = json.Unmarshal(body, req) 79 | if err != nil { 80 | p.logger.Debugf("registry: cannot unmarshal http.Request body") 81 | http.Error(w, "Invalid Input", http.StatusBadRequest) 82 | return 83 | } 84 | 85 | auths, err := p.plugin.List(r.Context(), req) 86 | if err != nil { 87 | p.logger.Debugf("registry: cannot list registries: %s", err) 88 | http.Error(w, err.Error(), http.StatusNotFound) 89 | return 90 | } 91 | out, _ := json.Marshal(auths) 92 | 93 | // If the client can optionally accept an encrypted 94 | // response, we encrypt the payload body using secretbox. 95 | if r.Header.Get("Accept-Encoding") == "aesgcm" { 96 | key, err := aesgcm.Key(p.secret) 97 | if err != nil { 98 | p.logger.Errorf("registry: invalid encryption key: %s", err) 99 | http.Error(w, err.Error(), http.StatusInternalServerError) 100 | return 101 | } 102 | out, err = aesgcm.Encrypt(out, key) 103 | if err != nil { 104 | p.logger.Errorf("registry: cannot encrypt message: %s", err) 105 | http.Error(w, err.Error(), http.StatusInternalServerError) 106 | return 107 | } 108 | w.Header().Set("Content-Encoding", "aesgcm") 109 | w.Header().Set("Content-Type", "application/octet-stream") 110 | } 111 | 112 | w.Write(out) 113 | } 114 | -------------------------------------------------------------------------------- /plugin/registry/handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package registry 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "net/http" 22 | "net/http/httptest" 23 | "testing" 24 | "time" 25 | 26 | "github.com/drone/drone-go/drone" 27 | "github.com/drone/drone-go/plugin/internal/aesgcm" 28 | 29 | "github.com/99designs/httpsignatures-go" 30 | ) 31 | 32 | func TestHandler(t *testing.T) { 33 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 34 | 35 | buf := new(bytes.Buffer) 36 | json.NewEncoder(buf).Encode(&Request{}) 37 | 38 | res := httptest.NewRecorder() 39 | req := httptest.NewRequest("GET", "/", buf) 40 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 41 | 42 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 43 | if err != nil { 44 | t.Error(err) 45 | return 46 | } 47 | 48 | want := []*drone.Registry{ 49 | { 50 | Username: "docker_password", 51 | Password: "correct-horse-battery-staple", 52 | }, 53 | } 54 | plugin := &mockPlugin{ 55 | res: want, 56 | err: nil, 57 | } 58 | 59 | handler := Handler(key, plugin, nil) 60 | handler.ServeHTTP(res, req) 61 | 62 | if got, want := res.Code, 200; got != want { 63 | t.Errorf("Want status code %d, got %d", want, got) 64 | } 65 | 66 | resp := []*drone.Registry{} 67 | json.Unmarshal(res.Body.Bytes(), &resp) 68 | if got, want := len(resp), len(want); got != want { 69 | t.Errorf("Want %d registry credentials, got %d", want, got) 70 | return 71 | } 72 | if got, want := resp[0].Username, want[0].Username; got != want { 73 | t.Errorf("Want registry username %s, got %s", want, got) 74 | } 75 | if got, want := resp[0].Password, want[0].Password; got != want { 76 | t.Errorf("Want registry password %s, got %s", want, got) 77 | } 78 | } 79 | 80 | func TestHandler_Encrypted(t *testing.T) { 81 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 82 | 83 | buf := new(bytes.Buffer) 84 | json.NewEncoder(buf).Encode(&Request{}) 85 | 86 | res := httptest.NewRecorder() 87 | req := httptest.NewRequest("GET", "/", buf) 88 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 89 | req.Header.Add("Accept-Encoding", "aesgcm") 90 | 91 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 92 | if err != nil { 93 | t.Error(err) 94 | return 95 | } 96 | 97 | want := []*drone.Registry{ 98 | { 99 | Username: "docker_password", 100 | Password: "correct-horse-battery-staple", 101 | }, 102 | } 103 | plugin := &mockPlugin{ 104 | res: want, 105 | err: nil, 106 | } 107 | 108 | handler := Handler(key, plugin, nil) 109 | handler.ServeHTTP(res, req) 110 | 111 | if got, want := res.Code, 200; got != want { 112 | t.Errorf("Want status code %d, got %d", want, got) 113 | } 114 | if got, want := res.Header().Get("Content-Encoding"), "aesgcm"; got != want { 115 | t.Errorf("Want Content-Encoding %s, got %s", want, got) 116 | } 117 | if got, want := res.Header().Get("Content-Type"), "application/octet-stream"; got != want { 118 | t.Errorf("Want Content-Type %s, got %s", want, got) 119 | } 120 | 121 | keyb, err := aesgcm.Key(key) 122 | if err != nil { 123 | t.Error(err) 124 | return 125 | } 126 | body, err := aesgcm.Decrypt(res.Body.Bytes(), keyb) 127 | if err != nil { 128 | t.Error(err) 129 | return 130 | } 131 | 132 | resp := []*drone.Registry{} 133 | json.Unmarshal(body, &resp) 134 | if got, want := len(resp), len(want); got != want { 135 | t.Errorf("Want %d registry credentials, got %d", want, got) 136 | t.Errorf("Response body %s", res.Body) 137 | return 138 | } 139 | if got, want := resp[0].Username, want[0].Username; got != want { 140 | t.Errorf("Want registry username %s, got %s", want, got) 141 | } 142 | if got, want := resp[0].Password, want[0].Password; got != want { 143 | t.Errorf("Want registry password %s, got %s", want, got) 144 | } 145 | } 146 | 147 | func TestHandler_MissingSignature(t *testing.T) { 148 | res := httptest.NewRecorder() 149 | req := httptest.NewRequest("GET", "/", nil) 150 | 151 | handler := Handler("xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil, nil) 152 | handler.ServeHTTP(res, req) 153 | 154 | got, want := res.Body.String(), "Invalid or Missing Signature\n" 155 | if got != want { 156 | t.Errorf("Want response body %q, got %q", want, got) 157 | } 158 | } 159 | 160 | func TestHandler_InvalidSignature(t *testing.T) { 161 | sig := `keyId="hmac-key",algorithm="hmac-sha256",signature="QrS16+RlRsFjXn5IVW8tWz+3ZRAypjpNgzehEuvJksk=",headers="(request-target) accept accept-encoding content-type date digest"` 162 | res := httptest.NewRecorder() 163 | req := httptest.NewRequest("GET", "/", nil) 164 | req.Header.Set("Signature", sig) 165 | 166 | handler := Handler("xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil, nil) 167 | handler.ServeHTTP(res, req) 168 | 169 | got, want := res.Body.String(), "Invalid Signature\n" 170 | if got != want { 171 | t.Errorf("Want response body %q, got %q", want, got) 172 | } 173 | } 174 | 175 | type mockPlugin struct { 176 | res []*drone.Registry 177 | err error 178 | } 179 | 180 | func (m *mockPlugin) List(ctx context.Context, req *Request) ([]*drone.Registry, error) { 181 | return m.res, m.err 182 | } 183 | -------------------------------------------------------------------------------- /plugin/registry/registry.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package registry 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | ) 22 | 23 | // V1 is version 1 of the registry API 24 | const V1 = "application/vnd.drone.registry.v1+json" 25 | 26 | type ( 27 | // Request defines a registry request. 28 | Request struct { 29 | Repo drone.Repo `json:"repo,omitempty"` 30 | Build drone.Build `json:"build,omitempty"` 31 | } 32 | 33 | // Plugin responds to a registry request. 34 | Plugin interface { 35 | List(context.Context, *Request) ([]*drone.Registry, error) 36 | } 37 | ) 38 | -------------------------------------------------------------------------------- /plugin/secret/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package secret 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | "github.com/drone/drone-go/plugin/internal/client" 22 | ) 23 | 24 | // Client returns a new plugin client. 25 | func Client(endpoint, secret string, skipverify bool) Plugin { 26 | client := client.New(endpoint, secret, skipverify) 27 | client.Accept = V1 28 | return &pluginClient{ 29 | client: client, 30 | } 31 | } 32 | 33 | type pluginClient struct { 34 | client *client.Client 35 | } 36 | 37 | func (c *pluginClient) Find(ctx context.Context, in *Request) (*drone.Secret, error) { 38 | res := new(drone.Secret) 39 | err := c.client.Do(ctx, in, res) 40 | return res, err 41 | } 42 | -------------------------------------------------------------------------------- /plugin/secret/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package secret 16 | 17 | import ( 18 | "encoding/json" 19 | "io/ioutil" 20 | "net/http" 21 | 22 | "github.com/drone/drone-go/plugin/internal/aesgcm" 23 | "github.com/drone/drone-go/plugin/logger" 24 | 25 | "github.com/99designs/httpsignatures-go" 26 | ) 27 | 28 | // Handler returns a http.Handler that accepts JSON-encoded 29 | // HTTP requests for a secret, invokes the underlying secret 30 | // plugin, and writes the JSON-encoded secret to the HTTP response. 31 | // 32 | // The handler verifies the authenticity of the HTTP request 33 | // using the http-signature, and returns a 400 Bad Request if 34 | // the signature is missing or invalid. 35 | // 36 | // The handler can optionally encrypt the response body using 37 | // aesgcm if the HTTP request includes the Accept-Encoding header 38 | // set to aesgcm. 39 | func Handler(secret string, plugin Plugin, logs logger.Logger) http.Handler { 40 | handler := &handler{ 41 | secret: secret, 42 | plugin: plugin, 43 | logger: logs, 44 | } 45 | if handler.logger == nil { 46 | handler.logger = logger.Discard() 47 | } 48 | return handler 49 | } 50 | 51 | type handler struct { 52 | secret string 53 | plugin Plugin 54 | logger logger.Logger 55 | } 56 | 57 | func (p *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 58 | signature, err := httpsignatures.FromRequest(r) 59 | if err != nil { 60 | p.logger.Debugf("secrets: invalid or missing signature in http.Request") 61 | http.Error(w, "Invalid or Missing Signature", http.StatusBadRequest) 62 | return 63 | } 64 | if !signature.IsValid(p.secret, r) { 65 | p.logger.Debugf("secrets: invalid signature in http.Request") 66 | http.Error(w, "Invalid Signature", http.StatusBadRequest) 67 | return 68 | } 69 | 70 | body, err := ioutil.ReadAll(r.Body) 71 | if err != nil { 72 | p.logger.Debugf("secrets: cannot read http.Request body") 73 | w.WriteHeader(http.StatusBadRequest) 74 | return 75 | } 76 | 77 | req := &Request{} 78 | err = json.Unmarshal(body, req) 79 | if err != nil { 80 | p.logger.Debugf("secrets: cannot unmarshal http.Request body") 81 | http.Error(w, "Invalid Input", http.StatusBadRequest) 82 | return 83 | } 84 | 85 | secret, err := p.plugin.Find(r.Context(), req) 86 | if err != nil { 87 | p.logger.Debugf("secrets: cannot find secret %s: %s", req.Name, err) 88 | http.Error(w, err.Error(), http.StatusNotFound) 89 | return 90 | } 91 | out, _ := json.Marshal(secret) 92 | 93 | // If the client can optionally accept an encrypted 94 | // response, we encrypt the payload body using secretbox. 95 | if r.Header.Get("Accept-Encoding") == "aesgcm" { 96 | key, err := aesgcm.Key(p.secret) 97 | if err != nil { 98 | p.logger.Errorf("secrets: invalid encryption key: %s", err) 99 | http.Error(w, err.Error(), http.StatusInternalServerError) 100 | return 101 | } 102 | out, err = aesgcm.Encrypt(out, key) 103 | if err != nil { 104 | p.logger.Errorf("secrets: cannot encrypt message: %s", err) 105 | http.Error(w, err.Error(), http.StatusInternalServerError) 106 | return 107 | } 108 | w.Header().Set("Content-Encoding", "aesgcm") 109 | w.Header().Set("Content-Type", "application/octet-stream") 110 | } 111 | 112 | w.Write(out) 113 | } 114 | -------------------------------------------------------------------------------- /plugin/secret/handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package secret 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "net/http" 22 | "net/http/httptest" 23 | "testing" 24 | "time" 25 | 26 | "github.com/drone/drone-go/drone" 27 | "github.com/drone/drone-go/plugin/internal/aesgcm" 28 | 29 | "github.com/99designs/httpsignatures-go" 30 | ) 31 | 32 | func TestHandler(t *testing.T) { 33 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 34 | 35 | buf := new(bytes.Buffer) 36 | json.NewEncoder(buf).Encode(&Request{ 37 | Name: "docker_password", 38 | }) 39 | 40 | res := httptest.NewRecorder() 41 | req := httptest.NewRequest("GET", "/", buf) 42 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 43 | 44 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 45 | if err != nil { 46 | t.Error(err) 47 | return 48 | } 49 | 50 | want := &drone.Secret{ 51 | Name: "docker_password", 52 | Data: "correct-horse-battery-staple", 53 | } 54 | plugin := &mockPlugin{ 55 | res: want, 56 | err: nil, 57 | } 58 | 59 | handler := Handler(key, plugin, nil) 60 | handler.ServeHTTP(res, req) 61 | 62 | if got, want := res.Code, 200; got != want { 63 | t.Errorf("Want status code %d, got %d", want, got) 64 | } 65 | 66 | resp := &drone.Secret{} 67 | json.Unmarshal(res.Body.Bytes(), resp) 68 | if got, want := resp.Name, want.Name; got != want { 69 | t.Errorf("Want secret name %s, got %s", want, got) 70 | } 71 | } 72 | 73 | func TestHandler_Encrypted(t *testing.T) { 74 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 75 | 76 | buf := new(bytes.Buffer) 77 | json.NewEncoder(buf).Encode(&Request{ 78 | Name: "docker_password", 79 | }) 80 | 81 | res := httptest.NewRecorder() 82 | req := httptest.NewRequest("GET", "/", buf) 83 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 84 | req.Header.Add("Accept-Encoding", "aesgcm") 85 | 86 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 87 | if err != nil { 88 | t.Error(err) 89 | return 90 | } 91 | 92 | want := &drone.Secret{ 93 | Name: "docker_password", 94 | Data: "correct-horse-battery-staple", 95 | } 96 | plugin := &mockPlugin{ 97 | res: want, 98 | err: nil, 99 | } 100 | 101 | handler := Handler(key, plugin, nil) 102 | handler.ServeHTTP(res, req) 103 | 104 | if got, want := res.Code, 200; got != want { 105 | t.Errorf("Want status code %d, got %d", want, got) 106 | } 107 | if got, want := res.Header().Get("Content-Encoding"), "aesgcm"; got != want { 108 | t.Errorf("Want Content-Encoding %s, got %s", want, got) 109 | } 110 | if got, want := res.Header().Get("Content-Type"), "application/octet-stream"; got != want { 111 | t.Errorf("Want Content-Type %s, got %s", want, got) 112 | } 113 | 114 | keyb, err := aesgcm.Key(key) 115 | if err != nil { 116 | t.Error(err) 117 | return 118 | } 119 | body, err := aesgcm.Decrypt(res.Body.Bytes(), keyb) 120 | if err != nil { 121 | t.Error(err) 122 | return 123 | } 124 | 125 | resp := &drone.Secret{} 126 | json.Unmarshal(body, resp) 127 | if got, want := resp.Name, want.Name; got != want { 128 | t.Errorf("Want secret name %s, got %s", want, got) 129 | } 130 | } 131 | 132 | func TestHandler_MissingSignature(t *testing.T) { 133 | res := httptest.NewRecorder() 134 | req := httptest.NewRequest("GET", "/", nil) 135 | 136 | handler := Handler("xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil, nil) 137 | handler.ServeHTTP(res, req) 138 | 139 | got, want := res.Body.String(), "Invalid or Missing Signature\n" 140 | if got != want { 141 | t.Errorf("Want response body %q, got %q", want, got) 142 | } 143 | } 144 | 145 | func TestHandler_InvalidSignature(t *testing.T) { 146 | sig := `keyId="hmac-key",algorithm="hmac-sha256",signature="QrS16+RlRsFjXn5IVW8tWz+3ZRAypjpNgzehEuvJksk=",headers="(request-target) accept accept-encoding content-type date digest"` 147 | res := httptest.NewRecorder() 148 | req := httptest.NewRequest("GET", "/", nil) 149 | req.Header.Set("Signature", sig) 150 | 151 | handler := Handler("xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil, nil) 152 | handler.ServeHTTP(res, req) 153 | 154 | got, want := res.Body.String(), "Invalid Signature\n" 155 | if got != want { 156 | t.Errorf("Want response body %q, got %q", want, got) 157 | } 158 | } 159 | 160 | type mockPlugin struct { 161 | res *drone.Secret 162 | err error 163 | } 164 | 165 | func (m *mockPlugin) Find(ctx context.Context, req *Request) (*drone.Secret, error) { 166 | return m.res, m.err 167 | } 168 | -------------------------------------------------------------------------------- /plugin/secret/secret.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package secret 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | ) 22 | 23 | // V1 is version 1 of the secrets API 24 | const V1 = "application/vnd.drone.secret.v1+json" 25 | 26 | type ( 27 | // Request defines a secret request. 28 | Request struct { 29 | Path string `json:"path"` 30 | Name string `json:"name"` 31 | Repo drone.Repo `json:"repo,omitempty"` 32 | Build drone.Build `json:"build,omitempty"` 33 | } 34 | 35 | // Plugin responds to a secret request. 36 | Plugin interface { 37 | Find(context.Context, *Request) (*drone.Secret, error) 38 | } 39 | ) 40 | -------------------------------------------------------------------------------- /plugin/validator/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package validator 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | "github.com/drone/drone-go/plugin/internal/client" 22 | ) 23 | 24 | // Client returns a new plugin client. 25 | func Client(endpoint, secret string, skipverify bool) Plugin { 26 | client := client.New(endpoint, secret, skipverify) 27 | client.Accept = V1 28 | return &pluginClient{ 29 | client: client, 30 | } 31 | } 32 | 33 | type pluginClient struct { 34 | client *client.Client 35 | } 36 | 37 | func (c *pluginClient) Validate(ctx context.Context, in *Request) error { 38 | err := c.client.Do(ctx, in, nil) 39 | if xerr, ok := err.(*drone.Error); ok { 40 | if xerr.Code == httpStatusSkip { 41 | return ErrSkip 42 | } 43 | if xerr.Code == httpStatusBlock { 44 | return ErrBlock 45 | } 46 | } 47 | return err 48 | } 49 | -------------------------------------------------------------------------------- /plugin/validator/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package validator 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "io/ioutil" 21 | "net/http" 22 | "testing" 23 | ) 24 | 25 | var noContext = context.Background() 26 | 27 | func TestErrSkip(t *testing.T) { 28 | client := http.Client{} 29 | client.Transport = roundTripFunc(func(r *http.Request) (*http.Response, error) { 30 | buf := bytes.NewBuffer(nil) 31 | buf.WriteString("skip") 32 | return &http.Response{ 33 | Body: ioutil.NopCloser(buf), 34 | StatusCode: 498, 35 | }, nil 36 | }) 37 | 38 | plugin := Client("http://localhost", "top-secret", false) 39 | plugin.(*pluginClient).client.Client = &client 40 | 41 | err := plugin.Validate(noContext, &Request{}) 42 | if err != ErrSkip { 43 | t.Errorf("Expect skip error, got %v", err) 44 | } 45 | } 46 | 47 | type roundTripFunc func(r *http.Request) (*http.Response, error) 48 | 49 | func (s roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { 50 | return s(r) 51 | } 52 | -------------------------------------------------------------------------------- /plugin/validator/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package validator 16 | 17 | import ( 18 | "encoding/json" 19 | "io/ioutil" 20 | "net/http" 21 | 22 | "github.com/drone/drone-go/drone" 23 | "github.com/drone/drone-go/plugin/logger" 24 | 25 | "github.com/99designs/httpsignatures-go" 26 | ) 27 | 28 | const ( 29 | httpStatusSkip = 498 30 | httpStatusBlock = 499 31 | ) 32 | 33 | // Handler returns a http.Handler that accepts JSON-encoded 34 | // HTTP requests to validate the yaml configuration, invokes 35 | // the underlying plugin. A 2xx status code is returned if 36 | // the configuration file is valid. 37 | // 38 | // The handler verifies the authenticity of the HTTP request 39 | // using the http-signature, and returns a 400 Bad Request if 40 | // the signature is missing or invalid. 41 | // 42 | // The handler can optionally encrypt the response body using 43 | // aesgcm if the HTTP request includes the Accept-Encoding header 44 | // set to aesgcm. 45 | func Handler(secret string, plugin Plugin, logs logger.Logger) http.Handler { 46 | handler := &handler{ 47 | secret: secret, 48 | plugin: plugin, 49 | logger: logs, 50 | } 51 | if handler.logger == nil { 52 | handler.logger = logger.Discard() 53 | } 54 | return handler 55 | } 56 | 57 | type handler struct { 58 | secret string 59 | plugin Plugin 60 | logger logger.Logger 61 | } 62 | 63 | func (p *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 64 | signature, err := httpsignatures.FromRequest(r) 65 | if err != nil { 66 | p.logger.Debugf("validator: invalid or missing signature in http.Request") 67 | http.Error(w, "Invalid or Missing Signature", http.StatusBadRequest) 68 | return 69 | } 70 | if !signature.IsValid(p.secret, r) { 71 | p.logger.Debugf("validator: invalid signature in http.Request") 72 | http.Error(w, "Invalid Signature", http.StatusBadRequest) 73 | return 74 | } 75 | 76 | body, err := ioutil.ReadAll(r.Body) 77 | if err != nil { 78 | p.logger.Debugf("validator: cannot read http.Request body") 79 | w.WriteHeader(http.StatusBadRequest) 80 | return 81 | } 82 | 83 | req := &Request{} 84 | err = json.Unmarshal(body, req) 85 | if err != nil { 86 | p.logger.Debugf("validator: cannot unmarshal http.Request body") 87 | http.Error(w, "Invalid Input", http.StatusBadRequest) 88 | return 89 | } 90 | 91 | err = p.plugin.Validate(r.Context(), req) 92 | if err == nil { 93 | w.WriteHeader(http.StatusNoContent) 94 | return 95 | } 96 | 97 | if err == ErrSkip { 98 | w.WriteHeader(httpStatusSkip) 99 | return 100 | } 101 | 102 | if err == ErrBlock { 103 | w.WriteHeader(httpStatusBlock) 104 | return 105 | } 106 | 107 | // The error should be converted to a drone.Error so that 108 | // it can be marshaled to JSON. 109 | if _, ok := err.(*drone.Error); !ok { 110 | err = &drone.Error{ 111 | Code: http.StatusBadRequest, 112 | Message: err.Error(), 113 | } 114 | } 115 | 116 | out, _ := json.Marshal(err) 117 | w.WriteHeader(http.StatusBadRequest) 118 | _, _ = w.Write(out) 119 | } 120 | -------------------------------------------------------------------------------- /plugin/validator/handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package validator 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "errors" 22 | "net/http" 23 | "net/http/httptest" 24 | "testing" 25 | "time" 26 | 27 | "github.com/drone/drone-go/drone" 28 | 29 | "github.com/99designs/httpsignatures-go" 30 | ) 31 | 32 | func TestHandler(t *testing.T) { 33 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 34 | 35 | buf := new(bytes.Buffer) 36 | json.NewEncoder(buf).Encode(&Request{ 37 | Config: drone.Config{ 38 | Data: "{kind: pipeline, type: docker}", 39 | }, 40 | }) 41 | 42 | res := httptest.NewRecorder() 43 | req := httptest.NewRequest("GET", "/", buf) 44 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 45 | 46 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 47 | if err != nil { 48 | t.Error(err) 49 | return 50 | } 51 | 52 | plugin := &mockPlugin{ 53 | err: nil, 54 | } 55 | 56 | handler := Handler(key, plugin, nil) 57 | handler.ServeHTTP(res, req) 58 | 59 | if got, want := res.Code, 204; got != want { 60 | t.Errorf("Want status code %d, got %d", want, got) 61 | } 62 | } 63 | 64 | func TestHandler_Error(t *testing.T) { 65 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 66 | 67 | buf := new(bytes.Buffer) 68 | json.NewEncoder(buf).Encode(&Request{ 69 | Config: drone.Config{ 70 | Data: "{kind: pipeline, type: docker}", 71 | }, 72 | }) 73 | 74 | res := httptest.NewRecorder() 75 | req := httptest.NewRequest("GET", "/", buf) 76 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 77 | 78 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 79 | if err != nil { 80 | t.Error(err) 81 | return 82 | } 83 | 84 | err = errors.New("insufficient permission to mount volumes") 85 | plugin := &mockPlugin{err: err} 86 | 87 | handler := Handler(key, plugin, nil) 88 | handler.ServeHTTP(res, req) 89 | 90 | if got, want := res.Code, 400; got != want { 91 | t.Errorf("Want status code %d, got %d", want, got) 92 | } 93 | 94 | resp := &drone.Error{} 95 | json.Unmarshal(res.Body.Bytes(), resp) 96 | if got, want := resp.Message, err.Error(); got != want { 97 | t.Errorf("Want validation error %s, got %s", want, got) 98 | } 99 | } 100 | 101 | func TestHandler_ErrorSkip(t *testing.T) { 102 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 103 | 104 | buf := new(bytes.Buffer) 105 | json.NewEncoder(buf).Encode(&Request{ 106 | Config: drone.Config{ 107 | Data: "{kind: pipeline, type: docker}", 108 | }, 109 | }) 110 | 111 | res := httptest.NewRecorder() 112 | req := httptest.NewRequest("GET", "/", buf) 113 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 114 | 115 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 116 | if err != nil { 117 | t.Error(err) 118 | return 119 | } 120 | 121 | err = ErrSkip 122 | plugin := &mockPlugin{err: err} 123 | 124 | handler := Handler(key, plugin, nil) 125 | handler.ServeHTTP(res, req) 126 | 127 | if got, want := res.Code, 498; got != want { 128 | t.Errorf("Want status code %d, got %d", want, got) 129 | } 130 | } 131 | 132 | func TestHandler_ErrorBlock(t *testing.T) { 133 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 134 | 135 | buf := new(bytes.Buffer) 136 | json.NewEncoder(buf).Encode(&Request{ 137 | Config: drone.Config{ 138 | Data: "{kind: pipeline, type: docker}", 139 | }, 140 | }) 141 | 142 | res := httptest.NewRecorder() 143 | req := httptest.NewRequest("GET", "/", buf) 144 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 145 | 146 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 147 | if err != nil { 148 | t.Error(err) 149 | return 150 | } 151 | 152 | err = ErrBlock 153 | plugin := &mockPlugin{err: err} 154 | 155 | handler := Handler(key, plugin, nil) 156 | handler.ServeHTTP(res, req) 157 | 158 | if got, want := res.Code, 499; got != want { 159 | t.Errorf("Want status code %d, got %d", want, got) 160 | } 161 | } 162 | 163 | func TestHandler_MissingSignature(t *testing.T) { 164 | res := httptest.NewRecorder() 165 | req := httptest.NewRequest("GET", "/", nil) 166 | 167 | handler := Handler("xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil, nil) 168 | handler.ServeHTTP(res, req) 169 | 170 | got, want := res.Body.String(), "Invalid or Missing Signature\n" 171 | if got != want { 172 | t.Errorf("Want response body %q, got %q", want, got) 173 | } 174 | } 175 | 176 | func TestHandler_InvalidSignature(t *testing.T) { 177 | sig := `keyId="hmac-key",algorithm="hmac-sha256",signature="QrS16+RlRsFjXn5IVW8tWz+3ZRAypjpNgzehEuvJksk=",headers="(request-target) accept accept-encoding content-type date digest"` 178 | res := httptest.NewRecorder() 179 | req := httptest.NewRequest("GET", "/", nil) 180 | req.Header.Set("Signature", sig) 181 | 182 | handler := Handler("xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil, nil) 183 | handler.ServeHTTP(res, req) 184 | 185 | got, want := res.Body.String(), "Invalid Signature\n" 186 | if got != want { 187 | t.Errorf("Want response body %q, got %q", want, got) 188 | } 189 | } 190 | 191 | type mockPlugin struct { 192 | err error 193 | } 194 | 195 | func (m *mockPlugin) Validate(ctx context.Context, req *Request) error { 196 | return m.err 197 | } 198 | -------------------------------------------------------------------------------- /plugin/validator/validater.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package validator 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | 21 | "github.com/drone/drone-go/drone" 22 | ) 23 | 24 | // V1 is version 1 of the validator API 25 | const V1 = "application/vnd.drone.validate.v1+json" 26 | 27 | // ErrSkip is returned when the build should be skipped 28 | // instead of throwing an error. 29 | var ErrSkip = errors.New("skip") 30 | 31 | // ErrBlock is returned when the build should be blocked 32 | // instead of throwing an error. 33 | var ErrBlock = errors.New("block") 34 | 35 | type ( 36 | // Request defines a validator request. 37 | Request struct { 38 | Build drone.Build `json:"build,omitempty"` 39 | Config drone.Config `json:"config,omitempty"` 40 | Repo drone.Repo `json:"repo,omitempty"` 41 | Token drone.Token `json:"token,omitempty"` // not implemented 42 | } 43 | 44 | // Plugin responds to a validator request. 45 | Plugin interface { 46 | Validate(context.Context, *Request) error 47 | } 48 | ) 49 | -------------------------------------------------------------------------------- /plugin/webhook/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package webhook 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/plugin/internal/client" 21 | ) 22 | 23 | // Client returns a new plugin client. 24 | func Client(endpoint, secret string, skipverify bool) Plugin { 25 | client := client.New(endpoint, secret, skipverify) 26 | client.Accept = V1 27 | return &pluginClient{ 28 | client: client, 29 | } 30 | } 31 | 32 | type pluginClient struct { 33 | client *client.Client 34 | } 35 | 36 | func (c *pluginClient) Deliver(ctx context.Context, in *Request) error { 37 | return c.client.Do(ctx, in, nil) 38 | } 39 | -------------------------------------------------------------------------------- /plugin/webhook/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package webhook 16 | 17 | import ( 18 | "encoding/json" 19 | "io/ioutil" 20 | "net/http" 21 | 22 | "github.com/drone/drone-go/plugin/logger" 23 | 24 | "github.com/99designs/httpsignatures-go" 25 | ) 26 | 27 | // Handler returns a http.Handler that accepts JSON-encoded 28 | // HTTP requests for a webhook, invokes the underlying webhook 29 | // plugin, and writes the JSON-encoded data to the HTTP response. 30 | // 31 | // The handler verifies the authenticity of the HTTP request 32 | // using the http-signature, and returns a 400 Bad Request if 33 | // the signature is missing or invalid. 34 | // 35 | // The handler can optionally encrypt the response body using 36 | // aesgcm if the HTTP request includes the Accept-Encoding header 37 | // set to aesgcm. 38 | func Handler(plugin Plugin, secret string, logs logger.Logger) http.Handler { 39 | handler := &handler{ 40 | secret: secret, 41 | plugin: plugin, 42 | logger: logs, 43 | } 44 | if handler.logger == nil { 45 | handler.logger = logger.Discard() 46 | } 47 | return handler 48 | } 49 | 50 | type handler struct { 51 | secret string 52 | plugin Plugin 53 | logger logger.Logger 54 | } 55 | 56 | func (p *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 57 | signature, err := httpsignatures.FromRequest(r) 58 | if err != nil { 59 | p.logger.Debugf("webhook: invalid or missing signature in http.Request") 60 | http.Error(w, "Invalid or Missing Signature", http.StatusBadRequest) 61 | return 62 | } 63 | if !signature.IsValid(p.secret, r) { 64 | p.logger.Debugf("webhook: invalid signature in http.Request") 65 | http.Error(w, "Invalid Signature", http.StatusBadRequest) 66 | return 67 | } 68 | 69 | body, err := ioutil.ReadAll(r.Body) 70 | if err != nil { 71 | p.logger.Debugf("webhook: cannot read http.Request body") 72 | w.WriteHeader(http.StatusBadRequest) 73 | return 74 | } 75 | 76 | req := &Request{} 77 | err = json.Unmarshal(body, req) 78 | if err != nil { 79 | p.logger.Debugf("webhook: cannot unmarshal http.Request body") 80 | http.Error(w, "Invalid Input", http.StatusBadRequest) 81 | return 82 | } 83 | 84 | err = p.plugin.Deliver(r.Context(), req) 85 | if err != nil { 86 | http.Error(w, err.Error(), http.StatusInternalServerError) 87 | } else { 88 | w.WriteHeader(http.StatusNoContent) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /plugin/webhook/handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package webhook 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "errors" 22 | "net/http" 23 | "net/http/httptest" 24 | "strings" 25 | "testing" 26 | "time" 27 | 28 | "github.com/99designs/httpsignatures-go" 29 | ) 30 | 31 | func TestHandler(t *testing.T) { 32 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 33 | 34 | buf := new(bytes.Buffer) 35 | json.NewEncoder(buf).Encode(&Request{}) 36 | 37 | res := httptest.NewRecorder() 38 | req := httptest.NewRequest("GET", "/", buf) 39 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 40 | 41 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 42 | if err != nil { 43 | t.Error(err) 44 | return 45 | } 46 | 47 | plugin := &mockPlugin{ 48 | err: nil, 49 | } 50 | 51 | handler := Handler(plugin, key, nil) 52 | handler.ServeHTTP(res, req) 53 | 54 | if got, want := res.Code, 204; got != want { 55 | t.Errorf("Want status code %d, got %d", want, got) 56 | } 57 | } 58 | 59 | func TestHandlerError(t *testing.T) { 60 | key := "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh" 61 | 62 | buf := new(bytes.Buffer) 63 | json.NewEncoder(buf).Encode(&Request{}) 64 | 65 | res := httptest.NewRecorder() 66 | req := httptest.NewRequest("GET", "/", buf) 67 | req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat)) 68 | 69 | err := httpsignatures.DefaultSha256Signer.AuthRequest("hmac-key", key, req) 70 | if err != nil { 71 | t.Error(err) 72 | return 73 | } 74 | 75 | plugin := &mockPlugin{ 76 | err: errors.New("pc load letter"), 77 | } 78 | 79 | handler := Handler(plugin, key, nil) 80 | handler.ServeHTTP(res, req) 81 | 82 | if got, want := res.Code, 500; got != want { 83 | t.Errorf("Want status code %d, got %d", want, got) 84 | } 85 | 86 | got, want := strings.TrimSpace(res.Body.String()), plugin.err.Error() 87 | if got != want { 88 | t.Errorf("Want error %q, got %q", want, got) 89 | } 90 | } 91 | 92 | func TestHandler_MissingSignature(t *testing.T) { 93 | res := httptest.NewRecorder() 94 | req := httptest.NewRequest("GET", "/", nil) 95 | 96 | handler := Handler(nil, "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil) 97 | handler.ServeHTTP(res, req) 98 | 99 | got, want := res.Body.String(), "Invalid or Missing Signature\n" 100 | if got != want { 101 | t.Errorf("Want response body %q, got %q", want, got) 102 | } 103 | } 104 | 105 | func TestHandler_InvalidSignature(t *testing.T) { 106 | sig := `keyId="hmac-key",algorithm="hmac-sha256",signature="QrS16+RlRsFjXn5IVW8tWz+3ZRAypjpNgzehEuvJksk=",headers="(request-target) accept accept-encoding content-type date digest"` 107 | res := httptest.NewRecorder() 108 | req := httptest.NewRequest("GET", "/", nil) 109 | req.Header.Set("Signature", sig) 110 | 111 | handler := Handler(nil, "xVKAGlWQiY3sOp8JVc0nbuNId3PNCgWh", nil) 112 | handler.ServeHTTP(res, req) 113 | 114 | got, want := res.Body.String(), "Invalid Signature\n" 115 | if got != want { 116 | t.Errorf("Want response body %q, got %q", want, got) 117 | } 118 | } 119 | 120 | type mockPlugin struct { 121 | err error 122 | } 123 | 124 | func (m *mockPlugin) Deliver(ctx context.Context, req *Request) error { 125 | return m.err 126 | } 127 | -------------------------------------------------------------------------------- /plugin/webhook/webhook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Drone.IO Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package webhook 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/drone/drone-go/drone" 21 | ) 22 | 23 | // V1 is version 1 of the admission API 24 | const V1 = "application/vnd.drone.webhook.v1+json" 25 | 26 | // Webhook event types. 27 | const ( 28 | EventBuild = "build" 29 | EventRepo = "repo" 30 | EventUser = "user" 31 | ) 32 | 33 | // Webhook action types. 34 | const ( 35 | ActionCreated = "created" 36 | ActionUpdated = "updated" 37 | ActionDeleted = "deleted" 38 | ActionEnabled = "enabled" 39 | ActionDisabled = "disabled" 40 | ) 41 | 42 | type ( 43 | // Request defines a webhook request. 44 | Request struct { 45 | Event string `json:"event"` 46 | Action string `json:"action"` 47 | User *drone.User `json:"user,omitempty"` 48 | Repo *drone.Repo `json:"repo,omitempty"` 49 | Build *drone.Build `json:"build,omitempty"` 50 | System *drone.System `json:"system,omitempty"` 51 | } 52 | 53 | // Plugin responds to a webhook request. 54 | Plugin interface { 55 | Deliver(context.Context, *Request) error 56 | } 57 | ) 58 | --------------------------------------------------------------------------------