From 0bee87ab08a1afc3df68a06f93a88bcc9555cc09 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Wed, 12 May 2021 19:47:20 +0200 Subject: [PATCH 001/128] Initial commit Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- .gitignore | 4 ++++ README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6eac500 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.git/ +.idea/ +node_modules/ +vendor/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb602c5 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Gitea SonarQube PR Bot + +_Gitea SonarQube PR Bot_ is (obviously) a bot that receives messages from both SonarQube and Gitea to help developers +being productive. The idea behind this project is the missing ALM integration of Gitea in SonarQube. Unfortunately, +this won't be added in near future. _Gitea SonarQube PR Bot_ aims to fill the gap between working on pull requests and +being notified on quality changes. Luckily, both endpoints have a proper REST API to communicate with each others. + + +## Workflow + +[add workflow schema] + +- On PR create/PR push update; bot sets sonarqube status check to pending +- Some tool analyses code and sends it to SonarQube; it does not matter whether this tool waits for the results +- Webhook in SonarQube is sent to SonarQube/Gitea bot +- Bot activities + - extract data from SonarQube + - Read payload from hook post to receive project,branch/pr,quality-gate + - Reads "api/project_pull_requests" to get current issue counts and current state + - Load "api/issues/search" to get detailed information for unresolved issues + - Load "api/measures/component" + - comment PR in Gitea (/repos/{owner}/{repo}/issues/{index}/comments) + - stores mapping of repo+pr+comment-id in ?redis? + - updates status check (either failing/success) + - listen on "/sq-bot review" comments + -> updates comment (/repos/{owner}/{repo}/issues/comments/{id}) + -> updates status check (either failing/success) + + +## Authentication + +- Gitea + - User with token to access the REST API + - User needs "Read project" permissions with (??at least??) access to "Pull Requests" +- SonarQube + - User with token to access the REST API + - User needs "Browse on project" permissions + + +## Bot configuration + +- SonarQube + - Base URL + - Token + - Webhook Secret +- Gitea + - Base URL + - Token + - Webhook Secret + + +## SonarQube configuration + +- Add user with necessary permissions +- Create webhook pointing to the bot url (secure it with webhook secret) + + +## Gitea configuration + +- Add user with necessary permissions +- Create webhook on a project/organization pointing to the bot url (secure it with webhook secret) From 4cb21384517add22df46b64c46089a395a1dba26 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 24 May 2021 19:54:06 +0200 Subject: [PATCH 002/128] Visualize workflow Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- README.md | 13 ++++++------- assets/workflow.drawio | 1 + assets/workflow.png | Bin 0 -> 78680 bytes 3 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 assets/workflow.drawio create mode 100644 assets/workflow.png diff --git a/README.md b/README.md index bb602c5..51fc36f 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,17 @@ _Gitea SonarQube PR Bot_ is (obviously) a bot that receives messages from both SonarQube and Gitea to help developers being productive. The idea behind this project is the missing ALM integration of Gitea in SonarQube. Unfortunately, -this won't be added in near future. _Gitea SonarQube PR Bot_ aims to fill the gap between working on pull requests and -being notified on quality changes. Luckily, both endpoints have a proper REST API to communicate with each others. +this [won't be added in near future](https://github.com/SonarSource/sonarqube/pull/3248#issuecomment-701334327). +_Gitea SonarQube PR Bot_ aims to fill the gap between working on pull requests and being notified on quality changes. +Luckily, both endpoints have a proper REST API to communicate with each others. ## Workflow -[add workflow schema] +![Workflow](assets/workflow.png) + +**Insights** -- On PR create/PR push update; bot sets sonarqube status check to pending -- Some tool analyses code and sends it to SonarQube; it does not matter whether this tool waits for the results -- Webhook in SonarQube is sent to SonarQube/Gitea bot - Bot activities - extract data from SonarQube - Read payload from hook post to receive project,branch/pr,quality-gate @@ -26,7 +26,6 @@ being notified on quality changes. Luckily, both endpoints have a proper REST AP -> updates comment (/repos/{owner}/{repo}/issues/comments/{id}) -> updates status check (either failing/success) - ## Authentication - Gitea diff --git a/assets/workflow.drawio b/assets/workflow.drawio new file mode 100644 index 0000000..a2e5428 --- /dev/null +++ b/assets/workflow.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/assets/workflow.png b/assets/workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..1ac9d5692de062186b2b3de497ff221b2106b88a GIT binary patch literal 78680 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%0D8aJLyC!Io|wv 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 Date: Mon, 24 May 2021 20:53:22 +0200 Subject: [PATCH 003/128] Follow advisory: Add license, DCO and contributing Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- CONTRIBUTING.md | 22 ++++++++++++++++++++++ DCO | 37 +++++++++++++++++++++++++++++++++++++ LICENSE | 19 +++++++++++++++++++ README.md | 12 ++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 DCO create mode 100644 LICENSE diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d840a9e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# Contribution Guidelines + +## Table of Contents + +- [Contribution Guidelines](#contribution-guidelines) + - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) + + +## Developer Certificate of Origin (DCO) + +I consider the act of contributing to the code by submitting a Pull Request as the "Sign off" or agreement to the +certifications and terms of the [DCO](DCO) and [MIT license](LICENSE). No further action is required. Additionally, +you could add a line at the end of your commit message. + +``` +Signed-off-by: Joe Smith +``` + +If you set your `user.name` and `user.email` git configs, you can add the line to the end of your commit automatically +with `git commit -s`. + +I assume in good faith that the information you provide is legally binding. diff --git a/DCO b/DCO new file mode 100644 index 0000000..8201f99 --- /dev/null +++ b/DCO @@ -0,0 +1,37 @@ +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +1 Letterman Drive +Suite D4700 +San Francisco, CA, 94129 + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..61343ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 Steven Kriegler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 51fc36f..5a591be 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,15 @@ Luckily, both endpoints have a proper REST API to communicate with each others. - Add user with necessary permissions - Create webhook on a project/organization pointing to the bot url (secure it with webhook secret) + +## Contributing + +Expected workflow is: Fork -> Patch -> Push -> Pull Request + +NOTES: + +- **Please read and follow the [CONTRIBUTORS GUIDE](CONTRIBUTING.md).** + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for the full license text. From 99cd1366f8a8ad6ef6958e4786b15929e60701cd Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 6 Jun 2021 13:41:14 +0200 Subject: [PATCH 004/128] Add development environment - docker - golang Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- .editorconfig | 13 ++++++++ CONTRIBUTING.md | 13 ++++++++ contrib/Dockerfile | 10 ++++++ go.mod | 9 ++++++ go.sum | 77 ++++++++++++++++++++++++++++++++++++++++++++++ main.go | 18 +++++++++++ 6 files changed, 140 insertions(+) create mode 100644 .editorconfig create mode 100644 contrib/Dockerfile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6cbaf1e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +tab_width = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d840a9e..1f45c48 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,8 +3,21 @@ ## Table of Contents - [Contribution Guidelines](#contribution-guidelines) + - [Setup development environment](#setup-development-environment) - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) +## Setup development environment + +```bash +# Build docker environment +docker build -t gitea-sonarqube-pr-bot/dev -f contrib/Dockerfile contrib + +# start the environment +docker run --rm -it -p 9100:8080 -v "$(pwd):/projects" gitea-sonarqube-pr-bot/dev + +# Start the server +go run main.go +``` ## Developer Certificate of Origin (DCO) diff --git a/contrib/Dockerfile b/contrib/Dockerfile new file mode 100644 index 0000000..c418967 --- /dev/null +++ b/contrib/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.16-alpine3.13 + +RUN apk --no-cache add build-base git bash + +WORKDIR /projects + +VOLUME ["/projects"] +EXPOSE 8080 + +CMD ["bash"] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f2b7c6f --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/justusbunsi/gitea-sonarqube-pr-bot + +go 1.16 + +require ( + github.com/gin-gonic/gin v1.7.2 + github.com/jinzhu/gorm v1.9.16 // indirect + github.com/urfave/cli v1.22.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1c2f62e --- /dev/null +++ b/go.sum @@ -0,0 +1,77 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA= +github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= +github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..5281c8e --- /dev/null +++ b/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "net/http" + "github.com/gin-gonic/gin" +) + +func main() { +// gin.SetMode(gin.ReleaseMode) + + server := gin.Default() + + server.GET("/", func(ctx *gin.Context) { + ctx.JSON(http.StatusOK, gin.H{"data": "Hi! I'm the Gitea-SonarQube-PR bot. At your service."}) + }) + + server.Run() +} From 84e9aa1152554933c95b14474055c5157c1536d6 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 6 Jun 2021 16:35:37 +0200 Subject: [PATCH 005/128] Define configuration structure Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- .gitignore | 1 + README.md | 45 +++++++++++++------------------- config/config.example.yaml | 53 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 27 deletions(-) create mode 100644 config/config.example.yaml diff --git a/.gitignore b/.gitignore index 6eac500..deac53c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .idea/ node_modules/ vendor/ +config/ diff --git a/README.md b/README.md index 5a591be..b33b93f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,14 @@ this [won't be added in near future](https://github.com/SonarSource/sonarqube/pu _Gitea SonarQube PR Bot_ aims to fill the gap between working on pull requests and being notified on quality changes. Luckily, both endpoints have a proper REST API to communicate with each others. +## Table of Contents + +- [Gitea SonarQube PR Bot](#gitea-sonarqube-pr-bot) + - [Workflow](#workflow) + - [Setup](#setup) + - [Bot configuration](#bot-configuration) + - [Contributing](#contributing) + - [License](#license) ## Workflow @@ -26,38 +34,21 @@ Luckily, both endpoints have a proper REST API to communicate with each others. -> updates comment (/repos/{owner}/{repo}/issues/comments/{id}) -> updates status check (either failing/success) -## Authentication +## Setup -- Gitea - - User with token to access the REST API - - User needs "Read project" permissions with (??at least??) access to "Pull Requests" -- SonarQube - - User with token to access the REST API - - User needs "Browse on project" permissions +**SonarQube** +- Create a user and grant permissions to "Browse on project" for the desired project +- Create a token for this user that will be used by the bot. +- Create a webhook pointing to `https:///sonarqube`. Consider securing it with a secret. +**Gitea** +- Create a user and grant permissions to "Read project" for the desired projects including access to "Pull Requests" +- Create a token for this user that will be used by the bot. +- Create a project/organization/system webhook pointing to `https:///gitea`. Consider securing it with a secret. ## Bot configuration -- SonarQube - - Base URL - - Token - - Webhook Secret -- Gitea - - Base URL - - Token - - Webhook Secret - - -## SonarQube configuration - -- Add user with necessary permissions -- Create webhook pointing to the bot url (secure it with webhook secret) - - -## Gitea configuration - -- Add user with necessary permissions -- Create webhook on a project/organization pointing to the bot url (secure it with webhook secret) +See [config.example.yaml](config/config.example.yaml) for a full configuration specification and description. ## Contributing diff --git a/config/config.example.yaml b/config/config.example.yaml new file mode 100644 index 0000000..fe6ed36 --- /dev/null +++ b/config/config.example.yaml @@ -0,0 +1,53 @@ +# Gitea related configuration. Necessary for adding/updating comments on repository pull requests +gitea: + # API endpoint of your Gitea instance. Must be the API base path as shown in Swagger UI. + url: https://try.gitea.io/api/v1 + + # Created access token for the user that shall be used as bot account. + # User needs "Read project" permissions with access to "Pull Requests" + token: <...> + + # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the + # request will be ignored. + # The bot looks for `X-Gitea-Signature` header containing the sha256 hmac hash of the plain text secret. If the header + # exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be validated. + webhookSecret: {} + # # either plain text + # value: <...> + # # or path to file containing the plain text secret + # file: /path/to/gitea/webhook/secret + + # List of repository the used Gitea account has access to and shall be handled by the bot. Other repository webhooks + # will be ignored. + # A repository specification contains the owner name and the repository name itself. The owner can be the name of a + # real account or an organization in which the repository is located. + repositories: + - owner: justusbunsi + name: example-repo + - owner: my-organization + name: example-repo + + +# SonarQube related configuration. Necessary for requesting data from the API and processing the webhook. +sonarqube: + # API endpoint of your SonarQube instance. + url: https://sonarcloud.io/api + + # Created access token for the user that shall be used as bot account. + # User needs "Browse on project" permissions + token: <...> + + # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the + # request will be ignored. + # The bot looks for `X-Sonar-Webhook-HMAC-SHA256` header containing the sha256 hmac hash of the plain text secret. + # If the header exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be + # validated. + webhookSecret: {} + # # either plain text + # value: <...> + # # or path to file containing the plain text secret + # file: /path/to/gitea/webhook/secret + + projects: + - project-1 + - project-2 From d98545e90dafb29b956f01ed55e81402d04462ec Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 6 Jun 2021 17:19:02 +0200 Subject: [PATCH 006/128] Add missing description in example config Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- config/config.example.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.example.yaml b/config/config.example.yaml index fe6ed36..629ecd4 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -48,6 +48,7 @@ sonarqube: # # or path to file containing the plain text secret # file: /path/to/gitea/webhook/secret + # List of project keys from inside SonarQube that should be handled. Webhooks containing other projects will be ignored. projects: - project-1 - project-2 From 6d449ebb14aa51c369658b85563e9a87b38d9f2e Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 20 Jun 2021 15:14:26 +0200 Subject: [PATCH 007/128] Respect common go project structure Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- .editorconfig | 12 +++++++----- .gitignore | 10 +++++----- README.md | 2 +- {assets => docs}/workflow.drawio | 0 {assets => docs}/workflow.png | Bin 5 files changed, 13 insertions(+), 11 deletions(-) rename {assets => docs}/workflow.drawio (100%) rename {assets => docs}/workflow.png (100%) diff --git a/.editorconfig b/.editorconfig index 6cbaf1e..b4fc9e6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,15 @@ root = true [*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true indent_style = space indent_size = 2 -tab_width = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true + +[{*.go,Makefile,.gitmodules,go.mod,go.sum}] +indent_style = tab [*.md] trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index deac53c..bb63e77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -.git/ -.idea/ -node_modules/ -vendor/ -config/ +/.git/ +/.idea/ +/config/ +/vendor/ +/gitea-sonarqube-bot diff --git a/README.md b/README.md index b33b93f..cbd7ae9 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Luckily, both endpoints have a proper REST API to communicate with each others. ## Workflow -![Workflow](assets/workflow.png) +![Workflow](docs/workflow.png) **Insights** diff --git a/assets/workflow.drawio b/docs/workflow.drawio similarity index 100% rename from assets/workflow.drawio rename to docs/workflow.drawio diff --git a/assets/workflow.png b/docs/workflow.png similarity index 100% rename from assets/workflow.png rename to docs/workflow.png From 706aeb0d0ffd28e95fb9b33d8293d1d9a8c27eed Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 20 Jun 2021 15:16:06 +0200 Subject: [PATCH 008/128] Load config.yaml from config directory Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- CONTRIBUTING.md | 14 +- cmd/gitea-sonarqube-bot/main.go | 12 + go.mod | 11 +- go.sum | 605 ++++++++++++++++++++++++++--- internal/settings/settings.go | 19 + internal/settings/settings_test.go | 44 +++ main.go | 18 - 7 files changed, 654 insertions(+), 69 deletions(-) create mode 100644 cmd/gitea-sonarqube-bot/main.go create mode 100644 internal/settings/settings.go create mode 100644 internal/settings/settings_test.go delete mode 100644 main.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f45c48..7cdf12c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,7 @@ - [Contribution Guidelines](#contribution-guidelines) - [Setup development environment](#setup-development-environment) + - [Testing](#testing) - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) ## Setup development environment @@ -12,11 +13,20 @@ # Build docker environment docker build -t gitea-sonarqube-pr-bot/dev -f contrib/Dockerfile contrib -# start the environment +# Start the environment docker run --rm -it -p 9100:8080 -v "$(pwd):/projects" gitea-sonarqube-pr-bot/dev +# Build the binary +go build ./cmd/gitea-sonarqube-bot + # Start the server -go run main.go +./gitea-sonarqube-bot +``` + +## Testing + +```bash +go test ./... ``` ## Developer Certificate of Origin (DCO) diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go new file mode 100644 index 0000000..3fd7163 --- /dev/null +++ b/cmd/gitea-sonarqube-bot/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "path" + + "github.com/justusbunsi/gitea-sonarqube-pr-bot/internal/settings" +) + +func main() { + configPath := path.Join("config") + settings.Load(configPath) +} diff --git a/go.mod b/go.mod index f2b7c6f..3240254 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,12 @@ module github.com/justusbunsi/gitea-sonarqube-pr-bot go 1.16 require ( - github.com/gin-gonic/gin v1.7.2 - github.com/jinzhu/gorm v1.9.16 // indirect - github.com/urfave/cli v1.22.5 // indirect + github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect + github.com/coreos/etcd v3.3.10+incompatible // indirect + github.com/coreos/go-etcd v2.0.0+incompatible // indirect + github.com/spf13/viper v1.8.0 // indirect + github.com/stretchr/testify v1.7.0 // indirect + github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect + github.com/urfave/cli/v2 v2.3.0 // indirect + github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 // indirect ) diff --git a/go.sum b/go.sum index 1c2f62e..0bb7549 100644 --- a/go.sum +++ b/go.sum @@ -1,77 +1,590 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= -github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA= -github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= -github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.8.0 h1:QRwDgoG8xX+kp69di68D+YYTCWfYEckbZRfUlEIAal0= +github.com/spf13/viper v1.8.0/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= -github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/settings/settings.go b/internal/settings/settings.go new file mode 100644 index 0000000..568b18e --- /dev/null +++ b/internal/settings/settings.go @@ -0,0 +1,19 @@ +package settings + +import ( + "fmt" + + "github.com/spf13/viper" +) + +func Load(configPath string) { + viper.SetConfigName("config.yaml") + viper.SetConfigType("yaml") + viper.AddConfigPath(configPath) + + err := viper.ReadInConfig() + + if err != nil { + panic(fmt.Errorf("Fatal error while reading config file: %w \n", err)) + } +} diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go new file mode 100644 index 0000000..61fb9c0 --- /dev/null +++ b/internal/settings/settings_test.go @@ -0,0 +1,44 @@ +package settings + +import ( + "io/ioutil" + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" +) + +var defaultConfig []byte = []byte(` +gitea: + url: https://example.com/gitea + token: 1337 + webhookSecret: {} + repositories: [] +sonarqube: + url: https://example.com/sonarqube + token: 42 + webhookSecret: {} + projects: [] +`) + +func WriteConfigFile(t *testing.T, content []byte) { + dir := os.TempDir() + config := path.Join(dir, "config.yaml") + + t.Cleanup(func() { + os.Remove(config) + }) + + _ = ioutil.WriteFile(config, content,0444) +} + +func TestLoadWithMissingFile(t *testing.T) { + assert.Panics(t, func() { Load(os.TempDir()) }, "No panic while reading missing file") +} + +func TestLoadWithExistingFile(t *testing.T) { + WriteConfigFile(t, defaultConfig) + + assert.NotPanics(t, func() { Load(os.TempDir()) }, "Unexpected panic while reading existing file") +} diff --git a/main.go b/main.go deleted file mode 100644 index 5281c8e..0000000 --- a/main.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "net/http" - "github.com/gin-gonic/gin" -) - -func main() { -// gin.SetMode(gin.ReleaseMode) - - server := gin.Default() - - server.GET("/", func(ctx *gin.Context) { - ctx.JSON(http.StatusOK, gin.H{"data": "Hi! I'm the Gitea-SonarQube-PR bot. At your service."}) - }) - - server.Run() -} From 5c6229be8fc7889285f681436463df96ceb28d4f Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 20 Jun 2021 15:39:54 +0200 Subject: [PATCH 009/128] Allow custom config path via environment variable Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- cmd/gitea-sonarqube-bot/main.go | 13 +++++++++++-- cmd/gitea-sonarqube-bot/main_test.go | 22 ++++++++++++++++++++++ internal/settings/settings_test.go | 4 ++-- 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 cmd/gitea-sonarqube-bot/main_test.go diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index 3fd7163..ba36da2 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -1,12 +1,21 @@ package main import ( + "os" "path" "github.com/justusbunsi/gitea-sonarqube-pr-bot/internal/settings" ) -func main() { +func GetConfigLocation() string { configPath := path.Join("config") - settings.Load(configPath) + if customConfigPath, ok := os.LookupEnv("PRBOT_CONFIG_PATH"); ok { + configPath = customConfigPath + } + + return configPath +} + +func main() { + settings.Load(GetConfigLocation()) } diff --git a/cmd/gitea-sonarqube-bot/main_test.go b/cmd/gitea-sonarqube-bot/main_test.go new file mode 100644 index 0000000..6f55850 --- /dev/null +++ b/cmd/gitea-sonarqube-bot/main_test.go @@ -0,0 +1,22 @@ +package main + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetConfigLocationWithDefault(t *testing.T) { + assert.Equal(t, "config", GetConfigLocation()) +} + +func TestGetConfigLocationWithEnvironmentOverride(t *testing.T) { + os.Setenv("PRBOT_CONFIG_PATH", "/tmp/") + + assert.Equal(t, "/tmp/", GetConfigLocation()) + + t.Cleanup(func() { + os.Unsetenv("PRBOT_CONFIG_PATH") + }) +} diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 61fb9c0..1063cb9 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/assert" ) -var defaultConfig []byte = []byte(` -gitea: +var defaultConfig []byte = []byte( +`gitea: url: https://example.com/gitea token: 1337 webhookSecret: {} From 86ea377d6466fc380dd6672524c57e48fb1793c1 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 20 Jun 2021 16:49:12 +0200 Subject: [PATCH 010/128] Load gitea configuration from file Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/settings/settings.go | 35 +++++++++++++++++++++++++++--- internal/settings/settings_test.go | 28 ++++++++++++++++++++---- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 568b18e..af391c2 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -6,6 +6,22 @@ import ( "github.com/spf13/viper" ) +type GiteaRepository struct { + Owner string + Name string +} + +type GiteaConfig struct { + Url string + Token string + WebhookSecret string `mapstructure:"webhookSecret"` + Repositories []GiteaRepository +} + +var ( + Gitea GiteaConfig +) + func Load(configPath string) { viper.SetConfigName("config.yaml") viper.SetConfigType("yaml") @@ -13,7 +29,20 @@ func Load(configPath string) { err := viper.ReadInConfig() - if err != nil { - panic(fmt.Errorf("Fatal error while reading config file: %w \n", err)) - } + if err != nil { + panic(fmt.Errorf("Fatal error while reading config file: %w \n", err)) + } + + var giteaConfig GiteaConfig + + if viper.Sub("gitea") == nil { + panic("Gitea not configured") + } + + err = viper.UnmarshalKey("gitea", &giteaConfig) + if err != nil { + panic(fmt.Errorf("Unable to decode into struct, %v", err)) + } + + Gitea = giteaConfig } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 1063cb9..a8f77f7 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -9,16 +9,16 @@ import ( "github.com/stretchr/testify/assert" ) -var defaultConfig []byte = []byte( +var defaultConfigInlineSecrets []byte = []byte( `gitea: url: https://example.com/gitea token: 1337 - webhookSecret: {} + webhookSecret: "" repositories: [] sonarqube: url: https://example.com/sonarqube token: 42 - webhookSecret: {} + webhookSecret: "" projects: [] `) @@ -38,7 +38,27 @@ func TestLoadWithMissingFile(t *testing.T) { } func TestLoadWithExistingFile(t *testing.T) { - WriteConfigFile(t, defaultConfig) + WriteConfigFile(t, defaultConfigInlineSecrets) assert.NotPanics(t, func() { Load(os.TempDir()) }, "Unexpected panic while reading existing file") } + +func TestLoadWithMissingGiteaStructure(t *testing.T) { + WriteConfigFile(t, []byte(``)) + + assert.Panics(t, func() { Load(os.TempDir()) }, "No panic when Gitea is not configured") +} + +func TestLoadGiteaStructure(t *testing.T) { + WriteConfigFile(t, defaultConfigInlineSecrets) + Load(os.TempDir()) + + expected := &GiteaConfig{ + Url: "https://example.com/gitea", + Token: "1337", + WebhookSecret: "", + Repositories: []GiteaRepository{}, + } + + assert.True(t, assert.ObjectsAreEqualValues(&Gitea, expected)) +} From 6c2bb413cd194bb8f2882e3df270a130416fc80f Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 20 Jun 2021 18:24:12 +0200 Subject: [PATCH 011/128] Allow passing environment variables into config Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/settings/settings.go | 27 ++++++++++++++-------- internal/settings/settings_test.go | 37 +++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/internal/settings/settings.go b/internal/settings/settings.go index af391c2..c7fde51 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -2,6 +2,7 @@ package settings import ( "fmt" + "strings" "github.com/spf13/viper" ) @@ -22,27 +23,35 @@ var ( Gitea GiteaConfig ) -func Load(configPath string) { +func init() { viper.SetConfigName("config.yaml") viper.SetConfigType("yaml") + viper.SetEnvPrefix("prbot") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AllowEmptyEnv(true) + viper.AutomaticEnv() +} + +func Load(configPath string) { viper.AddConfigPath(configPath) err := viper.ReadInConfig() - if err != nil { panic(fmt.Errorf("Fatal error while reading config file: %w \n", err)) } - var giteaConfig GiteaConfig - - if viper.Sub("gitea") == nil { + if viper.IsSet("gitea") == false { panic("Gitea not configured") } - err = viper.UnmarshalKey("gitea", &giteaConfig) - if err != nil { - panic(fmt.Errorf("Unable to decode into struct, %v", err)) + var fullConfig struct { + Gitea GiteaConfig } - Gitea = giteaConfig + err = viper.Unmarshal(&fullConfig) + if err != nil { + panic(fmt.Errorf("Unable to load config into struct, %v", err)) + } + + Gitea = fullConfig.Gitea } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index a8f77f7..af2da01 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -12,13 +12,13 @@ import ( var defaultConfigInlineSecrets []byte = []byte( `gitea: url: https://example.com/gitea - token: 1337 - webhookSecret: "" + token: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 + webhookSecret: "haxxor" repositories: [] sonarqube: url: https://example.com/sonarqube - token: 42 - webhookSecret: "" + token: a09eb5785b25bb2cbacf48808a677a0709f02d8e + webhookSecret: "haxxor" projects: [] `) @@ -53,12 +53,33 @@ func TestLoadGiteaStructure(t *testing.T) { WriteConfigFile(t, defaultConfigInlineSecrets) Load(os.TempDir()) - expected := &GiteaConfig{ + expected := GiteaConfig{ Url: "https://example.com/gitea", - Token: "1337", - WebhookSecret: "", + Token: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", + WebhookSecret: "haxxor", Repositories: []GiteaRepository{}, } - assert.True(t, assert.ObjectsAreEqualValues(&Gitea, expected)) + assert.EqualValues(t, expected, Gitea) +} + +func TestLoadGiteaStructureWithEnvInjectedWebhookSecret(t *testing.T) { + os.Setenv("PRBOT_GITEA_WEBHOOKSECRET", "injected-secret") + os.Setenv("PRBOT_GITEA_TOKEN", "injected-token") + WriteConfigFile(t, defaultConfigInlineSecrets) + Load(os.TempDir()) + + expected := GiteaConfig{ + Url: "https://example.com/gitea", + Token: "injected-token", + WebhookSecret: "injected-secret", + Repositories: []GiteaRepository{}, + } + + assert.EqualValues(t, expected, Gitea) + + t.Cleanup(func() { + os.Unsetenv("PRBOT_GITEA_WEBHOOKSECRET") + os.Unsetenv("PRBOT_GITEA_TOKEN") + }) } From 9c9b7588abfa310e3c73bf4e16ba4e81c667753e Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 20 Jun 2021 19:47:55 +0200 Subject: [PATCH 012/128] Read webhook secret from file into configuration Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- config/config.example.yaml | 16 +++-- internal/settings/settings.go | 54 +++++++++++++++-- internal/settings/settings_test.go | 95 ++++++++++++++++++++++++++---- 3 files changed, 139 insertions(+), 26 deletions(-) diff --git a/config/config.example.yaml b/config/config.example.yaml index 629ecd4..606a906 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -5,15 +5,14 @@ gitea: # Created access token for the user that shall be used as bot account. # User needs "Read project" permissions with access to "Pull Requests" - token: <...> + token: "" # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the # request will be ignored. # The bot looks for `X-Gitea-Signature` header containing the sha256 hmac hash of the plain text secret. If the header # exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be validated. - webhookSecret: {} - # # either plain text - # value: <...> + webhookSecret: + value: "" # # or path to file containing the plain text secret # file: /path/to/gitea/webhook/secret @@ -35,18 +34,17 @@ sonarqube: # Created access token for the user that shall be used as bot account. # User needs "Browse on project" permissions - token: <...> + token: "" # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the # request will be ignored. # The bot looks for `X-Sonar-Webhook-HMAC-SHA256` header containing the sha256 hmac hash of the plain text secret. # If the header exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be # validated. - webhookSecret: {} - # # either plain text - # value: <...> + webhookSecret: + value: "" # # or path to file containing the plain text secret - # file: /path/to/gitea/webhook/secret + # file: /path/to/sonarqube/webhook/secret # List of project keys from inside SonarQube that should be handled. Webhooks containing other projects will be ignored. projects: diff --git a/internal/settings/settings.go b/internal/settings/settings.go index c7fde51..d8a36ec 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -2,6 +2,7 @@ package settings import ( "fmt" + "io/ioutil" "strings" "github.com/spf13/viper" @@ -12,15 +13,28 @@ type GiteaRepository struct { Name string } +type WebhookSecret struct { + Value string + File string +} + type GiteaConfig struct { Url string Token string - WebhookSecret string `mapstructure:"webhookSecret"` + WebhookSecret WebhookSecret `mapstructure:"webhookSecret"` Repositories []GiteaRepository } +type SonarQubeConfig struct { + Url string + Token string + WebhookSecret WebhookSecret `mapstructure:"webhookSecret"` + Projects []string +} + var ( Gitea GiteaConfig + SonarQube SonarQubeConfig ) func init() { @@ -30,6 +44,30 @@ func init() { viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AllowEmptyEnv(true) viper.AutomaticEnv() + + ApplyConfigDefaults() +} + +func ApplyConfigDefaults() { + viper.SetDefault("gitea.url", "") + viper.SetDefault("gitea.token", "") + viper.SetDefault("gitea.webhookSecret.value", "") + viper.SetDefault("gitea.webhookSecret.file", "") + viper.SetDefault("gitea.repositories", []interface{}{}) + viper.SetDefault("sonarqube.url", "") + viper.SetDefault("sonarqube.token", "") + viper.SetDefault("sonarqube.webhookSecret.value", "") + viper.SetDefault("sonarqube.webhookSecret.file", "") + viper.SetDefault("sonarqube.projects", []string{}) +} + +func ReadSecretFile(file string) string { + content, err := ioutil.ReadFile(file) + if err != nil { + panic(fmt.Errorf("Cannot read '%s' or it is no regular file. %w", file, err)) + } + + return string(content) } func Load(configPath string) { @@ -40,12 +78,9 @@ func Load(configPath string) { panic(fmt.Errorf("Fatal error while reading config file: %w \n", err)) } - if viper.IsSet("gitea") == false { - panic("Gitea not configured") - } - var fullConfig struct { Gitea GiteaConfig + SonarQube SonarQubeConfig `mapstructure:"sonarqube"` } err = viper.Unmarshal(&fullConfig) @@ -54,4 +89,13 @@ func Load(configPath string) { } Gitea = fullConfig.Gitea + SonarQube = fullConfig.SonarQube + + if Gitea.WebhookSecret.File != "" { + Gitea.WebhookSecret.Value = ReadSecretFile(Gitea.WebhookSecret.File) + } + + if SonarQube.WebhookSecret.File != "" { + SonarQube.WebhookSecret.Value = ReadSecretFile(SonarQube.WebhookSecret.File) + } } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index af2da01..342d26d 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -13,12 +13,25 @@ var defaultConfigInlineSecrets []byte = []byte( `gitea: url: https://example.com/gitea token: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 - webhookSecret: "haxxor" + webhookSecret: + value: haxxor-gitea-secret repositories: [] sonarqube: url: https://example.com/sonarqube token: a09eb5785b25bb2cbacf48808a677a0709f02d8e - webhookSecret: "haxxor" + webhookSecret: + value: haxxor-sonarqube-secret + projects: [] +`) + +var incompleteConfig []byte = []byte( +`gitea: + url: https://example.com/gitea + webhookSecret: + value: haxxor-gitea-secret +sonarqube: + url: https://example.com/sonarqube + token: a09eb5785b25bb2cbacf48808a677a0709f02d8e projects: [] `) @@ -43,12 +56,6 @@ func TestLoadWithExistingFile(t *testing.T) { assert.NotPanics(t, func() { Load(os.TempDir()) }, "Unexpected panic while reading existing file") } -func TestLoadWithMissingGiteaStructure(t *testing.T) { - WriteConfigFile(t, []byte(``)) - - assert.Panics(t, func() { Load(os.TempDir()) }, "No panic when Gitea is not configured") -} - func TestLoadGiteaStructure(t *testing.T) { WriteConfigFile(t, defaultConfigInlineSecrets) Load(os.TempDir()) @@ -56,7 +63,9 @@ func TestLoadGiteaStructure(t *testing.T) { expected := GiteaConfig{ Url: "https://example.com/gitea", Token: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", - WebhookSecret: "haxxor", + WebhookSecret: WebhookSecret{ + Value: "haxxor-gitea-secret", + }, Repositories: []GiteaRepository{}, } @@ -64,7 +73,7 @@ func TestLoadGiteaStructure(t *testing.T) { } func TestLoadGiteaStructureWithEnvInjectedWebhookSecret(t *testing.T) { - os.Setenv("PRBOT_GITEA_WEBHOOKSECRET", "injected-secret") + os.Setenv("PRBOT_GITEA_WEBHOOKSECRET_VALUE", "injected-secret") os.Setenv("PRBOT_GITEA_TOKEN", "injected-token") WriteConfigFile(t, defaultConfigInlineSecrets) Load(os.TempDir()) @@ -72,14 +81,76 @@ func TestLoadGiteaStructureWithEnvInjectedWebhookSecret(t *testing.T) { expected := GiteaConfig{ Url: "https://example.com/gitea", Token: "injected-token", - WebhookSecret: "injected-secret", + WebhookSecret: WebhookSecret{ + Value: "injected-secret", + }, Repositories: []GiteaRepository{}, } assert.EqualValues(t, expected, Gitea) t.Cleanup(func() { - os.Unsetenv("PRBOT_GITEA_WEBHOOKSECRET") + os.Unsetenv("PRBOT_GITEA_WEBHOOKSECRET_VALUE") os.Unsetenv("PRBOT_GITEA_TOKEN") }) } + +func TestLoadStructureWithResolvedWebhookFileFromEnvInjected(t *testing.T) { + secretFile := path.Join(os.TempDir(), "webhook-secret-sonarqube") + _ = ioutil.WriteFile(secretFile, []byte(`totally-secret`),0444) + + os.Setenv("PRBOT_GITEA_WEBHOOKSECRET_FILE", secretFile) + os.Setenv("PRBOT_SONARQUBE_WEBHOOKSECRET_FILE", secretFile) + os.Setenv("PRBOT_GITEA_TOKEN", "injected-token") + + WriteConfigFile(t, incompleteConfig) + Load(os.TempDir()) + + expectedGitea := GiteaConfig{ + Url: "https://example.com/gitea", + Token: "injected-token", + WebhookSecret: WebhookSecret{ + Value: "totally-secret", + File: secretFile, + }, + Repositories: []GiteaRepository{}, + } + + expectedSonarQube := SonarQubeConfig{ + Url: "https://example.com/sonarqube", + Token: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", + WebhookSecret: WebhookSecret{ + Value: "totally-secret", + File: secretFile, + }, + Projects: []string{}, + } + + assert.EqualValues(t, expectedGitea, Gitea) + assert.EqualValues(t, expectedSonarQube, SonarQube) + + t.Cleanup(func() { + os.Remove(secretFile) + os.Unsetenv("PRBOT_SONARQUBE_WEBHOOKSECRET_FILE") + os.Unsetenv("PRBOT_GITEA_TOKEN") + }) +} + +func TestReadSecretFileWhenDirectoryProvided(t *testing.T) { + assert.Panics(t, func() { ReadSecretFile(os.TempDir()) }, "No panic while trying to read content from directory") +} + +func TestReadSecretFileWhenMissingFileProvided(t *testing.T) { + assert.Panics(t, func() { ReadSecretFile(path.Join(os.TempDir(), "secret-file")) }, "No panic while trying to read missing file") +} + +func TestReadSecretFile(t *testing.T) { + secretFile := path.Join(os.TempDir(), "secret-file") + _ = ioutil.WriteFile(secretFile, []byte(`awesome-secret-content`),0444) + + assert.Equal(t, "awesome-secret-content", ReadSecretFile(secretFile)) + + t.Cleanup(func() { + os.Remove(secretFile) + }) +} From 71b19c35b8432cf0f65babc020f1d3e1e4cb594c Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 21 Jun 2021 10:31:11 +0200 Subject: [PATCH 013/128] Fix repository/project type from config Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- config/config.example.yaml | 18 ++++++++++-------- internal/settings/settings.go | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/config/config.example.yaml b/config/config.example.yaml index 606a906..6d81f06 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -20,11 +20,12 @@ gitea: # will be ignored. # A repository specification contains the owner name and the repository name itself. The owner can be the name of a # real account or an organization in which the repository is located. - repositories: - - owner: justusbunsi - name: example-repo - - owner: my-organization - name: example-repo + # If empty array given, don't filter requests for repositories and proceed them all. + repositories: [] + # - owner: justusbunsi + # name: example-repo + # - owner: my-organization + # name: example-repo # SonarQube related configuration. Necessary for requesting data from the API and processing the webhook. @@ -47,6 +48,7 @@ sonarqube: # file: /path/to/sonarqube/webhook/secret # List of project keys from inside SonarQube that should be handled. Webhooks containing other projects will be ignored. - projects: - - project-1 - - project-2 + # If empty array given, don't filter requests for repositories and proceed them all. + projects: [] + # - project-1 + # - project-2 diff --git a/internal/settings/settings.go b/internal/settings/settings.go index d8a36ec..7894119 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -53,7 +53,7 @@ func ApplyConfigDefaults() { viper.SetDefault("gitea.token", "") viper.SetDefault("gitea.webhookSecret.value", "") viper.SetDefault("gitea.webhookSecret.file", "") - viper.SetDefault("gitea.repositories", []interface{}{}) + viper.SetDefault("gitea.repositories", []GiteaRepository{}) viper.SetDefault("sonarqube.url", "") viper.SetDefault("sonarqube.token", "") viper.SetDefault("sonarqube.webhookSecret.value", "") From 9fc9323f23ab1caa1278d7bc5a7efb2b3d4de08b Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 21 Jun 2021 11:00:31 +0200 Subject: [PATCH 014/128] Rewrite webhook config structure It now allows more than just secret/secretFile. Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- config/config.example.yaml | 12 +-- internal/settings/settings.go | 26 ++--- internal/settings/settings_test.go | 158 +++++++++++++++++------------ 3 files changed, 114 insertions(+), 82 deletions(-) diff --git a/config/config.example.yaml b/config/config.example.yaml index 6d81f06..a3c2639 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -11,10 +11,10 @@ gitea: # request will be ignored. # The bot looks for `X-Gitea-Signature` header containing the sha256 hmac hash of the plain text secret. If the header # exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be validated. - webhookSecret: - value: "" + webhook: + secret: "" # # or path to file containing the plain text secret - # file: /path/to/gitea/webhook/secret + # secretFile: /path/to/gitea/webhook/secret # List of repository the used Gitea account has access to and shall be handled by the bot. Other repository webhooks # will be ignored. @@ -42,10 +42,10 @@ sonarqube: # The bot looks for `X-Sonar-Webhook-HMAC-SHA256` header containing the sha256 hmac hash of the plain text secret. # If the header exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be # validated. - webhookSecret: - value: "" + webhook: + secret: "" # # or path to file containing the plain text secret - # file: /path/to/sonarqube/webhook/secret + # secretFile: /path/to/sonarqube/webhook/secret # List of project keys from inside SonarQube that should be handled. Webhooks containing other projects will be ignored. # If empty array given, don't filter requests for repositories and proceed them all. diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 7894119..7ab2c5c 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -13,22 +13,22 @@ type GiteaRepository struct { Name string } -type WebhookSecret struct { - Value string - File string +type Webhook struct { + Secret string + SecretFile string } type GiteaConfig struct { Url string Token string - WebhookSecret WebhookSecret `mapstructure:"webhookSecret"` + Webhook Webhook `mapstructure:"webhook"` Repositories []GiteaRepository } type SonarQubeConfig struct { Url string Token string - WebhookSecret WebhookSecret `mapstructure:"webhookSecret"` + Webhook Webhook `mapstructure:"webhook"` Projects []string } @@ -51,13 +51,13 @@ func init() { func ApplyConfigDefaults() { viper.SetDefault("gitea.url", "") viper.SetDefault("gitea.token", "") - viper.SetDefault("gitea.webhookSecret.value", "") - viper.SetDefault("gitea.webhookSecret.file", "") + viper.SetDefault("gitea.webhook.secret", "") + viper.SetDefault("gitea.webhook.secretFile", "") viper.SetDefault("gitea.repositories", []GiteaRepository{}) viper.SetDefault("sonarqube.url", "") viper.SetDefault("sonarqube.token", "") - viper.SetDefault("sonarqube.webhookSecret.value", "") - viper.SetDefault("sonarqube.webhookSecret.file", "") + viper.SetDefault("sonarqube.webhook.secret", "") + viper.SetDefault("sonarqube.webhook.secretFile", "") viper.SetDefault("sonarqube.projects", []string{}) } @@ -91,11 +91,11 @@ func Load(configPath string) { Gitea = fullConfig.Gitea SonarQube = fullConfig.SonarQube - if Gitea.WebhookSecret.File != "" { - Gitea.WebhookSecret.Value = ReadSecretFile(Gitea.WebhookSecret.File) + if Gitea.Webhook.SecretFile != "" { + Gitea.Webhook.Secret = ReadSecretFile(Gitea.Webhook.SecretFile) } - if SonarQube.WebhookSecret.File != "" { - SonarQube.WebhookSecret.Value = ReadSecretFile(SonarQube.WebhookSecret.File) + if SonarQube.Webhook.SecretFile != "" { + SonarQube.Webhook.Secret = ReadSecretFile(SonarQube.Webhook.SecretFile) } } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 342d26d..e6d1efb 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -13,25 +13,16 @@ var defaultConfigInlineSecrets []byte = []byte( `gitea: url: https://example.com/gitea token: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 - webhookSecret: - value: haxxor-gitea-secret - repositories: [] -sonarqube: - url: https://example.com/sonarqube - token: a09eb5785b25bb2cbacf48808a677a0709f02d8e - webhookSecret: - value: haxxor-sonarqube-secret - projects: [] -`) - -var incompleteConfig []byte = []byte( -`gitea: - url: https://example.com/gitea - webhookSecret: - value: haxxor-gitea-secret + webhook: + secret: haxxor-gitea-secret + repositories: + - owner: some-owner + name: a-repository-name sonarqube: url: https://example.com/sonarqube token: a09eb5785b25bb2cbacf48808a677a0709f02d8e + webhook: + secret: haxxor-sonarqube-secret projects: [] `) @@ -63,17 +54,22 @@ func TestLoadGiteaStructure(t *testing.T) { expected := GiteaConfig{ Url: "https://example.com/gitea", Token: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", - WebhookSecret: WebhookSecret{ - Value: "haxxor-gitea-secret", + Webhook: Webhook{ + Secret: "haxxor-gitea-secret", + }, + Repositories: []GiteaRepository{ + GiteaRepository{ + Owner: "some-owner", + Name: "a-repository-name", + }, }, - Repositories: []GiteaRepository{}, } assert.EqualValues(t, expected, Gitea) } -func TestLoadGiteaStructureWithEnvInjectedWebhookSecret(t *testing.T) { - os.Setenv("PRBOT_GITEA_WEBHOOKSECRET_VALUE", "injected-secret") +func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { + os.Setenv("PRBOT_GITEA_WEBHOOK_SECRET", "injected-webhook-secret") os.Setenv("PRBOT_GITEA_TOKEN", "injected-token") WriteConfigFile(t, defaultConfigInlineSecrets) Load(os.TempDir()) @@ -81,37 +77,90 @@ func TestLoadGiteaStructureWithEnvInjectedWebhookSecret(t *testing.T) { expected := GiteaConfig{ Url: "https://example.com/gitea", Token: "injected-token", - WebhookSecret: WebhookSecret{ - Value: "injected-secret", + Webhook: Webhook{ + Secret: "injected-webhook-secret", + }, + Repositories: []GiteaRepository{ + GiteaRepository{ + Owner: "some-owner", + Name: "a-repository-name", + }, }, - Repositories: []GiteaRepository{}, } assert.EqualValues(t, expected, Gitea) t.Cleanup(func() { - os.Unsetenv("PRBOT_GITEA_WEBHOOKSECRET_VALUE") + os.Unsetenv("PRBOT_GITEA_WEBHOOK_SECRET") os.Unsetenv("PRBOT_GITEA_TOKEN") }) } -func TestLoadStructureWithResolvedWebhookFileFromEnvInjected(t *testing.T) { - secretFile := path.Join(os.TempDir(), "webhook-secret-sonarqube") - _ = ioutil.WriteFile(secretFile, []byte(`totally-secret`),0444) - - os.Setenv("PRBOT_GITEA_WEBHOOKSECRET_FILE", secretFile) - os.Setenv("PRBOT_SONARQUBE_WEBHOOKSECRET_FILE", secretFile) - os.Setenv("PRBOT_GITEA_TOKEN", "injected-token") - - WriteConfigFile(t, incompleteConfig) +func TestLoadSonarQubeStructure(t *testing.T) { + WriteConfigFile(t, defaultConfigInlineSecrets) Load(os.TempDir()) + expected := SonarQubeConfig{ + Url: "https://example.com/sonarqube", + Token: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", + Webhook: Webhook{ + Secret: "haxxor-sonarqube-secret", + }, + Projects: []string{}, + } + + assert.EqualValues(t, expected, SonarQube) +} + +func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { + os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRET", "injected-webhook-secret") + os.Setenv("PRBOT_SONARQUBE_TOKEN", "injected-token") + WriteConfigFile(t, defaultConfigInlineSecrets) + Load(os.TempDir()) + + expected := SonarQubeConfig{ + Url: "https://example.com/sonarqube", + Token: "injected-token", + Webhook: Webhook{ + Secret: "injected-webhook-secret", + }, + Projects: []string{}, + } + + assert.EqualValues(t, expected, SonarQube) + + t.Cleanup(func() { + os.Unsetenv("PRBOT_SONARQUBE_WEBHOOK_SECRET") + os.Unsetenv("PRBOT_SONARQUBE_TOKEN") + }) +} + +func TestLoadStructureWithFileReferenceResolving(t *testing.T) { + giteaSecretFile := path.Join(os.TempDir(), "webhook-secret-gitea") + sonarqubeSecretFile := path.Join(os.TempDir(), "webhook-secret-sonarqube") + + _ = ioutil.WriteFile(giteaSecretFile, []byte(`gitea-totally-secret`),0444) + _ = ioutil.WriteFile(sonarqubeSecretFile, []byte(`sonarqube-totally-secret`),0444) + + WriteConfigFile(t, []byte( +`gitea: + url: https://example.com/gitea + token: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 + repositories: [] +sonarqube: + url: https://example.com/sonarqube + token: a09eb5785b25bb2cbacf48808a677a0709f02d8e + projects: [] +`)) + os.Setenv("PRBOT_GITEA_WEBHOOK_SECRETFILE", giteaSecretFile) + os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRETFILE", sonarqubeSecretFile) + expectedGitea := GiteaConfig{ Url: "https://example.com/gitea", - Token: "injected-token", - WebhookSecret: WebhookSecret{ - Value: "totally-secret", - File: secretFile, + Token: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", + Webhook: Webhook{ + Secret: "gitea-totally-secret", + SecretFile: giteaSecretFile, }, Repositories: []GiteaRepository{}, } @@ -119,38 +168,21 @@ func TestLoadStructureWithResolvedWebhookFileFromEnvInjected(t *testing.T) { expectedSonarQube := SonarQubeConfig{ Url: "https://example.com/sonarqube", Token: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", - WebhookSecret: WebhookSecret{ - Value: "totally-secret", - File: secretFile, + Webhook: Webhook{ + Secret: "sonarqube-totally-secret", + SecretFile: sonarqubeSecretFile, }, Projects: []string{}, } + Load(os.TempDir()) assert.EqualValues(t, expectedGitea, Gitea) assert.EqualValues(t, expectedSonarQube, SonarQube) t.Cleanup(func() { - os.Remove(secretFile) - os.Unsetenv("PRBOT_SONARQUBE_WEBHOOKSECRET_FILE") - os.Unsetenv("PRBOT_GITEA_TOKEN") - }) -} - -func TestReadSecretFileWhenDirectoryProvided(t *testing.T) { - assert.Panics(t, func() { ReadSecretFile(os.TempDir()) }, "No panic while trying to read content from directory") -} - -func TestReadSecretFileWhenMissingFileProvided(t *testing.T) { - assert.Panics(t, func() { ReadSecretFile(path.Join(os.TempDir(), "secret-file")) }, "No panic while trying to read missing file") -} - -func TestReadSecretFile(t *testing.T) { - secretFile := path.Join(os.TempDir(), "secret-file") - _ = ioutil.WriteFile(secretFile, []byte(`awesome-secret-content`),0444) - - assert.Equal(t, "awesome-secret-content", ReadSecretFile(secretFile)) - - t.Cleanup(func() { - os.Remove(secretFile) + os.Remove(giteaSecretFile) + os.Remove(sonarqubeSecretFile) + os.Unsetenv("PRBOT_GITEA_WEBHOOK_SECRETFILE") + os.Unsetenv("PRBOT_SONARQUBE_WEBHOOK_SECRETFILE") }) } From 4797d38c70ce8ac91103d578b40d336f520d7aaf Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 21 Jun 2021 11:08:56 +0200 Subject: [PATCH 015/128] Switch token configuration to object Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/settings/settings.go | 20 ++++++++++---- internal/settings/settings_test.go | 44 ++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 7ab2c5c..4560381 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -13,6 +13,11 @@ type GiteaRepository struct { Name string } +type Token struct { + Value string + File string +} + type Webhook struct { Secret string SecretFile string @@ -20,15 +25,15 @@ type Webhook struct { type GiteaConfig struct { Url string - Token string - Webhook Webhook `mapstructure:"webhook"` + Token Token + Webhook Webhook Repositories []GiteaRepository } type SonarQubeConfig struct { Url string - Token string - Webhook Webhook `mapstructure:"webhook"` + Token Token + Webhook Webhook Projects []string } @@ -50,12 +55,15 @@ func init() { func ApplyConfigDefaults() { viper.SetDefault("gitea.url", "") - viper.SetDefault("gitea.token", "") + viper.SetDefault("gitea.token.value", "") + viper.SetDefault("gitea.token.file", "") viper.SetDefault("gitea.webhook.secret", "") viper.SetDefault("gitea.webhook.secretFile", "") viper.SetDefault("gitea.repositories", []GiteaRepository{}) + viper.SetDefault("sonarqube.url", "") - viper.SetDefault("sonarqube.token", "") + viper.SetDefault("sonarqube.token.value", "") + viper.SetDefault("sonarqube.token.file", "") viper.SetDefault("sonarqube.webhook.secret", "") viper.SetDefault("sonarqube.webhook.secretFile", "") viper.SetDefault("sonarqube.projects", []string{}) diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index e6d1efb..f37a477 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -12,7 +12,8 @@ import ( var defaultConfigInlineSecrets []byte = []byte( `gitea: url: https://example.com/gitea - token: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 + token: + value: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 webhook: secret: haxxor-gitea-secret repositories: @@ -20,7 +21,8 @@ var defaultConfigInlineSecrets []byte = []byte( name: a-repository-name sonarqube: url: https://example.com/sonarqube - token: a09eb5785b25bb2cbacf48808a677a0709f02d8e + token: + value: a09eb5785b25bb2cbacf48808a677a0709f02d8e webhook: secret: haxxor-sonarqube-secret projects: [] @@ -53,7 +55,9 @@ func TestLoadGiteaStructure(t *testing.T) { expected := GiteaConfig{ Url: "https://example.com/gitea", - Token: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", + Token: Token{ + Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", + }, Webhook: Webhook{ Secret: "haxxor-gitea-secret", }, @@ -70,13 +74,15 @@ func TestLoadGiteaStructure(t *testing.T) { func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { os.Setenv("PRBOT_GITEA_WEBHOOK_SECRET", "injected-webhook-secret") - os.Setenv("PRBOT_GITEA_TOKEN", "injected-token") + os.Setenv("PRBOT_GITEA_TOKEN_VALUE", "injected-token") WriteConfigFile(t, defaultConfigInlineSecrets) Load(os.TempDir()) expected := GiteaConfig{ Url: "https://example.com/gitea", - Token: "injected-token", + Token: Token{ + Value: "injected-token", + }, Webhook: Webhook{ Secret: "injected-webhook-secret", }, @@ -92,7 +98,7 @@ func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { t.Cleanup(func() { os.Unsetenv("PRBOT_GITEA_WEBHOOK_SECRET") - os.Unsetenv("PRBOT_GITEA_TOKEN") + os.Unsetenv("PRBOT_GITEA_TOKEN_VALUE") }) } @@ -102,7 +108,9 @@ func TestLoadSonarQubeStructure(t *testing.T) { expected := SonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", + Token: Token{ + Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", + }, Webhook: Webhook{ Secret: "haxxor-sonarqube-secret", }, @@ -114,13 +122,15 @@ func TestLoadSonarQubeStructure(t *testing.T) { func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRET", "injected-webhook-secret") - os.Setenv("PRBOT_SONARQUBE_TOKEN", "injected-token") + os.Setenv("PRBOT_SONARQUBE_TOKEN_VALUE", "injected-token") WriteConfigFile(t, defaultConfigInlineSecrets) Load(os.TempDir()) expected := SonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: "injected-token", + Token: Token{ + Value: "injected-token", + }, Webhook: Webhook{ Secret: "injected-webhook-secret", }, @@ -131,7 +141,7 @@ func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { t.Cleanup(func() { os.Unsetenv("PRBOT_SONARQUBE_WEBHOOK_SECRET") - os.Unsetenv("PRBOT_SONARQUBE_TOKEN") + os.Unsetenv("PRBOT_SONARQUBE_TOKEN_VALUE") }) } @@ -145,11 +155,13 @@ func TestLoadStructureWithFileReferenceResolving(t *testing.T) { WriteConfigFile(t, []byte( `gitea: url: https://example.com/gitea - token: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 + token: + value: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 repositories: [] sonarqube: url: https://example.com/sonarqube - token: a09eb5785b25bb2cbacf48808a677a0709f02d8e + token: + value: a09eb5785b25bb2cbacf48808a677a0709f02d8e projects: [] `)) os.Setenv("PRBOT_GITEA_WEBHOOK_SECRETFILE", giteaSecretFile) @@ -157,7 +169,9 @@ sonarqube: expectedGitea := GiteaConfig{ Url: "https://example.com/gitea", - Token: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", + Token: Token{ + Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", + }, Webhook: Webhook{ Secret: "gitea-totally-secret", SecretFile: giteaSecretFile, @@ -167,7 +181,9 @@ sonarqube: expectedSonarQube := SonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", + Token: Token{ + Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", + }, Webhook: Webhook{ Secret: "sonarqube-totally-secret", SecretFile: sonarqubeSecretFile, From f84442009c09b1adc278b6aa80a3853419f54007 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 21 Jun 2021 11:30:02 +0200 Subject: [PATCH 016/128] Allow loading token value from file reference Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- config/config.example.yaml | 10 ++++++-- internal/settings/settings.go | 17 ++++++------- internal/settings/settings_test.go | 38 ++++++++++++++++++++---------- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/config/config.example.yaml b/config/config.example.yaml index a3c2639..76e4c2c 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -5,7 +5,10 @@ gitea: # Created access token for the user that shall be used as bot account. # User needs "Read project" permissions with access to "Pull Requests" - token: "" + token: + value: "" + # # or path to file containing the plain text secret + # file: /path/to/gitea/token # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the # request will be ignored. @@ -35,7 +38,10 @@ sonarqube: # Created access token for the user that shall be used as bot account. # User needs "Browse on project" permissions - token: "" + token: + value: "" + # # or path to file containing the plain text secret + # file: /path/to/sonarqube/token # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the # request will be ignored. diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 4560381..070668f 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -69,7 +69,11 @@ func ApplyConfigDefaults() { viper.SetDefault("sonarqube.projects", []string{}) } -func ReadSecretFile(file string) string { +func ReadSecretFile(file string, defaultValue string) (string) { + if file == "" { + return defaultValue + } + content, err := ioutil.ReadFile(file) if err != nil { panic(fmt.Errorf("Cannot read '%s' or it is no regular file. %w", file, err)) @@ -99,11 +103,8 @@ func Load(configPath string) { Gitea = fullConfig.Gitea SonarQube = fullConfig.SonarQube - if Gitea.Webhook.SecretFile != "" { - Gitea.Webhook.Secret = ReadSecretFile(Gitea.Webhook.SecretFile) - } - - if SonarQube.Webhook.SecretFile != "" { - SonarQube.Webhook.Secret = ReadSecretFile(SonarQube.Webhook.SecretFile) - } + Gitea.Webhook.Secret = ReadSecretFile(Gitea.Webhook.SecretFile, Gitea.Webhook.Secret) + Gitea.Token.Value = ReadSecretFile(Gitea.Token.File, Gitea.Token.Value) + SonarQube.Webhook.Secret = ReadSecretFile(SonarQube.Webhook.SecretFile, SonarQube.Webhook.Secret) + SonarQube.Token.Value = ReadSecretFile(SonarQube.Token.File, SonarQube.Token.Value) } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index f37a477..6e36a2f 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -146,35 +146,44 @@ func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { } func TestLoadStructureWithFileReferenceResolving(t *testing.T) { - giteaSecretFile := path.Join(os.TempDir(), "webhook-secret-gitea") - sonarqubeSecretFile := path.Join(os.TempDir(), "webhook-secret-sonarqube") + giteaWebhookSecretFile := path.Join(os.TempDir(), "webhook-secret-gitea") + _ = ioutil.WriteFile(giteaWebhookSecretFile, []byte(`gitea-totally-secret`),0444) - _ = ioutil.WriteFile(giteaSecretFile, []byte(`gitea-totally-secret`),0444) - _ = ioutil.WriteFile(sonarqubeSecretFile, []byte(`sonarqube-totally-secret`),0444) + giteaTokenFile := path.Join(os.TempDir(), "token-secret-gitea") + _ = ioutil.WriteFile(giteaTokenFile, []byte(`d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565`),0444) + + sonarqubeWebhookSecretFile := path.Join(os.TempDir(), "webhook-secret-sonarqube") + _ = ioutil.WriteFile(sonarqubeWebhookSecretFile, []byte(`sonarqube-totally-secret`),0444) + + sonarqubeTokenFile := path.Join(os.TempDir(), "token-secret-sonarqube") + _ = ioutil.WriteFile(sonarqubeTokenFile, []byte(`a09eb5785b25bb2cbacf48808a677a0709f02d8e`),0444) WriteConfigFile(t, []byte( `gitea: url: https://example.com/gitea token: - value: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 + value: fake-gitea-token repositories: [] sonarqube: url: https://example.com/sonarqube token: - value: a09eb5785b25bb2cbacf48808a677a0709f02d8e + value: fake-sonarqube-token projects: [] `)) - os.Setenv("PRBOT_GITEA_WEBHOOK_SECRETFILE", giteaSecretFile) - os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRETFILE", sonarqubeSecretFile) + os.Setenv("PRBOT_GITEA_WEBHOOK_SECRETFILE", giteaWebhookSecretFile) + os.Setenv("PRBOT_GITEA_TOKEN_FILE", giteaTokenFile) + os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRETFILE", sonarqubeWebhookSecretFile) + os.Setenv("PRBOT_SONARQUBE_TOKEN_FILE", sonarqubeTokenFile) expectedGitea := GiteaConfig{ Url: "https://example.com/gitea", Token: Token{ Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", + File: giteaTokenFile, }, Webhook: Webhook{ Secret: "gitea-totally-secret", - SecretFile: giteaSecretFile, + SecretFile: giteaWebhookSecretFile, }, Repositories: []GiteaRepository{}, } @@ -183,10 +192,11 @@ sonarqube: Url: "https://example.com/sonarqube", Token: Token{ Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", + File: sonarqubeTokenFile, }, Webhook: Webhook{ Secret: "sonarqube-totally-secret", - SecretFile: sonarqubeSecretFile, + SecretFile: sonarqubeWebhookSecretFile, }, Projects: []string{}, } @@ -196,9 +206,13 @@ sonarqube: assert.EqualValues(t, expectedSonarQube, SonarQube) t.Cleanup(func() { - os.Remove(giteaSecretFile) - os.Remove(sonarqubeSecretFile) + os.Remove(giteaWebhookSecretFile) + os.Remove(giteaTokenFile) + os.Remove(sonarqubeWebhookSecretFile) + os.Remove(sonarqubeTokenFile) os.Unsetenv("PRBOT_GITEA_WEBHOOK_SECRETFILE") + os.Unsetenv("PRBOT_GITEA_TOKEN_FILE") os.Unsetenv("PRBOT_SONARQUBE_WEBHOOK_SECRETFILE") + os.Unsetenv("PRBOT_SONARQUBE_TOKEN_FILE") }) } From 6dfb2dd8460ef72c9cb9c776792132197cefe321 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Tue, 22 Jun 2021 11:27:53 +0200 Subject: [PATCH 017/128] Properly map projects from SQ and Gitea in config Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- config/config.example.yaml | 27 +++++--------- internal/settings/settings.go | 16 ++++++-- internal/settings/settings_test.go | 59 ++++++++++++++++-------------- 3 files changed, 53 insertions(+), 49 deletions(-) diff --git a/config/config.example.yaml b/config/config.example.yaml index 76e4c2c..5c845c6 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -19,18 +19,6 @@ gitea: # # or path to file containing the plain text secret # secretFile: /path/to/gitea/webhook/secret - # List of repository the used Gitea account has access to and shall be handled by the bot. Other repository webhooks - # will be ignored. - # A repository specification contains the owner name and the repository name itself. The owner can be the name of a - # real account or an organization in which the repository is located. - # If empty array given, don't filter requests for repositories and proceed them all. - repositories: [] - # - owner: justusbunsi - # name: example-repo - # - owner: my-organization - # name: example-repo - - # SonarQube related configuration. Necessary for requesting data from the API and processing the webhook. sonarqube: # API endpoint of your SonarQube instance. @@ -53,8 +41,13 @@ sonarqube: # # or path to file containing the plain text secret # secretFile: /path/to/sonarqube/webhook/secret - # List of project keys from inside SonarQube that should be handled. Webhooks containing other projects will be ignored. - # If empty array given, don't filter requests for repositories and proceed them all. - projects: [] - # - project-1 - # - project-2 +# List of project mappings to take care of. Webhooks for other projects will be ignored. +# At least one must be configured. Otherwise all webhooks (no matter which source) because the bot cannot map on its own. +projects: + - sonarqube: + key: project-1 + # A repository specification contains the owner name and the repository name itself. The owner can be the name of a + # real account or an organization in which the repository is located. + gitea: + owner: justusbunsi + name: example-repo diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 070668f..943251d 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -27,19 +27,25 @@ type GiteaConfig struct { Url string Token Token Webhook Webhook - Repositories []GiteaRepository } type SonarQubeConfig struct { Url string Token Token Webhook Webhook - Projects []string +} + +type Project struct { + SonarQube struct { + Key string + } `mapstructure:"sonarqube"` + Gitea GiteaRepository } var ( Gitea GiteaConfig SonarQube SonarQubeConfig + Projects []Project ) func init() { @@ -59,14 +65,14 @@ func ApplyConfigDefaults() { viper.SetDefault("gitea.token.file", "") viper.SetDefault("gitea.webhook.secret", "") viper.SetDefault("gitea.webhook.secretFile", "") - viper.SetDefault("gitea.repositories", []GiteaRepository{}) viper.SetDefault("sonarqube.url", "") viper.SetDefault("sonarqube.token.value", "") viper.SetDefault("sonarqube.token.file", "") viper.SetDefault("sonarqube.webhook.secret", "") viper.SetDefault("sonarqube.webhook.secretFile", "") - viper.SetDefault("sonarqube.projects", []string{}) + + viper.SetDefault("projects", []Project{}) } func ReadSecretFile(file string, defaultValue string) (string) { @@ -93,6 +99,7 @@ func Load(configPath string) { var fullConfig struct { Gitea GiteaConfig SonarQube SonarQubeConfig `mapstructure:"sonarqube"` + Projects []Project } err = viper.Unmarshal(&fullConfig) @@ -102,6 +109,7 @@ func Load(configPath string) { Gitea = fullConfig.Gitea SonarQube = fullConfig.SonarQube + Projects = fullConfig.Projects Gitea.Webhook.Secret = ReadSecretFile(Gitea.Webhook.SecretFile, Gitea.Webhook.Secret) Gitea.Token.Value = ReadSecretFile(Gitea.Token.File, Gitea.Token.Value) diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 6e36a2f..a9e475b 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -9,23 +9,25 @@ import ( "github.com/stretchr/testify/assert" ) -var defaultConfigInlineSecrets []byte = []byte( +var defaultConfig []byte = []byte( `gitea: url: https://example.com/gitea token: value: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 webhook: secret: haxxor-gitea-secret - repositories: - - owner: some-owner - name: a-repository-name sonarqube: url: https://example.com/sonarqube token: value: a09eb5785b25bb2cbacf48808a677a0709f02d8e webhook: secret: haxxor-sonarqube-secret - projects: [] +projects: + - sonarqube: + key: gitea-sonarqube-pr-bot + gitea: + owner: example-organization + name: pr-bot `) func WriteConfigFile(t *testing.T, content []byte) { @@ -44,13 +46,13 @@ func TestLoadWithMissingFile(t *testing.T) { } func TestLoadWithExistingFile(t *testing.T) { - WriteConfigFile(t, defaultConfigInlineSecrets) + WriteConfigFile(t, defaultConfig) assert.NotPanics(t, func() { Load(os.TempDir()) }, "Unexpected panic while reading existing file") } func TestLoadGiteaStructure(t *testing.T) { - WriteConfigFile(t, defaultConfigInlineSecrets) + WriteConfigFile(t, defaultConfig) Load(os.TempDir()) expected := GiteaConfig{ @@ -61,12 +63,6 @@ func TestLoadGiteaStructure(t *testing.T) { Webhook: Webhook{ Secret: "haxxor-gitea-secret", }, - Repositories: []GiteaRepository{ - GiteaRepository{ - Owner: "some-owner", - Name: "a-repository-name", - }, - }, } assert.EqualValues(t, expected, Gitea) @@ -75,7 +71,7 @@ func TestLoadGiteaStructure(t *testing.T) { func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { os.Setenv("PRBOT_GITEA_WEBHOOK_SECRET", "injected-webhook-secret") os.Setenv("PRBOT_GITEA_TOKEN_VALUE", "injected-token") - WriteConfigFile(t, defaultConfigInlineSecrets) + WriteConfigFile(t, defaultConfig) Load(os.TempDir()) expected := GiteaConfig{ @@ -86,12 +82,6 @@ func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { Webhook: Webhook{ Secret: "injected-webhook-secret", }, - Repositories: []GiteaRepository{ - GiteaRepository{ - Owner: "some-owner", - Name: "a-repository-name", - }, - }, } assert.EqualValues(t, expected, Gitea) @@ -103,7 +93,7 @@ func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { } func TestLoadSonarQubeStructure(t *testing.T) { - WriteConfigFile(t, defaultConfigInlineSecrets) + WriteConfigFile(t, defaultConfig) Load(os.TempDir()) expected := SonarQubeConfig{ @@ -114,7 +104,6 @@ func TestLoadSonarQubeStructure(t *testing.T) { Webhook: Webhook{ Secret: "haxxor-sonarqube-secret", }, - Projects: []string{}, } assert.EqualValues(t, expected, SonarQube) @@ -123,7 +112,7 @@ func TestLoadSonarQubeStructure(t *testing.T) { func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRET", "injected-webhook-secret") os.Setenv("PRBOT_SONARQUBE_TOKEN_VALUE", "injected-token") - WriteConfigFile(t, defaultConfigInlineSecrets) + WriteConfigFile(t, defaultConfig) Load(os.TempDir()) expected := SonarQubeConfig{ @@ -134,7 +123,6 @@ func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { Webhook: Webhook{ Secret: "injected-webhook-secret", }, - Projects: []string{}, } assert.EqualValues(t, expected, SonarQube) @@ -163,12 +151,10 @@ func TestLoadStructureWithFileReferenceResolving(t *testing.T) { url: https://example.com/gitea token: value: fake-gitea-token - repositories: [] sonarqube: url: https://example.com/sonarqube token: value: fake-sonarqube-token - projects: [] `)) os.Setenv("PRBOT_GITEA_WEBHOOK_SECRETFILE", giteaWebhookSecretFile) os.Setenv("PRBOT_GITEA_TOKEN_FILE", giteaTokenFile) @@ -185,7 +171,6 @@ sonarqube: Secret: "gitea-totally-secret", SecretFile: giteaWebhookSecretFile, }, - Repositories: []GiteaRepository{}, } expectedSonarQube := SonarQubeConfig{ @@ -198,7 +183,6 @@ sonarqube: Secret: "sonarqube-totally-secret", SecretFile: sonarqubeWebhookSecretFile, }, - Projects: []string{}, } Load(os.TempDir()) @@ -216,3 +200,22 @@ sonarqube: os.Unsetenv("PRBOT_SONARQUBE_TOKEN_FILE") }) } + +func TestLoadProjectsStructure(t *testing.T) { + WriteConfigFile(t, defaultConfig) + Load(os.TempDir()) + + expectedProjects := []Project{ + Project{ + SonarQube: struct {Key string}{ + Key: "gitea-sonarqube-pr-bot", + }, + Gitea: GiteaRepository{ + Owner: "example-organization", + Name: "pr-bot", + }, + }, + } + + assert.EqualValues(t, expectedProjects, Projects) +} From 08eab186dbe9881959d519421f43477827a7268e Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Thu, 24 Jun 2021 10:36:52 +0200 Subject: [PATCH 018/128] Let the bot panic if no project mapping provided The bot cannot properly handle webhooks when no projects are configured. So it is good to not let the bot run with such an invalid configuration. Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/settings/settings.go | 4 ++++ internal/settings/settings_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 943251d..04c9dc4 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -107,6 +107,10 @@ func Load(configPath string) { panic(fmt.Errorf("Unable to load config into struct, %v", err)) } + if len(fullConfig.Projects) == 0 { + panic("Invalid configuration. At least one project mapping is necessary.") + } + Gitea = fullConfig.Gitea SonarQube = fullConfig.SonarQube Projects = fullConfig.Projects diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index a9e475b..a2631eb 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -155,6 +155,12 @@ sonarqube: url: https://example.com/sonarqube token: value: fake-sonarqube-token +projects: + - sonarqube: + key: gitea-sonarqube-pr-bot + gitea: + owner: example-organization + name: pr-bot `)) os.Setenv("PRBOT_GITEA_WEBHOOK_SECRETFILE", giteaWebhookSecretFile) os.Setenv("PRBOT_GITEA_TOKEN_FILE", giteaTokenFile) @@ -219,3 +225,24 @@ func TestLoadProjectsStructure(t *testing.T) { assert.EqualValues(t, expectedProjects, Projects) } + +func TestLoadProjectsStructureWithNoMapping(t *testing.T) { + invalidConfig := []byte( +`gitea: + url: https://example.com/gitea + token: + value: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 + webhook: + secret: haxxor-gitea-secret +sonarqube: + url: https://example.com/sonarqube + token: + value: a09eb5785b25bb2cbacf48808a677a0709f02d8e + webhook: + secret: haxxor-sonarqube-secret +projects: [] +`) + WriteConfigFile(t, invalidConfig) + + assert.Panics(t, func() { Load(os.TempDir()) }, "No panic for empty project mapping that is required") +} From e42c671689394ccce44e77fa56c935a186712775 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Thu, 24 Jun 2021 10:52:14 +0200 Subject: [PATCH 019/128] Document TODOs Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index cbd7ae9..4ae98ab 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,23 @@ Luckily, both endpoints have a proper REST API to communicate with each others. ## Table of Contents - [Gitea SonarQube PR Bot](#gitea-sonarqube-pr-bot) + - [TODOs](#todos) - [Workflow](#workflow) - [Setup](#setup) - [Bot configuration](#bot-configuration) - [Contributing](#contributing) - [License](#license) +## TODOs + +- [ ] Maybe drop `PRBOT_CONFIG_PATH` environment variable in favor of `--config path/to/config.yaml` cli attribute +- [ ] Configure SonarQube PR branch naming pattern for more flexibility (currently focused on Jenkins with [Gitea Plugin](https://github.com/jenkinsci/gitea-plugin)) +- [ ] Configuration live reloading +- [ ] _Caching_ of outgoing requests in case the target is not available +- [ ] Parsable logging for monitoring +- [ ] Official image for containerized hosting +- [ ] Helm chart for Kubernetes + ## Workflow ![Workflow](docs/workflow.png) From 118c08090a4d336a7a42776dcfcd95325fba93ea Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Thu, 24 Jun 2021 11:13:42 +0200 Subject: [PATCH 020/128] Add missing SQ communication in docs Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- docs/workflow.drawio | 2 +- docs/workflow.png | Bin 78680 -> 79069 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/workflow.drawio b/docs/workflow.drawio index a2e5428..d33ca54 100644 --- a/docs/workflow.drawio +++ b/docs/workflow.drawio @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/docs/workflow.png b/docs/workflow.png index 1ac9d5692de062186b2b3de497ff221b2106b88a..9bec2a5d308b4c5700b87795bf7dd268f22c0beb 100644 GIT binary patch literal 79069 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∋7n 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

bH?C(N=}#LCNi-1E_gb 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 literal 78680 zcmY(qcRZX?*ENjjM3f-W%V?t$(K}&8H=}n35k!yPOAx{6(YqNfT67Y<6Eb=iy+`lg zBeV_XeGiLmpbFO{%*?aA^*NM_pS9*$1g^z}Y_7tS7ppAxx0sr@di;emwhqb;B z4UGW}q#z6SGTk@9@d6{KOaynbH09XyRE}s#lJkS^Zu~5Nn$md^Q|o^V?eF^Qm8;Pc z&1CsA8jD1MBjl-q%vUCK48~-DAcxN5hpcqLCOmYE#Lt2jIgjhlnwlHV=6kY@V=b!)27yyXWlm9&=E{1GmQ2g&)UzQri z|6Twi{O`;EyUhR0hbx8|R*1dGneY_vzm|myN*?=Pt3*vCGtS5W5ZX44a!~$%kNR&6 zWrQF0f3H1+2%Qlwd#PeA|KH1!)7}vLzYqSOwG+GJ;)0$w>oHN2{1bZqJyH+wXjye{ zT5)Y!b#FoHQFb)1-tR`tKW)4Y+~v%Du%uxnQw`dRiwe9FX81g<#+s(a8kWu(Ae4bt zd-?T7^hnTeEN9R5>a^=9{Oj~(-+oq?^U;I6Zkeyey>aK8X{)Fr;&I+rKq9n9*UuOE9ua61-?bZs)BnZD3|9r!mSULNw zd9IAf>rVPePjRC4PfXy)F8M)A=FaE8m^fT9KH)vn35*`xR|b6wo_zXTe9?}Ieuo80 zN19@?t5AEzt`M1;SQ*MfG-E8YgAV;Ys1j>C!h)!@RV3iuIy)?CfqL3jXT z@&}~t9r4utv~y3 zfOrCHUL?~w;O5S7%^B`?{0!7FdBFI%z|6n)uwKCQ&3BHR@zgsh79Zu&2TTlrde4*O zSuAwfTx=2s(BjRwKA=|txK`(&!D!4wiCx_5gVdz-+)TBc&eL{B(8ta=!?-9t|D>+T zBs?8i@6zI(_>2s+$v3h0-v_emARHzeai6)kHXZln2_%VR+GV`hVbOOgq)g76IaElT zaSlsYZeNJb-AEQAp|z3IE;LAxlF|@RvU~&EPM?fz)sED5Ss&P3Vf=8{UOF5Vwv8)7 zOH}n2U!S({tBmT4f$z5QD+%3-k?G_de>`9c8K@`gOVRn-H6v1%xzykPMD$EPuJ=UwQ?!hnJHU0&nB5&s-Vl(dMH9$~ zt2D2))Z6}Ynt>;TEuIl|{@BD((M;n)BPi317?%-qS0jK{dfAt8?v-ly+!NhDsiOSN zko+Ue$iYngw&f5VSpIl*KwWb?e+#BB*K7Cp#G$%T3A={6)$;IU4t|U69I7qf0DoP) z3nRxDa}qz5l0@jIMQaUOwZ%nU-_l+Mbdam9F(s|R`z~?H+1cGJt%nrig3@ss@PZmmjaCceX9Kymi8R;Q9uyOMRRz29HHB+?`j&L(d$&ds+B2z#EcD52wlC@x zPLO=IKJFW~v`%&&^;*sZ48JGRCvSQs^$79FQ_$n(;0re3H@4$kHnx?;Y!# zukEiQ`RwNg$`25~ZJ2+WDuo=f96a|FEyd?uHVW&lCfVL%xdI%}KPE&{o8CNc(l9C0 z7P9_7k^cYBc*Fv@uUc*COq*`F!unWp)wG|BQG*po=@G#Nt-wED+s~>+JqfJqe)ZFjwWtK1kSAK6_9?1Y(Iq}i9)(_um*h;nV4dC!8>So|x zDj=*Jssm!d6dDs!S59s|-g2>*6>_;cB4X~T`ttbQo}(p4%fLni?FCB7X9n#{be+9D zM~(uDLX53-R2-u}j>=Id+mvaC;RtijGA8FMeD z>4dZ?z9xJf?_#=s)7pE#D_fru=Am!cR5SRV1?p)mcuDq0!n;C+&BC8gIMNJ*i{?aLahq;LxY~uxSDGeiQ;G%*a`pZsZPAvA zU3Dg%ZY3>Y3SGeIOIgrX+*HP#XZ21R7`ANrTHv8wh#$2lTc5bj6TPcp5m*&z-K?y@RXFVxbV zr_J@XJDgLXB2t}pdy0>$f=u#uix+1>{#n;e>*yBf45S&w-1qS%MUv>kww5#vExj+! ztcGcUs{!=CJ%beY8(Qf}+Xu_P+Yf1eHcHFc@O`anYbR!iCZY&n`qZk^-y%@AH8Izf z$Vwy=d2d!i$s{#V{#^Q*Y<>RkRk>TAU(o%5%hoza|tYB;xA{+QH=#*!?vw{{~bn%7!$uYUf0%Q%Gk)HN`zdvMp|H zmfV9(uwFY1aBAI7Ul)VT;kh-tIk(e0n1GR{BK6EgCetGN?P@?9adpJFG~UhEdkV!6 zP{es|#q&$W^F~|dv4>{@%fgcg+>cw>bQ7c1ELG0~`QwCG-ejf9S4r1-OZ??#*}l2* z?a%g(1&%IjKz`^`$|o#w3G1&I;nKydSH!@pQX;qR&%sRARVPf?TjQwgkz=#k&Qvr63&9ao zr$Uch>^jjL$p$8^iuEpfd1?l7{9DPP-rvlMDB*XIEWM#U2s{g{0@oE-PT6zegRX3a z_Sipy`OkAh28<1@#DYijgea^}0w~1z7grs4U-Hy6f%&OoDkF+|IK{ao+pUS50rHR8 z_hgA5E)2mw!K1<(9*BzW2-~AImP&97X#9F9ji_3@3b!psKWy6Rnr-w$#=Z_hezS{% znj8}|m`Vu+9%JQ!WdG8>BV+u<4 zQZAzL^sQu%{8w|=K!7R0oY|0-{ew2b2VlUpy|c@rZ5Yhr0(Ig*8W54{k{TDQ+Y6|Y z4z>GuV>gdKWJ2RV=JlzMILggMLJ{OUrb_4Vfl zFmDvdVYnhel@NDF%AV{f_b|mYl&nMhNOf!4dMeF|*3zTUOiBP=utAS;8$9RHY^^}Bw)_S#e-PdpLDbADgylB<8#`N#HHHFyK zb`#}7_M74gT-(_P1VFVwTr1h6ODRL8dx&Q7{J_@32$gE=9-iXG0Rg z!6Kb3iwZnkfusPRr6Sca>vk;-yV*l+D_Boe`*O+92p@Sx+A(vrMeT}$C*BKBk?WM}uYD*@_ zQg#5@&59)<3bUsRF2wlC6(uZNvE&^r1~Ezp?QNlD?4%Z(@viFGSxrqhne%^DAk!Sh za(vIGE!|em$Jme?#ncfk>p-JJB(+un97a8TM)!_RD>8D7aoA9!^sLghS6CD zHW*|J)|6@ktWFYGOTmokmNJl}=@|+LnQR+J#8Uc^{letjlODSlWW`gzIIs}Zx(O9> z?VNa=Ac0AOGylv~4t$4ngul6WEWUlhA{@7dbswC(lQ75%pKC~p)$Pz$ROgxiR*l+< zWQVK2S%x$dTT_aUGqBRM5%%ac!^{qb-tR%0&_kr8k-FKO+&>d@-s)j&#M!gkt+x#a zL&dP&K1DJ6J7119;`5VaFPObVZLG5^GXvoKPr#$Ak1RtWajxPo(JZ4CIR6u5d&5Sm zg;D@~>|1uY6qsVxcF9giF{Hjz(_{3T@Bljj(!&(E#oWX^P{cH|Jbswe9fv%wQ@4x` z=Fw(a1(5tcP!}42F0l~Ak}`EA-Jq{$C`?4u;kI2Z(}P~>65w~#^Bdn*i|4*fLkjbkoqI;-xGrZr>e1s z7_HMf17_;3>3b@$C4N!-gQxbK$c>8YBJv4(aEo3lLD$#R2AiS@HR$}zK7;qfp8m&2 zmhJJ}g-D@7soWX^6!}rAh1%{Z_Dq5Ff4G_zL$bIdE|2!>Y6s=hzvUlM6V#s2v5TVF^z z*)U=*_CUKSW%MJhl!Y3uF_1NEq8<&wJ-EZ&i7m{gDM`XSN)jQ|tBFPqjcY*)UM)tJ9jF%U7g~;Qg({|ZYOjFbZps0UED{IxR23Y)@BBz^z4}CSe&esEg;V^93cwNI zk)}lW^lCu6q8Q=ekcwzC=A``KGFm;x)43VRBU=m5(%x8ZLG3acie}RJM>8>o%5

G@5JjDn87lW{0*H%%7_Kk_a{}+(7v}$<7JfC_n(`uV#9eQT34)ABX{jJ&T{D zt1<8^JD(QDzxm$Pj?5PkLeWwyCk%X$C<)`%;|4A#V z6ECzj$S?h}MauzS-v|t3>cLvSpXVp7_5YL@Qxh#ha?2t4shaiCpQ?|Y zlgH#vagY1{Z79>WN^<4)hRPVES;$zxmLwp{mBet0_k$6&0KNKb=|6`T`BzBNXs2ub zDuhYW~%{!);x0@ueC)u)VB zK=CokZg)%&IO2xY$#AWn3dAY-(PhHrAG`~wmnw-S7MMld8ZrsbHT0Zrn-2Wi<9?YoYb z=U~P$x?2eYvl9WL9lzmXdi3;Z)RWZ4RO(O7% zrMRZQG(mIPk!U_N|H$QFR)t=xAOzmIvqYrUnoj<>e8Bm)UK81)1Xh4EO^zi$B`j^; zrO_>x*rq7W4BcWGU;&ljzoo<^1!tRWt?tHpH(Qg#@9%ZRz#?tygoR3)w`kxKs|Diu?tq2Bv(lu;`0k&!$Db3{~vRj9B5Uzri2rOld75_W*{3SpZ@X|Iz5BA4q*h1*4~6 zh~g3`EFD`$pbE#$D*f`UL|W}o9zmyFNlzw3QfeVPG{f|k<|s~_T7FLW+~o0m+RSCA zDAls>3oR}IfxN4$Kut}}mpwOTVHD3UV~4Vr(5vN)eM4dq4w76fWY84+ssnuyT;D`g zBFLKv1dz*^NYsm#Ja35^1QQ{6J~0Is0tY_-3W!|?Cy?Oaak#O1oRE{haa>klg5xnA zLFqUk3dK(fdm}hQ8XNj6KXDUe*ai1!HoYz@;gIf_4qf%rou_ttJ8kPdS3%^Y>-5;Y zCHmnq;um5jCY15xW~+8Z5!0Zv#3^Kl=!YD)RgdJ+bXL`?lOiUS@-#QY*>y-xYIjDs z4dA<3F{SSIn;uaQB`#8(#r|A@rt=kWNC&<{^sD!MXl>KQ*n50SL%%~!niSJo%{ewr zB%d>j7g<~$E;e4R#Z(8}`?w#>k<*HMb}P~QhxNozTK(xyVB6R-BjU!*bU^iPBxjcz zGxdmZEiio~PNe399K*zw&Q2khiRD^7%!}6BS)ul{cpRKF1_Pr%??9U+0$VW5SVd(T zF`CgsO|IS5+&L9Dzr~NH@^4&KUH9mFWu`8jujV*PG26( zM*ncOd{RE?yiwPC}g@R>T`j!+QU#nHGASV4M6AG)n zJ^`7^_Fv`~Q`#2HIGerKxhaL23q&ThXPxc*4z>O)nl9F@hLso%-Bb8}%hg~#ScnBK zCU+z)7_Yae8w`Y=Jd-~$a#ewdWQ4(X8S$tlvE6|f(-R8L3PYI5&jzbYKG7BR3?}8g zhR^^~2or^4<7AHBWY=LC8t|*;kdg&I8KjX`ACV2|AD6^K=!SbsN4>g~{ks6*G0w_* zf0CK0N2at?Ms5b|rAkGV7whMB0J|_UhGMfFnyBdW`dj~6s&Sf`MV^92TV85ormYc^ z>;4LeR&)aF40~{Ma{L^hjX7EAH+>;p*TNKix#0GUF7VF9*rkX3>S{gCqIPz?5JYxP z?|)D4e4qTM=j!AA{V(N_1l-(#+kh`;=|V6bf8NU>|7*d|r&L>_69I^3(=1;rOG`~{ z?fmGIFRy@)`(B3?5?j^B<@V3~BD{`KER3;nxs!N86L8Fod=x^mvm0-Je?4)zdw;SQ zb+^i^bVqYa$^Bq5miHJ?S9SOD(P6O}axbS>Whxmc^+ooi9MzfidRl|`FGUeyWEHo^ z1YX#MzubDJ*`UKc%;8}!vo4anKJar~@KkL_908&yh<=gH8f6_&LQT~|F}RX+t1cOcco{*4-tefLm*|k7N?sd+v9oi9#TAg z{u)r^uJZEU9#P~I&lcLdK}qfOjj_*RvsZ1buOKF6(crz?&k8Mw_g@)a9+jzn`%-K} z7t>7}^NlL%<(Amo{Z&u%2c_a>- z3s|6UHuMUzt<&fF6hm-7>WG5Lt52A8Q1sxBD8}Pq!LgNI>JJKXa!A@pF%4fzE1&N_ z*SF435#dmTa&(V2O>;P#{22JDhu7ESgW(tyc$(Wc%h=wtWS{_4ubDi1WBji3@#m?= zDzb#`(&W*7_M_cwL5>P#t|iz{joMTcnf{o*1&vr8keX*JRPV@g7do0)z{na0tVJ|#~bx6RE>%bT;E zH1Us>_cQ*0IbVPT_Za5Mt0MQz8%k|f8rYaX(gD`8zIlry?M~1rf2qLp8oNflT=`1jhW@D zu4*G(j;Mr3z_JBQd$ZY|CQof6aP4Z>#3nW7JrfK1n4NDR?U?u7asEqRpIHpfQ47w& zShEZ{?}70HiD{rG1!|!tMO*?bL4(f5UN0hwd&-{U&Y7`%-A}t-kxX-6@8I(~euvMw z*dKHOITPLxfqmeFm=}xjkq33_B_8sidU{jyXu&Eqga3ewtNMA2496t;CwQNtjFqAu zW&s5G8)|es_p!rbx5?l6Jm@0QAMv4SgFdm7L&T2jR-pNll!C%EL{zcfX!w=3J zVizxiP`#Y$;{&Y5zk{5}^Qo+6VCU?4($wyPZ;k88hEmM&-;$wkC%4JbfQQo*Q2+7f zkbsB9T;03fS>Fd~DGxz7O6Na?o;+iN-zXKfqL^0%G>`r?934%Yy7A{Bx#Ie2+B7Ds5LnGFu^ zhiygL+}lMwZki){J^-o5} zJ)$Oc>f4=vVimTLN@qJ! z(-wdK3?QzyZhYxFA3hVdp0Cx;+{{-+TT9*Tzwdt<*M*q2foonDf)e?h?i3V+m3y)- z)n)VQU!2-X1PPAP$NW+E2YZ?Lrc@f#z8P^@A8wjC-6C=@x~p>G!li5Rh2Pve{foiCA1d~v z3D9Rc*4c27Ter8c#gb~Qz4|ck)S@<`v$#jbs;YSzM=SolQ$neE^zob9c;kHISb@ow6mQFZ z4gTe_&t8Rn_O7Yzi-h&RL%dadV+obZey1b50WhL*H?h#PvCeA1+@mx2@}|*c{r7zxzCiY+^7P|*RUrJf zs^w73L;7)X{Mn>PTHIA!;N25x_g1ufL*bst^@y8*)>m!u$y8ECY%n@QnQBQ6&Z&9> zf6r=KX&SCdnVJ_DV|8o0P$ybaHHJ@HXruEFC2+*v`xX0+a;p6=&vq=?S(+mn&_xw7k$W{Te<9^8XtW zyt@SuIm1WNWewL`Z)f+Y)7`Q>TOt&k#Nc?{`B|`yHbH;(!InJvu1+Ceh!kS5=`ewo z|ou8wt?Ppjo@ji_8~vAPqiVybZTg?jYp7M}?h-3Ie#R>hQT zTgotv6lFjo?kW!osV$uiVYQqod1e{|F0!N zi1~1Ls!{iTTD?dW8P2#GAicb16(@-e%!4s+u*LsvWccAXGF$8Ncz;>821mGR8=)Gd zs9;6bko^nG@PH;IC0DMmNL?e9{U)O3mJF|$j%(L~-SDGzzE87Mi$cAa5XI3xM-5yv zpzlV@&WzX4+v{+2xHq!4WW9C1IM>b4s!lChTdK6G*YD#b{*V`Ci zqwb3tlM)akyfxOhIGL@`6(-_$b*Hmy3j6)`L+jI$GrAy-2IENm29pEfdf&kAqE}tt zi|7%runzP3H#xpX;|k^(g}z9}X$g6Xn@L`F`mRMWPTz`%4`R`t$XZRZONl9IN7nnB zU{1GsJT}x5yu8l|e)qyK{oZF!O4aMpH*>j#Hd~@0_0AV1New2G38I6WG`o|)d@yDP zm{GgLfJ;LjYz4V&@LJ)P_#5cYU-y<8y7zl%yeJAE2%_NJiOuuXPyz_Q@sSiu4{R3b z!l%g4fgqF5yo=No$H6%_Z?b`&Fx}1UFCC8-kRYM0s6H92i_{Jx^U+1&wJDeB(z<7x z;ZFDOpY^Kx(9ATD$1S@>0z)jPfw153u{%l>HH71)4ALI!@CE7yG`+a)cq{}XTjgSa z)>M3U#1SU`oKk)_jHr~D>|D2-C-zPVIQU~^+PKoyD4pnJvoB|TbBMQhr)#WN>4+zc znHBG};|iNsS`06)?TU5uFsb-L5qi1*zHhzymX+fzylHH-H(S!zd6ApU`8?_6i#h(M z4Q7F{+QGL(G=?KkF(OKFc3&EJ(JhbLlqd57M+LEX7>fw^IOB%WmA(E^Vg`Kr0D4Z@arKHWV5Y#@mG4=WLcG-nQxd04 z=N4Y~vE2u{dySrutZhBqgLr0<=5nX4WU9Q*31(%4h70OupJJtKxI+H)z~9 z6JFb^N?AGC(J&u9i~I%s6TZFhk-_e~+Y_!DyyDZEvp$FSk!jHMv(WqLweJR1d1lk2 zSId(c?7Vsx-?ws1X0Km(BE4E4f6KoR_*PZb{5Wu#YCb$Gp9CD10B^?D?slk^RC;+o z$Pw;MB5kKMn8reQh>CUj6f`=^Xvwz8hMwbWR#6$%PDGwJ>{|lfgu|!GefT<3RbIzb zyFbDH{_WmfM@;Je|9$xq#nM=8#Mi~bF+ zH7oMD1NR@+Zu0#3nvFQk&`HE86AD@T!mNTAhs#?v;bE^Wgn`dqLTC{xG(c6yeTkm6 zvQlS_c3@yTcTVz-){J*QHrc`;DCjz6uU17SP-D|$_vh<8l!OVw;53)k;YzK!=&j-r z`}Z*F;%#e=J@0^7S)nq7|H!!o@@csge=h?4X#q3eH&D4f;D^MlPl821w_9ngJv7bc zcK&qeH_cqR8E1w&|2Pr41P(Qt<~hr|qEqx1&4f&9R31J?X#>G<@-x^*ZgNsdMv_tZ zfrp25pap~OFAG+Wszc*?mIeink=@z;#G@fuRWEKfacbU*g)n|eqp~ET|8%79aE~di zJ#EX3`<(9tvHHmJ3vaIJW~hZFB~4tl@4L0=C@~SCG6lZNKTNg=f85JUAeRwtS`mu% zcQgy-3--}}9K_o+se9OAy(n=p_~lfC$|nV-4^Dup+T3VIF#Nj!c~D`TCz#Me=cGWI zU7Youl|PIp;^CZ+)YyMSX~7V1wO>_-Bvrdl&l!-V*WP?D@MlE&ZWg6b-8h=#Yt9D_ zUzBF1Le9Pdv^i{K$v}*d(i=___+0Krp><7Lt3(2Cxv?_*LyS zMh0J9etwIsv<>&;(=Cq=QMYfD)$KhVB>N>pWT2QSyk=jt3f|D_W>O=R@j&ONBh}{i zM@wzvkm~?-wm1|y{X>h`&TCA9>u%aWq{Yh3gHl{Lp*5lI9ju%0G(yF+v`&%9g+*47 z;=FwJ0|au>Wz_w|DUqpkxE`>0Q;@3)$x2N7rO2>A2#GJEebOi;seQmhIqJ0PvbVC5 z8R?)-_>SVJ7COl5QF^G?i;W5PhU?a>lP4=5##kK5WxSPCa&@3lRmxr5Gk8Zw<_GL4 zB1%=U-A1xw!%f+qkm_bj&yKq$g`t>3g|u)RpKoZNRbGuLSXPRE*l9KPr@vIseDk7J za~kS1d=s0Z4bGdk@xcX-6_hoC?n({M`lZh~d;D?sA!N3D&czE=j4{XbA4(unnH_r&nc6jGM~Z$xz(vKQVc3 zq`yf^sQn2Q;*IVZYhQR5eZgnF*!Su2(=`;{ah8%_Gdtu_xG&`+bRaEJA6R29rhBFF zNi(Z*COa9(T$MhvL;|yi*~S6@Lk`3i$X9wT4k69T0iy8anza-bpbL@<#sH|6Iy9tI(m{6J2iQadH&+rVT)DOtc6+i276xE07`*9V&A9+2 z=qoCQS;NYm`gyee#9PC#AAIgox)!Dc$n&S{8Mgqk4tl&)w24D4To9+%G85(Co^3vB z!+jN5sOdqCxvUh*?0ex-FtU)H+;Imi!%~7BBbTks}LkyQVn1zr^uc2@xD`P>g2s@`*Qw?Z?`u zm;QY%TIzYojd%{bfJq>#00lq?QwKfdCRh_>m>xG^tQJxG@}2I1XAcE5DGpf zDI|GnW~v#S0{H})7 zAtFN8<#fn9aK9r8v6?Ha#0oE=8HMT{lDJmgh*xrj)_Op&N=Al z+^~BuKWzB!>M6c)RH?99VvekIk}fhWw6`6UmFtn?L<9kJ04C*)N3(o}9yyjXITvr6 z`=b#Iw^MtvC#U_OWLW|Hpt!whojw{E&_ZQ2+Ij>(6W1Tnvn= z*1yraAqGw7-ld%YRNNNs3xR+BOb7rZ=?&kA7NHyo7X!lZ&Bcu8&Ha(vaFZ7w&*9>m zMcK8IPc{WFa*p7YnzsE*`U7KYMQP3#IU1S@F35nGzvNJ)^Z+*=Il1~M2xF$o`X%RPx|zzvkg*o9mS%WwxK1qTk^@AI$KPUfVz3!ig9WGK zlKI#FuDwz|Q$I1boyiGHvrfWEk0jVk4w_LA1)U$Z^7`r^r zC1R?)C}5^AL*URt7xmuv_2i?5)y`~3=RQ9E)6_>2RGz~OrZn|Jzg+U=#U~)p_;Zr$ z`$id&Qx&%QfGV@!qJ9-YIoOYC;wy`f6g+-AoPM5rc4rPtGT{>j78>W8k$>t9RS4MB zSVeh}>cIFqO(qhfWD7{RCYXziX&Bg%gb$3OmOn19oL-E-l5pc3Y8~72cU8Zr6aG3g zDHTmy@uGRUDZP0-D(v$+46r9$MjH$IB=`Zli~}}+l4C!2L1L+))L~;NkyHpJs@bn> z2Tws=iRd~b9N3F^lFeg8q3@uMe3SAHkL$*gh}%sq7N5r-{O(;*i)^(4+5q=u&mH8T zYp_1uRVHMPlcl$W!mY}IN#;DQL&VumK=m#6!`hD~WlfO!+bpM|AhHZ^l#!Z}JFLuG zUP*kEUAFS4`Ll&OeVSPRD01Db6MTI9T}&xC?Y-{zN%FWD9`wPGK$;)WD(4&+psa-yC z$S;)aD_S2_lfs|uEB9bOk&woF9eE+Q;Rh%cJ5#S0>KM-Z3x%)PlN)&F`Q&VzQrEl_ z+R*%?UGh}Zk%!9-USBWGMKGAwHF$;|aTVKRrRv5fhP{oA-OP^`7`1Y1M@&}v$+ZMq z%Pw(SE5kPMvPE0F@8Vqyj*@a?$Khj43fUAkhKBV0t746uFL!r$b7@j5Zup?5B7bUf z|K&McjG+eGm9N5j<~xp$k!?^5^73L4dSufAX~|$gA#5FebzG?YEX6#SZyYUI!Uq*_56yOR=SdDJ4(F?W3==#m>gY zCGeOovhSD59?i(*bd_W`~t(Di3YUVM-7@wV-Urx+kk%XTe$YF ziq9xWPW=eU^RxhYI#&zX+St96!Ac6;cFrBss80f93|1Y+_q5J9Z+12>;y=qe8q$0i z!+A?gefTbP@D=ukSG-C7)WH(_8;)5(DPRu#6aMc7JRU4Vz?(7l z0zFA+h;S3C?kiWBCVvPL7iA)Vy*;X zZzAqMf9zz*No*=L2Xgl6?d{W_haWzbI7DkLo-ei!>Dfb_E=<9xvXvSv_O6rR4=gS` zDACnCEFvSB<+=QUk7jjYjh!m(iVz}ZzyCL~MGBA2u_(V&O{5~KirQvk`{0cB(_Pe_o7oDMxp8qfJ2VIxC zZH@N)48_^QkU?VMt_eWtFm(}!brY!QpxuxvMkl|tubn~cArEWFxo?Hr@X za424CdZUetm??qz?RxWYFtW=QqR9J8B+ehzkQdCX^w(=TG9#L(9MJh)G>O?rfs$Ah z&4Gdxb%%M2#DjR2@}ogazPg6qMqpA^iZI2nbZ|oY#Rq6K=BX_zi{%+L7PBa@uMP7U zfg@-Ag7AUl*4dz=z;`~3!>oxKJFKh48_!QQy-f?m(FY`!8RZ|}e?RP8KpMG>%}PQ{ z5rqG!rU1;tj>mxE4B=-uL^L7~k)g+U|DfXY{{Q7*2g9Kv+kjgSyLOE{B(IdZ%xy9Np*>ssou{-l{39(kQ?fI+p5Ms6gG& z_WtJ1z>rYDFBK(t3T@Kc{H|TU|Dj*_{5jCuRRVM@vaKltO#*K3nDS1mmYHlUeAlR) zsZ3V10V{j7K(f=v1d=q~ax|kIQ@NQmVIvJ*bB-VA!oQP-dPc}`LJDo{5HiVMZEamB z$)}0V^M9zD(F}WgH0C|GZHrV&eTZmif25rpqYtWXGt9xvu@lW_&^*N*?|%$^CiP>f z&G&u4-wum6jPSrG+ckx#fJ7sWk>jYMCkm%7F%IL#8tw8K*_$f0ZBW+4j01eOa4L54 zbRv$e^Li#bp{Nd)G4QU$iK32L>!*l9sBuJ&QqrRC6K}ncVd}fc?!Cy(UQms@gnxg} zRBg3Nj=EH(o+Ur%tGCZBr{WOC+mIL*KfV11bseuZ;9zU;_MM5MP@=qnGBo~kif^8) ziRYwcTGrMUL-oDby>kW+BEvU&e|w2zn@Nl8n0mw zO6SeJi`T_>O(D-gOQN65`&Xh=Cqcl{jk<$S-?h2D8K0QgMI`wfZC+fi0MdZ4*vmim zgHLSwy!fy0Y@Q$Y@9^F3Z%&QSu(XiGhNJ(?{+)ZytQDnkr`tB6VDZf&Aw00gkiTVu zdD6XmTRRgtwGRwpzp%sI+~w?TDEMMlFxN5K_~#x-qhI@eh|bV#(^#%8>P(s5z@6$l zw`z)Xnz~ZYe$pUhIVQo^%Q)x4f~%9Y>nnbq<5EM<(k{mPjLOn1*th>?T~obH=F{+ycMIu<6^OAcev;ePbF_K% z6loosudtfKX(}CU=Qotebh_;&d$>)DcJ;CwdkK@^_;n7v){FiA$Bt`!4nrS?kS`n@ zQRUexiNh=?lS+to9oOcs^jluGy6nU7kY6V3Y+9QNLKv9PLFagK?t0jK&*2U27ecL< z-}zgbgub^)h4jd}d5jziK_V*L0lAM{Xri;T5XKfau^A>KdURS<8G3}!SZbNbW#2Rm zBCgH7lJE0UV3L)w4tAO}5see#jDA1rtQ;-z{F=HQJj9jUphMP^!;D^p*$58gPG9FhSro+APB0cge!U)$ z2DV(P6U9pu(ThZy$qp8`CHdZo{7(y@vh%^i3+R^v8aYwrU)#StrGP5p@afzC)T-|q zyhZyuT3+rRv?A{Bc(VLS&%6e7O5o?*#Pr*D?bq=&xpv?c)4bbPrgQ)(55*!X z21R_TN+&a19bmASCx6gM(8O;^5^S%=#}bzirk zQI0v#hLB`miO)hM0Vu&75X48uNcI#GY$-lNUOdwC=i504$o!q zqVpviCZs=JcLyk*8R8qQrRogEQZLnF>QZYzny}^3&k?|Ks5>mhZZKJ6d9+}q04<<&1L@CqTzET_8Z<6T_Gsfa*jI={Za`LVZc zJx#=gsPEj$U_>Q&kpU>rWe_XdPUPy>vcMUyxGha&=qnAEv9ThK(QK|%nuT#HyYShmtfdk?aXqC8ynqAiw&SD;z_L}l5Fb&-f@ebvV z)uR50$x^f^ik^@L?#dt7@`*&t=28;FO0X;DB0{b0?^&soKCx>m`$F$+@2|!>;y+mB$ zdFV)Hm^Q;KGWSeeVB>Bh*Op1=W!=?}1=3!KS?q*+%DkKvW&^Aad1_8+G*y@-3hspz z)$?7-CC6WaJ~aaxlX!Jw*|McF0j4*>elv32AC&M*AkvLsJkWTlQAjA9nt38xs^g#3 zSsZ+dVzk_s_}uA}I$j|xEHk|6idG^7lwbEHJ}E*u=nOwNQZFLXNx93FpfXIMMo0=G z9O;Bl1WVGpyp4#IassStESVjJD_j~cIbWki_-h*08;cseelO;-O<`$VvMba*`EUOuLZE`Kq2 z#fdKOB}&y!_Z$3ZaS~LKE+E#*oO+H;v%33L5=lv}oG?_+I-JHwFC>VQWjG2Yfrs59 zfF{k0hk-_M-N$Mb1T?thB8IGA$=5b7MxdPf7_1q|!)*qA4{zE+segpeRPa8JSHsnL z{aJaOY(tQC=GUoR2_D8;!7rEADUs6%+t=wz5sGyuIdrYn!)0MEihQ>DLNiyw6NKGu zlP$9wVs2@uW|*m9Pj{=a)1E<$)bIPcfoa=cJ3sl7E&4O;?3}xG{`8y}gNphf;TC?s zw~oE;UVjdxyd&z5%kG=hTwHjf_h;(aV$x5qMeTpmU6%t@mn!d|*7u$Wx{#oVUgf^8 z*q_SfFON5E4lVaMd#zuHlIo_AY%&YR#b6MA5g)s3plOlzT=%!KDID@F*U8Oy3F%?Q zqIBiC%Y(HOJl3FQR&Mzl7*dFtddJe^joF{+31e=>c(7LFJ9TXTv>UFT*C3t#HVREL zNeyjk5kgF{a(%J_QX5y_C?7Y`AS`2sSD@Fr_m}<3osPX zJf#6wAPtCNqdbFmU-9x=dTd6pY*dyl{HcX(ByDv6$gPfM)k=K0wu!@x5*aO96}u~w z`OWVcQzYOZ#0)PP@Evx3|QaqHfC%BgMvGX5X5=R?%e@#03c=O_I$CV~DCi~rP z>&W#dxCggAtFy-jl#Q)-dl?>VwIZGfBmKfr8@$4rySv|?tg^6MP_le(w!m!WUajG<-ERQy`Z!WKhmTDr^V}+1)Q`Q|~+69W3%d^GA>B>C21nrF~(-MWJZt zl<$rx`A%w$A|fPA*9f+b4^ngt4GEsmHt+&|4A1bbkc&$H+E&0_yx*%MT6MR8+K9S-1kzIap2u~tRuZQc+d`}eWGVaR*qJvx^_u9D0j6ewR z5dmoa_oulMU$GRD?;*RM4_f7(3%8xO@y8p9^3t}VrwDg^e9yf*NBgExY1P_H5E7Kh z!($%rYc(&a5}ziWwrGAd+8e&Lo_>+*bprbTx03kOJnzFAi4+~L#6qYcR#!w0eQCkN zTRJ#|8U8slG2+wAZXx~Re=K@b@!{J&FJ;O#D~^7LQck1#K|jKj+hq8k>X%6Zc;YIh zQ0E1X*V0n?OJ~9L^~3K^1tLz4PmgYhsz>1xwFfM_{8EE~Sx%We*;oo-LI7de^ROLT zMEZIfXzgB;&@Ub``T;U6>^~@bzH2MhcMpj>YjY2SU1fL|cQ$vNoJa`M^Y78nu|6f4 zdOmIc3lyi_X^gwmiv@e+Z!UMsfdGL;oz%@N3YXN6GU>xlG=vV0KxQEtqWpMa0Aq^9QU9nhko2|lw~ILoNy`P< z#q$5drv{MuSKy6?uw7CWfHMl#_PW-gI<9vSN?PaQj~beQQZ$f^el7=iAVdUsHrcZmQo6xz6yl#m*e{?+Z0T+H9W2M3>}_|-VM^{g^#X)KjTRtbtZ7Vu;Ch{+5v6v8P*adk`S)G12^u z8ghHserW)lid7~+mGLz~Y%~FB;O0yth;dr>)|UfqmM4o&|!$tgKk<+RrtG?vB1WuR{K}`|)IVqk8lW3?0h}YEt(8d67S3 z_4f?CYkawIKos4|NzEcZ++dBVkq*K}p+JM10%%`=_7ntHVs`+3R2H zTer@b-K)3=4VAz{Jq1y*PoHXG2f4zYE`P%t@aKTNYdsVJqA7COFdYgaK77MxZNRTvZx!Jcj`LOJ0aQuk|_2(%4E3~)JHCIQ@Pct5`dgAmH+{RK;Shk0L*A`z+!EW)!mR=N#UVq{~3GqKa>Y5ucq!1O|y zojUI#c9UVLH69!A%B8fr?|tB(&Lg#7R|}lEQ%0SX#uc)asqZm^i?B~S<040}x_^NF zG!CK!iGNRzRC27xlJiXP8UNYF-yl1OS)Z3D_OBuIa;j$(Z?SOXMt>!IhQ-~ zt#X^!ZTPNe4bt`TDO{_{P*GFEjR{|pRW@&AsEUNxAO0UQJb(^Pb;HTkrC!Da@*#5<4M_`UmE9ijwg&^VohXa1`B?acn z^2p?JT+$Z!toiNp>?Uf}SocK}Rkyq>J__@zU`2n5v|BXTgoL452JT3y`Yzi4e?~h4 zE%7y=djNz}d~^nqk@Og@Hf3j&K9^m$U%gA(@OwC2ErO@S{X^Q1&Uv}b9Tb@6|qBY5@hS;J!{w>5!GEJZXXxxgfeTj;cRFnMBs-mPFJ_R?piiVAi2)xgM{ zenS=It}aS_I9>IBRv_c2|9ZuJlRV4!;X2{PwbnDuv z;9-x3mVWSrvLMDpv66bD$k`9cfsv8k(=pLlP$Gl>-(Qpup`rMnYZv|O(e(&eXMgi%4(U8HPJ#~Cp^BW<$SrkD7@1z8Gk=yd{r~YL5xEAtX=mUnA(U2E1b`Ec(#yw=YmVQrbnsx#LcJZJdK?vFr=0|LkVJ@f@G)4p8$!WDxmZAw9J6-;i>u)Qr$Wl zHtuoW)3>&$6gqz3&Fxtxr0nrKwEeGLp*xS{=;%87{q1iU`ClkL$p*X>Fi4(JIV+6c zaicV#Y@VUaSD2q7bQ&;WQE3akV!uat?$p=DSbezO|Jq1NyK6e>@7?)=V}x@#UZn8G zbRh1B2%ufcaQDSMs3ImaBLL2p-{ew10;SwOl@)#6Cd|$9==<3avDMhgf zBBf!s*3XsGrLk2}sz2`dBU5w%6655lq*2)l8}VLG;u}gHn>5&6Rdmh$!KqrxF{94X ztq`tfwi>Adgr89ZvhnT0P=oA0I|~d{A1%4Ay#JBcbuXcx83qFC948RCLX-9C>tFJK zJ5?uwjudHctLjIc ziH#WW67Ca@OzO=gmYx;ZR)QL+ik*lLayiW+0HEUV1O|stAe!ps5~U3qkjm!m9T5O& z(d_s2$GxTch+lUmH*)Vmee;T+=p|*$P)B`9u1^b#&6+Rv-0zTW!rtEA`U73kmqr5& zi2hVhL1TZueruu8KJMV?s)3&F{OTy76}aQS=p=v5L<&&VzQ=ppenT-y`c}ZGWaV_R zov&#N>Fl=QwUIT2C3T^(5{)PpNf1ULXuUNAwqn5OZ_m z^Le?x+0Mi@j z!!(kicfPwicDr11#kZ{3muToDe=!GO<{p$dsNHNe+)|4(`l`OBueAiYJXRyw^}kBa zD@!>V+*?fP!pGL<=bnX15-xORoo%KZQS z{h;9B_dsyjTIBz@bZmu(2?WFfKxOunwPxHAOFwHgNN)cu3{`P2M}0;V!xNHcZLBQZ z59G49L_TkJuv)P(Mq8i^B_ca|t+~zQ{D#A_0Dyqgt!ts1a-&2of3s+~&{6LzNK^~t zlrX^G_Z|pA8jwQv7f<+Int{1)CmFZ(Z>y5be~7%m4H-R$FUnRW5Jyle9A7k6QT(3k zG2A2Y5eq}sfTG&=;d~PDk54@|00o}gjL@<{fVm!6c_p~PFnOgY==SkqhMtr!{K-eS z#sqo3!B&@16#%V3yP+x9C+4Xk45p~6LxD(xS+#=dIs(+*uXFegduIu{+6C`w zxEJFJv6KP(KnZ|wqz~|Bd&<3@eKF*bur|$D#13Im^iKVG{PXA8fjvIoettNcg7Z}I z9ce>q{=Oo+Qp67&bvbnMk;atYK1=V!AK}57|9ABO*$?3JU8-fX^F{```kQxy*#PGI z2#-Rh1j>GEnLtyJ0U;UO4W;zi+I>|Im_L{~O06f4Z4G_ekUyBOM{ivADCbxl8`3mb zYuB0LdeC#N7{4UjmovM4>^^LKAqnn$FSqrEC>j_;LB!^(k#!Mvz&q)@NAXTE&z?o( zzJokn*x$Ok-})1_fzuo8H)kW`;UO?SKEBh_4A^vq&oG4oHpZSlZu!LmXe2;gh z$u~pOFn)f1{0L7NddvD{?$OM!F3v&&y0MOv*o5_A)j^@p%H`&l-H%(h^joLvaGje8 zs6hVs^+(K)?!3z%;RAsFv-zt3OHpC4iBqZwcCKXh=IM_jF6_P^$>2T>-UKO^ePq|J zg-SH#&XlDsyDZqU2>YS29s(&_BiZsqt;umXCQBS-Cn6^7mnKwBV*2fEw52Ev!AvSqG<%UB_v=<-QC??PML;XQTksk zfN~|?CO&|>4GRC_ZYu=;#i|wY<;Gm90&x>5UfXD39I7hX43_1%nN(A4a}C!nGUp9R z5O7$P8cydbncrBF(XE8N=#=z_Y8H(m@AVDn&RA%Yh?Y(+`2?aZ6$_sxC{;x1Nb1Mpu5nUTks zm9M%06_GlN$K^rANe~_$UX9Z>c%8Leoh^#AY^XoQ{UgNCIYZhk^7Z`c9l&o;)tIZ1 zA}{cc(|l&B(#v*i^SrcMFBEj%nMAuhxizv!!KucI0$|xQDv(*G@xyHex+DMq?bZIR z7Q~~vzG7__xZOTaD>T7L&j?{iT_UbtvTK3y+8|#RjE(OVCP0?*#8#l;nG9hq9~B#; z*S&STi+26lArqJ=bQSad{=sH4o%3mFL@New{r?EdP%b#E^=W{>(ZSXbiT+s1DxJ0Q z=-(deffAyV*nfN+e6;f6{d;O$T-=9^rODwF;6V1Mp_SbI zRn!C!`478p<~gn8z77ti~N z@<9#uQrg;3zM`T+duEG@9RwFq@(-Qbqd8Acu2Oue zJrIc>#H~RyeAS&Hl*lqxJ=C}hs%qA0Hl@_Cr=kJ8?2SLnQZddbf;Ds#KFh74{1~$n z6MJ^B+#Y#%eQLd^^L^MP!HQVfTownUq@seH?ENbg#|UvxQVg5!r@uVAQciPXBE2=Y zj$_X8YR2_In*$OMc#bgGgTd{6+U27g$NdI&F=suIrI+`XnIM=$wl*%EXd(j96w|c;I*=A%{9msbY)Lr z(|ryzQ6N@!OosrkWau-vgyY6a4?!!7AKyY_o$}>YY#Dw1r*tT+;p|;}B#js`WPYP$ zrO?O6hkd8_n>iO~N}6a6RYpt~WgvW{*>Us5QqF*UJU3pd&6Y$|X)MbqBiO-r?WR;J zg2#UO6@QD}lE5}@bkkeO!}EhBU60s?FVfp|?h2Z343y(a-*>gSpF->6i|tw%y*SB_ z&cs~gIol!$B)6CGAP55HUX0M3Na<{Qm02mUWx{bUbGNk3U}#W~+`^XA1|%l?Lv zh&zRYT`T;Zsv=OK(BA(Xj)soAv{!>V2$(1~ILn}!qpN`3Rx*cjSp$uB{4|c7ynF(? z;?KEBuj>=bCB4W!U{7%rg~EWH{46eJ5SciNbue3lP@z+wQ0oTyXs9=evv3nIIhw(Z zp2(n-00~FuKf#0qsE*$j*ZQBe@ZBf@V+a(qNX2` zrb!L0NT~R!Zeg!@t*3|M+PRa(@p-a!&svhiN7|gpm;I$xTMkKKy{zUovQYg9+5LK1 zjfuesA~=~YXPhSI-4sZVC@n35lc5Y2Rw6{OP;OteNePgM zfi4352SJ#6i82Zn7O&0icTmCc$S2u&hyquc1!0uG{UF*vlLOK+5?a1fXE>~v*<-j4 zj0&4Kq|{VmWHkmBJR|KFaiuOe`T6s!rH{D4>8_LozQGSnI7dFS{XN0|1K2MRp$StV zF)=afymwr#aFJutYF25Jn`B>NNg+^iW0p6r4X(kVaa^)DJyKq`K_NgXle(^B1CA7JTq5GZHt+h}$++=?|A< z-(km6ozoP=GaQA}I|x>#Cgf4iXn(Du=(W0F0DfGl@*CUh?-O9Da)v%jA~Z}hEbz@&WTMI+d^~<59+i_ngbpRzP!vHNfs>_lCax!T9b? zF`qOyqH>g7=;wtQHODD~Oy`J;;sQu&TU=*ZrBdIT5_3b4-_ZFV8Mk0BcXX}H?Dg;{ zzf4-ObO=qE6N3f^mIZh}iq7^@U0Aj~>#-USGiMZK3(bGx5L3#ieM7GOPap)7@_Zw)v^;=j%o?r~=G=*QW6;BpL@1l?d(eYr$a&$S0ZrPtTKJi8EhrZz3q)NyJZQb&q@(cNhTLk;-<@lTU&sEcRFm(8$ z#6O5(4GGfvlK=8;uX(gmU`V8%0q>8|TQzJW+VxlF>JzFm4esGRAnBMFWk zEY-8LTevsgHC*;+0GwY!6j=uIPs;)Hn-YVJ{a(M;G#r$`B9N%2$UJJCBUESo@9RGE zulck-1mJ243zt|R7(-;ld`$G>$cGwmj$hg;57N=F114SEdPDic3xK5z)WPXZe*&)j zbQ6P|)qF44!0518bN=BwuBFPF{?K~F+B!AwYeSyzH6c(mFX?IZxTYx_MeuJybj>yZAeZu7taK*`1 zH`yYFYp0!w_f1k?G9h_l!zb3k$@GX%T2)n}Ad058)zhjcHL-D&|SfORAs^kDO#eJlZ2yPt* z+d}z?>@(?)0in$|+Q$-enHvEDZ$QgauARU#3T4r^KRrA1L9sb|InI$Q>2f2SMZ_anuHgkPt9`3Hi>mS2{(BQ3D5_euF+~gr@+|)-7D1#)Lf7e0KM{B6rp!1)n zQXVQZlr?*R&{G*H27x3wC51#@Z{kYMKNNpEIQyYhgI_r_b9;Fh3mAztM|XO)eRcNr zw_N(#}VnI&?AZi!XAn;Z<#qU8bCBX=MCxKI$U^ zB4|IEXc9n%FyWm2x;88FdY2(-hp(GXgFH(+k_S7?)1C<}Xhzn2T*;JS37iVWv_c!Z zLsGN-scGx?Rr57`lwUDU-l(lo#@qJ^8M;Ysg6}4*XYJPbI$R_N;Pp`axg0}i^FFaz zmW!T_DkV|!>vHcFJ{Nv|2p^2|@S6j+Z7ThA?^;l#WMt&mulN!(6khO8Y_ya!Y$B2vnoCAw<%6{T?GD0G{6n@kHuZ=i`GUFdP{dA0NN@PK!A2 z%0)C0qJ=8FFbM3DJM~AG-|wynI2fIMz`}_Fd2ia#2_>%rM%>c}54qrhkEY`8{4iHb zpE$WV4R4RUd&?NSR(^O@>xxDD(e9Y^2=fJeT%iC+c)k6lF3d?7;8d&r*+Bimd z-^Y7iokqJ6I&KxHo1Y!dFU4j%$#VDpC#Tb?axDek$NT$xwpy((r!Uzw*)r%zogUVh zkIv3RLmL}+=*~Z0S{*U#y_gccUeKv?7ozP#X`+Vry*qxQX(G?goR-|Gu3S3p;@-3V67ONsh!^|U9@~XeY#~B*jM|F zD^PZCMX)0(&);SFKP^k3TQGh^ufwaE&If2c(kS?G@ ze7sjT#t9l>tIo2VSg5t|C4#%fmXni0DTt;Ugsw?#IBuK!SFOjEt{Ot$q4|@agv_%IlbR6vL$}}xByYeQCN*Q$V5Snj&hnvn$ zzgANn4WO{RTEyd1q5IGm{%3MCpa2O=2dU@=$|5(kM7E_$^4kq4*w`5oNQ zk$NbMLfFGk2VvZ8C}9Z1j-#`7dHU&Sd_GvN^(9tr0exI{0@O-(lD2)9+HmkB3pBzi zcP9euy&Q_oTrCpdHUeCu1&?3*M&6HKK6;_K%LYL5VpSfHNxhQ;-C6?M8~8!AqI zfP~#91jO#Ci4Yp7lrpPUWfV}bDJ)#A;@oVdJ&Vp?dv+43esakisUZ7&SXgVCLtI`j zDe331vXV^mdoFUgcu=~m=S(0+B4p~wTsq3HoK{R0X5hKcz5T7(fTEYH%mXgu{&n;- ziy?QjcaeT~-FW9I6mN{BD$^keDhroR8%Z&6|Kv8PrcY}wCv5V4un3>pMKTJbu+%%i zuab@!=q&NZLFNy5eGId#xi=@KC`F{le=Z%dl1A)(@npIjdSSiP=zjM*hx<3F9?2Ae zeWUhs0R8~wf%q?g@e>=%5suHufRUB}`|-a~=^Pq7ni_$wltkX{xEb?1siT58 zXph+pX)8g)$cSnyOiWI0K$33is-g23iES-XeHAyksf#`a@lVKkJzM!l!4s{k&>&ht zT8#COj&hw3(EW#1vu0(ORahO3OjkWl-uKe8))cO?=AQV*Kz+~nO*7$sUXGwE?|N+P z#+!BBd-+}E#RdZ2$3>LKp?d#e#U%2ZhQF9(UJzcSSDxDIE5Uz4>~xi3EMS}Sbj*XY zEFG7SMpL-pEB*Wi(#9{m4J_pM&o}#GFq$q-QwI#WC3XZfF4ng%BEGnZEFD~;vVU-heK-c~X<023Ugmwpas7~%Mu@MHp667JwoMI9t z2;)@Ahc`jKO&b5ni1pSp@xOD}%jZy?_N6ZlBs0r^Ax=rJQBy1d zoABUWFCP6*6&Pu&-?O^i+!PLX4Z@mzzt|(VArpw#_jbaH6Y<{wYuTka0&d-tXJeD+ zW#z8GdsF^I$WjoPTs92~3*@q{!GsgYWN860yXUiP53u))QAT$w&3ALhB9|qDT!^$?`!g%}8#rtA97sl6^r%!RH*;FNTtC>l|S_3a;;jQyHH!+%2`( zh(c+aFBqI)m!&0?IF!z_^clk?2uGC+4wf!YUIG**p@4!-uJN?dZYcujRVuUq+CK6p zzASD=vuI|WTLlR&c?MK;>b5(yyqkXCS^7{$AYb`;QXK)RktcNqn9ZtPND+LF3jz+nM>s^p zYbZ|f0W>i``h^q|HF1&WCq?s!qXLT;AJ!^L5 zdCn*!5}jAl@{PBYPX3(2ejC!-FMV#0GUf$W+HuFxYF6_M$h1F1;U`O_-YWi#Y>Y^R zEMrc8AAh}%+^@BA?EJ+w019pZys?VG3>v0t!FEH%E{DtdR-qnONAH;<vnaM-^b{?@G6k; z?~$jfwwdEp%;Ljov|pKyo&Y|5iRxQSnv`Fx<571tGMqY{hN+^`V3Cbwo%zTQpuzFO zowenq88Zk!yY!DN@+-?=mPXq9d1&h&RaOGpI8UH~{mC05Ve}UlXoNmQ-X8h;V;IhRuH_T; z3@F;nC)(x%0QbgJ?LG(?eYX|wN`of6xK8PuO(7KZb?S?th=)t|H7cs+5-IKxz!35K3W$qaPwFh;aU2OjJYlVM{*Yr?uIyB z)VBjdm`HIW)(C)zZ$;uao*>F!0DiA(HVqY_UG=IWVAo^vX6kf5Ek2D_8-NF5hP?TF z6l&R#wz49?9y^XeWu&D1YB`)5luV7H!z=FK`}`4*t3A%G!W2ko66GCxi1}~U>2Lj$C>5#!ItcIgPz?z0g(B<=6pTPura7eh1_M8W% zN`8H>F%ev&P%D`v$|_<7|dEah}SAb`AuWb z&a^N^H0%YybCVXjZKi5)aajFW6ElBqRXJatp_~};=~G-{x_{Q^7eIwf!>DlgJ)uLE z$9qIV*=73vu&&O3&*smc#m5M;8xdseN3oT};;dW4XKrDSsU649gbp50kB*W$HvX7? z4K!fXH9fU-+Bh#iV%K@WpO8f6CI>s%P43(LL$yAsh6t%dk}n98^$@vb=^ewFf#GJ=aY&Sug6yW5oS9caw}v5;D*` zLkG=!od<4IZ`z- z2+8<4kzY&#Gie0b@ar`XwUkO!ohva1J|xX2B$)4Xlh|D^!d@>0|fn*BV`p%M4E#v zb>&Y+9cy!bG8Vfx`EtAH(Q)QBAPA7o-L06fteUBM+P-W_K!QD%Jt`LJqwF8EbjY`T1-{;#hb%duHns`W+0TsJxg-1zPpwPPL}#(wWIXQ z(w{`Zl)le<1B@}%ABM6)>6c(jNFwG{_>LnE`OKl;z zA&C=uQiWApQf6WDn-Z*%u_+!jHL?3uOU2F24Mdk>FxKXcAR-+pdUc3gmB_w*ANmN$ z0hwCmiC4Zry81W8LoVUClL1mn+&k`^{-pJ?tN@^mUfd07s7=yY2M@+Z5mQDc+Fz=l z1U?vq_n_Ec>qdUDjs`?=4Nl7G3GY4JfeSX04dNbE9IDHrdEMu@{%9Mq7|tbBJPOsx z03~yvtwm~`S-Jqd`*Camcq1<_agm%D!U3+bjfgnXL^FmmkLwy!(Jw*IlJicmP-TCn z=;PcC-;cht5|X{;7MorYEN!LM#BhF2Ubd;S`XLu3C`vcTm>jtjru%&;Y*vQnSO#E^ zixiwjz5W2L{F!0CW`Igfj|Xe<+MZ90?g7YZQe#id7D!MU>T?!EYCtk%qPV2Ny+jU9U5n`ueh}R_^Joq(HGX5pc3$i3GB2-4 zBUjlfJmH+3V$Kv*nSB3@zjdZG7kmWO$1kpkUJ=O>H5NBirAjK&raCl#u8uST`WfGO zT3VY^ujY+wAq@^!CG8NVw=B%6Ys?$57JOiASl(TFysiAQ^gK=deZ2keOsh}SE7w3f zq$EUMWBui2tjG-tz%^-TRM93gD}%{v$Zhl(HGHgvITBbpmpC~7urYBubt{t}@O)Ty_S%{ubW> z7;XO2>g)^b%^OXI6(wk=_(=$U0ZY_u1)l84=Q6B4$mYA|5foM2aNKu5q3izxhi+;k z%<@A)Av>q#fHY4q&JTL{q@U2!FnZ6Z6uay4bE(do7hpW)@{T3nwb*DrE(;YkZ;$&a*8r@CIaOHv z5lCWfH&5RnW``|gY@lO#Sx&mdp|*_MrHlD)<Nz}x2vyuknE~ak-BTssol9t z_1bLcWm&?vB861F{w<+bW(lxNRoEIzBEi1UXA;csy~xf}XyzJ!6EB zaW}(}=3YGUxyc*d^E$IqM88b}rMMW0q`hMn`Acto)c99{II1uGd6(TJtsZ@T2efo@ zy>#xDOIxK&=CX}>LHPZt_f!vXO1x-|F80z}P!*hakD#f^{t=?6%tsr|>{sK&i+}KL zRedvkhGv;?-qIXQhDWlG0d%(pYCP6p9t+ROxZ0|cC&y9*j1v%2aZRqU8+NooJmOjU zh_v$67T*w*lE6D1ngPE9cnnpgl0y79Am_K;uX3L#NfLiY%J!Ju&I;M}My*1lC3D#I zC-Vc8#jAbY$(2gG_XkgXhVif2WKou4|7rnBTaTeu9W4O`-**XVk5j8uOW#{3k@847 zB$A7RN4R@xpzBSmn6+Y|w6E28>+zTxG@}GTV9}>obXWds-O0?$JvKFU}`!nR;$q_n^sso z3wq*GP5W)#YmUrtv)_+RmC(O@#W$divYqtmFFIy()$4AI9Zh%SV1zpxSQ_GbOm zCvFQaW@LY9H6%znrQv2=omyq(@DoM57L`4{0p-1rg-6>p;|G~iO5b%5zyKiT7WCo> zT&wq*7$5dBt98hCEUC|cOpopa9~K5gSDGgzC;d0nh)w?t!fJ}^u5O~ zQ7K=LpJOm#%YyQO*7uFj<6VEgCo?3(Aj`Dnf^U4dCw_z1l6Me7^oLdZt2zI)a@%qL zB_Dq?GRb46-9DtXFW0BF)2X#{^yWkBpDo*ya5Y;T)w1Wc35jG9;Oub1l^s`xo1twA zPE!<@y;*;tE6vO}IhgR0u}t|QbV`s2PGt1Ak)L+k26j{3qe>Rg9z5si{nq%-a%!*j zdWL~HaN#I+0k%yi4Y`S6kK|s?E`u3VunLN_aic*hLWf%$ou$FjpmIQ!Eh~^Z6bmpl zz&!OD@P7|_1`}_XDf{WBoM#HvUs)vVyqwi8!^3Ivw84tO@F2tK)C*FHUxjIZ;sv}8 z11V(_m$aWg28beGn~UrVK&A~akNV2J*&hT<@9I!F6YZ#-#u;^ZQ}-Pu5uam{q1;sC zkdl^@9^x|ssr7Sa{rhp8n5vGo@5}d;{!u*&N2u(6kX>#2_#Jv2RkEY#rbgc+A5DJu67eUZ}Dac(xnX&gif#uOX4~qq9 zmq%9f#1@iu_2#kkHGGETFCeq0+Fcm7IDs4+=B(oWCX9xVfXyeWN)vl_Lg?9?*n93!b;E{}N74V#k__QmbZB@Oz)oXc!?@W9g>=fJ8)^K~lqF&Ke-h{opNGOzpL za#bboLMs$UwK-+gBlav@Onz7#F=$GQB7o!W#@#8e=A_pL_fWR|Z-tCD@iW93U%t=f7(Hy0KaWeT)}ez*zFCZ~Kk zZ}{X@(H~{e!~lQnDEn(ih4r^#KFfF&L*7sp##57rc%~}vL9^esB->+`e(}aW(+RY@ zJV%tYb%IuDs~v1Sh9NaTw7%dFv6YoEjoj#RALTbx@37gG%QW4Ab&xF@9A!s%2h*ii-YcV-^<}f^s2>`8tufVJsw513b zE$R(GLZ{d{l5$omQO;%=B;-9fTxzvQ1Fk+f@IN4Uv3u{z&bI+&hy%d6*{pOdsp4tO zirs9t19gX-!1no%AF5^TRtpWb;)m)LkYULvuuY1Ghaemra{QZ;=i|Ub&PgXe#ei7TP&_>--v&xj1zcWpef0tDVq1W@&{ilT zmTX<~yfVP6pognIq&C(c62kG?CzzkFg;&G3l!(bEer-)zOP$AL`gJbp>)PniA9J(N zF2`6uBJT&Ev9a6i{Yovas}(L)FEcd1%-!-(>oP{}dl4>_aqCc@&hVYvpBQ&yvea-ZI00pkN}9da>Md=AYM*MlSq7Ia1(daB5hfF$q!;}JWJd^ovQ%o* ziLtSMF_e-rMY15{tywX1zkT1JsFgl3iu^&l)3L z?MCe5diIL!t=bx58y&C=6B2;`Lifk2p&Y!vw0NM}Ye|e0RhlAg{gkKrDCuJODsZY` zr1%1$c`kx)_#EfgQg6|K)~h$LoqHvw&bu?_020JwXnn}D;F12*+OQU!c81bv>V=xM z99z6QSzVpmy7#l*MGMDj0S&Y!+yfvyM$P)Kk*@)Nl-e`D?k=Gpf+d}|&yXRQmkTSV znZ%?;vBM^=?AolK#&`kDE;_-&VBq5BeRytFDRUxM?7^GdR`5*$-7|y3D)pSdy&V0u zrKSzvH_)~BU1zLnI9}3daI$1P)C&154ehSSrCA?^=p~nuMex zQHi3QgTNn{9_|1qIEI*~=WzfIt?QZT4ttY9{9F(Hpwpj3eor)HWY_i8_!DtP!X)q? zsilg!G#K96!y_Qb;OF!U0MsrL@wkT`7cV1$LC@nv{58K@ymWCw!;X`SvN9IJi&s7g zj2beWL^p<@L~+FAYCB|4U%G%FoV1QG!rl9BP=#_lf}dTESvqV4_kK({+TnnyJcBzp zfU-UJ2;@L`SE*`3i>v9ODf^;AAVB7oW-$=idR6D8nA+1M1p|rfSLNV2gQ@Tv) z3DkFq*C(54Hmgwm6Bq$=45Z9j@4xjy!46m?$YNW-Ll|$a(279-``$peDvaC&o^SyB zWtL-uJ0s?Wgg#ZSyJlG&)*U(Q;0fn6Y#F{>oq#zz-P%mn=tbq*&>tZs$Mr2gj*|Ug zWT*a)a_6_aYGxH4yan6eA7z&5?i501Aw|)G8L|^YT`8(Wl+s$`=mOWzc6N4-FD`nf ztBkqREGQHCA}}ivU}jfG8@#Y4bkKj85=KqDgYENvdMzNV#@2e_?etUglQ`o-WhjOy_frl`hs1 zn+uPN7tec1q^GBcgoMsDGZv6dzQ# zhY%(nz`0lHcpc8aa9d&=ie;fU# zSlrbh(JqjUue;X>mzWdh%&-ole;#Z6k^+D}VwTcA3V*QhX6W7*0T`zfSh7GKFF|xm z*n9-yzCb64NOF-|VSEveqZ+^%0zt5pv69;Pj#mIxWP|%IuPI;ECu=wLZvzNqLJm)5DTk zhn1BybP|EKtB~A+K2NWd3}k>J0JL0dz}aV?$s})4RNjrDIwbjTJ7Radmj?DtKoBkD zh}6w4ni6Txzk=IE$Y7^^=Q|*Rn>VcCwV>csBk2OhaWQZFeJMjsFUUsd!{|xY&a!?? z$eRh%mvCE*(}ieaDN$3<2j^3}jb`wgnSYbfQjb}|b)!#{L66a;XmEf;ysys*1LV)J zM4w{+Oz%fm`n_Q)HMQy5vT*6*y0xy!9aQjxZbJ*IINL{eLwfz&M0fLQ`Ww8EQ|c>Z zbu+7vs4005NUB-sDO2fVlrTc#tFtndC*uIKO|rrV_V|1N5uKu`t?l^ul*L5ib7_*d zSIDu9*!0xs`W1vzKSgop0a}Eh7BLJ^#qOy(o9yN!vmfCu>K~*q5SeR!+9(z$#t)l9>d`cMbQ+L3$^xh9f_WZ0{T^0~4cf zfR$A8OE(?MSG70-g7gN(U z9=@U}DktKto4-NgyW6j6@VI#&r@9p+!i)D#dP1IQ8tZ`)#Em5u)7WAt{VGyC8>GDr zZWj!fhYNd};tVoda>W@2Xa6BCL+{vRh&D_j>Pv8K`w1JzvHN#`X-R=;oHi6ZJld=r zGhaa0(j9aC2Kv^7u=!HEt>X^iS*=)EUS}mgBh~End%}p*WS<_6e9b*B*7TDFZJ!Bf z-cX7RCBADZ2v}iF@(ilSe`rUY%+eq3Si@YA39ggsz0i@~=3EiAeA@DAKjawRHJWJA zz*KE8C_jU@Zq0P>G~Ch^{syuQi#q$#u-K(c?cGz8=M!BBTz4y0MfG1$3(GmN&L%z_fr5E#w+U{m#)kd+8^2&bFdF_HEFL8pZPBJwPSrbm0GxOB{`%+(ABRYQWUzxJjIsV9TH%Ik?H7bB zE?q}(tk*}>>VFRU>j96S=5DZkO1<%`CwB!ZzR(kTD90uDK4 z5hVtV1@9>A4$h9V%LZybH|rYC8P zXd_{MyBiJh*Ry7_$;*p$K4PL(D>FRSVJOUv1;Ecyb3Q{e2g90R?53X$}~%2a>Q$V_USB*)Nb=3nop^E*Ah zi64=wJw7BiDIaaq7O$^;`XPhJ95Jdfo*ZqySw!l+FKGIBZmC%mp$`f9~2Fhz$G{j`iS-Bd=(n%3usxbH_%%!LT zDG;~x2k8%SBZ0~nuxoM*--JQa?QAUKWUjnxo5yZM6h9-lGOVU@wY2CX$pCr!h`tP) zJI%!FsvjJ27U@=apE|o^2O}W)nY(%VFGbc)&^QAn~+`8$kX~D*#Ssz2C*j>m8$}0Nmw&|5yD?afX`LD zBWquTrw419D(p`Vn+K#e$+A0fw)JI?1zCcEnQRaXOk{qnM{HUu$> zA<;793b(h*Pb9VXFw*gD7+Me;bAgx{M2NauMC^+zMCmmuyOa;cA*5IX7cB}*dEVzs zSM4%Se629dqljxQ%Nc@-6xCSLUi}=gu34H}CZC?qX!Q?_#NQ7M48}^8i+x3V+J3k* zNy-c+9qp_)aix_AQu{&-HokTg##|rB3YsLq*C_8|OvtvlJXP-oSk^gch2`uqN`>$T zH1}Of!Lb<0!dB;%e&Vrv|2{e{fjovcWc|}80WvSKXQU=t)J6+Xd}$`u8B!fRleJ|9bIYf zOWKYhzlxbXFZ?A11Of5yCooP%@jXHo4%sIMkLlrT>37>`AD<3%Oy*_ctMfik9NBm` zkBL&TuLiLmzv)R)bq;N=ZF?_Wi^szGY81a&CIgczxL6O8AeO( zqMJomI0VzIc1xIJgO!;L4^t^y1?YU|=-|Q&nASK|!0DS!*_><*Z4w>eRp0qU03^AX z1N)Zy)~nx|ma+mtg4!C8`tbD``2P|=V+e!tXeBz7;m)O$=_wgVT&EaSv?f?sH4J{P z3;+EJBt_+fQzViTs1#|Nq6-%+$BgyU)_rqH^yx3 z7uSLt;XjK!_h5}-;-mzglmS_k{m^$k!}w>fkrv}4)$w=BqVx_s8Z}ngzbk=?$mRTR zo-5szbRiY0j;-8k`tAYx`-G$M*1Msb)%dRLPd~#uD4|ao3^7-X@%sx;Pf^P3WEq(< z)YjF*IK|?<`V2_;u5MQPW!n5b%ffOdcOQ@nmHRwjn<!jyG(a>O)^L|M*K- zH>x7A0|U-Mp7$;u^4_sou<)yJR~`f5X#HJnjR3KsG|)cm+o-%?*EozxB|PpzN*48z zW~hje#8^J?(!&Y&aYNb;ffNU&?wPO7Jw){~oT*a)(h&-~`h6Je1W}XxyBXyPXrj%t zUK$NuP2Z<~scwm{^lt9SFwhMmX1)m`9D0)Q)&6ka`fQrF8Coged+??FTl+icU!-2z zS;JHpe~|L@n}hu`7;|p_64PlG6itfuUvgvmZe_Z^#DDv78B0|@+;>wIXm@~DmXMjs zziv_=bp5KXd31QGUJT>nf)aB-A|zOb}h0q_U^wprlk< zu+=XJG*)oq#wFL>nIGYDZaGCZ2=){qT8N&^&n`#af`2^=Px?=CD$yYT{VOPI(u`Hw zlTC!aAor`~@mk^iB_Yn|ll1>ABsh8UaQY?1ps83AV+aUU;1}^rTs#S|NV5woE0FY7 zeOJi-tCN*$KJ@lh@7aEaE)s?`zxN~GOA>&ef#Snlan(|^1cF4I^1z|3gJ5B-sP zzg}Cjfc4}%dJ*+sE`aDSlvgm%VVg}kBocawl^~Yv65?h^^Kx9iaWR75ch;pcTup)E zIqFFKeA}t;`W6eWcIR`}yUemL;EB6DEp^;|a}RYZy*003bTEP+3<=+ti<6h$$kxei zaT7|AIc-fH)2m|}W-LusChVp;aawTZ^f`(3Pk%$Qo|5%^u9-Uog5ty{k)A*(H?i3-VtXVklhuVc2g-2%zVIWKA<)`(cJrOSGvjX6%Vjl ze%6@V<3-pb8N7XF%8o>U$PIQmTU5u1V+Hxzt+c8Qzv5Y4wQnN%*ZZr{6>L@b3HP@@ zEd7WgWOy2|tK#&8ZdQ)IYxcoduX8tBya79)O#ByGFDML&;8JwQU&A_Zus2wK3gFnB z8%aftOhD)nw-z(%q!$RYK&a=vd+IK#*Atl`KgxF5`l+5X)U|*NzWE`J6{_dO$%!y< ztZQj$VM_nm1-(4=;FN9jJ0E*8l(N6R>rA~^FY|VXaqRS%U5$D$BR*V|5qpRc?5I$r zr`7!KnNWZA7&&HhC7R-^qL?$pILn#*#(@yLUj{;}6E3!NtMd`bcA}slv><&8`{Tmf zYYLxErB&O$t9Q2P+ez_P%0D8aJLyC!Io|wv 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 Date: Thu, 24 Jun 2021 14:38:04 +0200 Subject: [PATCH 021/128] Add SonarQube endpoint Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/settings/settings.go | 60 +++++++------- internal/webhooks/sonarqube/main_test.go | 14 ++++ internal/webhooks/sonarqube/sonarqube.go | 61 ++++++++++++++ internal/webhooks/sonarqube/sonarqube_test.go | 79 +++++++++++++++++++ internal/webhooks/sonarqube/webhook.go | 47 +++++++++++ internal/webhooks/sonarqube/webhook_test.go | 23 ++++++ 6 files changed, 253 insertions(+), 31 deletions(-) create mode 100644 internal/webhooks/sonarqube/main_test.go create mode 100644 internal/webhooks/sonarqube/sonarqube.go create mode 100644 internal/webhooks/sonarqube/sonarqube_test.go create mode 100644 internal/webhooks/sonarqube/webhook.go create mode 100644 internal/webhooks/sonarqube/webhook_test.go diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 04c9dc4..208065d 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -48,33 +48,6 @@ var ( Projects []Project ) -func init() { - viper.SetConfigName("config.yaml") - viper.SetConfigType("yaml") - viper.SetEnvPrefix("prbot") - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - viper.AllowEmptyEnv(true) - viper.AutomaticEnv() - - ApplyConfigDefaults() -} - -func ApplyConfigDefaults() { - viper.SetDefault("gitea.url", "") - viper.SetDefault("gitea.token.value", "") - viper.SetDefault("gitea.token.file", "") - viper.SetDefault("gitea.webhook.secret", "") - viper.SetDefault("gitea.webhook.secretFile", "") - - viper.SetDefault("sonarqube.url", "") - viper.SetDefault("sonarqube.token.value", "") - viper.SetDefault("sonarqube.token.file", "") - viper.SetDefault("sonarqube.webhook.secret", "") - viper.SetDefault("sonarqube.webhook.secretFile", "") - - viper.SetDefault("projects", []Project{}) -} - func ReadSecretFile(file string, defaultValue string) (string) { if file == "" { return defaultValue @@ -88,10 +61,35 @@ func ReadSecretFile(file string, defaultValue string) (string) { return string(content) } -func Load(configPath string) { - viper.AddConfigPath(configPath) +func NewConfigReader() *viper.Viper { + v := viper.New() + v.SetConfigName("config.yaml") + v.SetConfigType("yaml") + v.SetEnvPrefix("prbot") + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + v.AllowEmptyEnv(true) + v.AutomaticEnv() - err := viper.ReadInConfig() + v.SetDefault("gitea.url", "") + v.SetDefault("gitea.token.value", "") + v.SetDefault("gitea.token.file", "") + v.SetDefault("gitea.webhook.secret", "") + v.SetDefault("gitea.webhook.secretFile", "") + v.SetDefault("sonarqube.url", "") + v.SetDefault("sonarqube.token.value", "") + v.SetDefault("sonarqube.token.file", "") + v.SetDefault("sonarqube.webhook.secret", "") + v.SetDefault("sonarqube.webhook.secretFile", "") + v.SetDefault("projects", []Project{}) + + return v +} + +func Load(configPath string) { + r := NewConfigReader() + r.AddConfigPath(configPath) + + err := r.ReadInConfig() if err != nil { panic(fmt.Errorf("Fatal error while reading config file: %w \n", err)) } @@ -102,7 +100,7 @@ func Load(configPath string) { Projects []Project } - err = viper.Unmarshal(&fullConfig) + err = r.Unmarshal(&fullConfig) if err != nil { panic(fmt.Errorf("Unable to load config into struct, %v", err)) } diff --git a/internal/webhooks/sonarqube/main_test.go b/internal/webhooks/sonarqube/main_test.go new file mode 100644 index 0000000..f3d2576 --- /dev/null +++ b/internal/webhooks/sonarqube/main_test.go @@ -0,0 +1,14 @@ +package sonarqube + +import ( + "log" + "io/ioutil" + "os" + "testing" +) + +// SETUP: mute logs +func TestMain(m *testing.M) { + log.SetOutput(ioutil.Discard) + os.Exit(m.Run()) +} diff --git a/internal/webhooks/sonarqube/sonarqube.go b/internal/webhooks/sonarqube/sonarqube.go new file mode 100644 index 0000000..5cdacd6 --- /dev/null +++ b/internal/webhooks/sonarqube/sonarqube.go @@ -0,0 +1,61 @@ +package sonarqube + +import ( + "fmt" + "log" + "io" + "io/ioutil" + "net/http" + + "github.com/justusbunsi/gitea-sonarqube-pr-bot/internal/settings" +) + +func inProjectsMapping(p []settings.Project, n string) bool { + for _, proj := range p { + if proj.SonarQube.Key == n { + return true + } + } + + return false +} + +func HandleWebhook(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + + project := r.Header.Get("X-SonarQube-Project") + if !inProjectsMapping(settings.Projects, project) { + log.Printf("Received hook for project '%s' which is not configured. Request ignored.", project) + + rw.WriteHeader(http.StatusOK) + io.WriteString(rw, fmt.Sprintf(`{"message": "Project '%s' not in configured list. Request ignored."}`, project)) + return + } + + log.Printf("Received hook for project '%s'. Processing data.", project) + + var raw []byte + var webhook *Webhook + var ok bool + var err error + + raw, err = ioutil.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + io.WriteString(rw, fmt.Sprintf(`{"message": "%s"}`, err.Error())) + return + } + + if webhook, ok = NewWebhook(raw); !ok { + rw.WriteHeader(http.StatusUnprocessableEntity) + io.WriteString(rw, `{"message": "Error parsing POST body."}`) + return + } + + log.Printf("%s", webhook) + + // Send response to SonarQube at this point to ensure being within 10 seconds limit of webhook response timeout + rw.WriteHeader(http.StatusOK) + io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`) +} diff --git a/internal/webhooks/sonarqube/sonarqube_test.go b/internal/webhooks/sonarqube/sonarqube_test.go new file mode 100644 index 0000000..5af8e4b --- /dev/null +++ b/internal/webhooks/sonarqube/sonarqube_test.go @@ -0,0 +1,79 @@ +package sonarqube + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/justusbunsi/gitea-sonarqube-pr-bot/internal/settings" + "github.com/stretchr/testify/assert" +) + +func withValidRequestData(t *testing.T) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) { + jsonBody := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) + req, err := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer(jsonBody)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("X-SonarQube-Project", "pr-bot") + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(HandleWebhook) + + return req, rr, handler +} + +func TestHandleWebhookProjectMapped(t *testing.T) { + settings.Projects = []settings.Project{ + settings.Project{ + SonarQube: struct{Key string}{ + Key: "pr-bot", + }, + }, + } + req, rr, handler := withValidRequestData(t) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) +} + +func TestHandleWebhookProjectNotMapped(t *testing.T) { + settings.Projects = []settings.Project{ + settings.Project{ + SonarQube: struct{Key string}{ + Key: "another-project", + }, + }, + } + req, rr, handler := withValidRequestData(t) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "Project 'pr-bot' not in configured list. Request ignored."}`, rr.Body.String()) +} + +func TestHandleWebhookInvalidJSONBody(t *testing.T) { + settings.Projects = []settings.Project{ + settings.Project{ + SonarQube: struct{Key string}{ + Key: "pr-bot", + }, + }, + } + + jsonBody := []byte(`{ "serverUrl": ["invalid-server-url-content"] }`) + req, err := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer(jsonBody)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("X-SonarQube-Project", "pr-bot") + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(HandleWebhook) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) + assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) +} diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go new file mode 100644 index 0000000..0e86693 --- /dev/null +++ b/internal/webhooks/sonarqube/webhook.go @@ -0,0 +1,47 @@ +package sonarqube + +import ( + "bytes" + "log" + + "github.com/spf13/viper" +) + +type Webhook struct { + ServerUrl string `mapstructure:"serverUrl"` + Revision string + Branch Branch + QualityGate QualityGate `mapstructure:"qualityGate"` +} + +type Branch struct { + Name string + Type string + Url string +} + +type QualityGate struct { + Status string + Conditions []QualityGateCondition +} + +type QualityGateCondition struct { + Metric string + Status string +} + +func NewWebhook(raw []byte) (*Webhook, bool) { + v := viper.New() + v.SetConfigType("json") + v.ReadConfig(bytes.NewBuffer(raw)) + + w := Webhook{} + + err := v.Unmarshal(&w) + if err != nil { + log.Printf("Error parsing SonarQube webhook: %s", err.Error()) + return nil, false + } + + return &w, true +} diff --git a/internal/webhooks/sonarqube/webhook_test.go b/internal/webhooks/sonarqube/webhook_test.go new file mode 100644 index 0000000..30164f8 --- /dev/null +++ b/internal/webhooks/sonarqube/webhook_test.go @@ -0,0 +1,23 @@ +package sonarqube + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewWebhook(t *testing.T) { + raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) + response, ok := NewWebhook(raw) + + assert.NotNil(t, response) + assert.True(t, ok) +} + +func TestNewWebhookInvalidJSON(t *testing.T) { + raw := []byte(`{ "serverUrl": ["invalid-server-url-content"] }`) + response, ok := NewWebhook(raw) + + assert.Nil(t, response) + assert.False(t, ok) +} From 7bfe729b67c9f6740d24191bb8f0803508151be6 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Thu, 24 Jun 2021 16:52:06 +0200 Subject: [PATCH 022/128] Skip non-PR SonarQube webhooks from processing Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/webhooks/sonarqube/sonarqube.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/internal/webhooks/sonarqube/sonarqube.go b/internal/webhooks/sonarqube/sonarqube.go index 5cdacd6..3ea4340 100644 --- a/internal/webhooks/sonarqube/sonarqube.go +++ b/internal/webhooks/sonarqube/sonarqube.go @@ -6,6 +6,7 @@ import ( "io" "io/ioutil" "net/http" + "strings" "github.com/justusbunsi/gitea-sonarqube-pr-bot/internal/settings" ) @@ -34,28 +35,30 @@ func HandleWebhook(rw http.ResponseWriter, r *http.Request) { log.Printf("Received hook for project '%s'. Processing data.", project) - var raw []byte - var webhook *Webhook - var ok bool - var err error - - raw, err = ioutil.ReadAll(r.Body) + raw, err := ioutil.ReadAll(r.Body) defer r.Body.Close() if err != nil { + log.Printf("Error reading request body %s", err.Error()) rw.WriteHeader(http.StatusInternalServerError) io.WriteString(rw, fmt.Sprintf(`{"message": "%s"}`, err.Error())) return } - if webhook, ok = NewWebhook(raw); !ok { + w, ok := NewWebhook(raw) + if !ok { rw.WriteHeader(http.StatusUnprocessableEntity) io.WriteString(rw, `{"message": "Error parsing POST body."}`) return } - log.Printf("%s", webhook) - // Send response to SonarQube at this point to ensure being within 10 seconds limit of webhook response timeout rw.WriteHeader(http.StatusOK) io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`) + + if strings.ToLower(w.Branch.Type) != "pull_request" { + log.Print("Ignore Hook for non-PR") + return + } + + log.Printf("%s", w) } From 4ba781d74f3e8b78578bceac85c7b036f78005c1 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Tue, 29 Jun 2021 08:07:28 +0200 Subject: [PATCH 023/128] Respect go package concepts Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- cmd/gitea-sonarqube-bot/main.go | 6 +- cmd/gitea-sonarqube-bot/main_test.go | 4 +- go.mod | 2 +- internal/settings/settings.go | 60 ++++++++++--------- internal/settings/settings_test.go | 38 ++++++------ internal/webhooks/sonarqube/sonarqube.go | 2 +- internal/webhooks/sonarqube/sonarqube_test.go | 2 +- internal/webhooks/sonarqube/webhook.go | 12 ++-- 8 files changed, 64 insertions(+), 62 deletions(-) diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index ba36da2..cdc33c0 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -4,10 +4,10 @@ import ( "os" "path" - "github.com/justusbunsi/gitea-sonarqube-pr-bot/internal/settings" + "gitea-sonarqube-pr-bot/internal/settings" ) -func GetConfigLocation() string { +func getConfigLocation() string { configPath := path.Join("config") if customConfigPath, ok := os.LookupEnv("PRBOT_CONFIG_PATH"); ok { configPath = customConfigPath @@ -17,5 +17,5 @@ func GetConfigLocation() string { } func main() { - settings.Load(GetConfigLocation()) + settings.Load(getConfigLocation()) } diff --git a/cmd/gitea-sonarqube-bot/main_test.go b/cmd/gitea-sonarqube-bot/main_test.go index 6f55850..b48aff4 100644 --- a/cmd/gitea-sonarqube-bot/main_test.go +++ b/cmd/gitea-sonarqube-bot/main_test.go @@ -8,13 +8,13 @@ import ( ) func TestGetConfigLocationWithDefault(t *testing.T) { - assert.Equal(t, "config", GetConfigLocation()) + assert.Equal(t, "config", getConfigLocation()) } func TestGetConfigLocationWithEnvironmentOverride(t *testing.T) { os.Setenv("PRBOT_CONFIG_PATH", "/tmp/") - assert.Equal(t, "/tmp/", GetConfigLocation()) + assert.Equal(t, "/tmp/", getConfigLocation()) t.Cleanup(func() { os.Unsetenv("PRBOT_CONFIG_PATH") diff --git a/go.mod b/go.mod index 3240254..abef9c1 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/justusbunsi/gitea-sonarqube-pr-bot +module gitea-sonarqube-pr-bot go 1.16 diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 208065d..7b4a0f8 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -8,47 +8,53 @@ import ( "github.com/spf13/viper" ) -type GiteaRepository struct { +type giteaRepository struct { Owner string Name string } -type Token struct { +type token struct { Value string File string } -type Webhook struct { +type webhook struct { Secret string SecretFile string } -type GiteaConfig struct { +type giteaConfig struct { Url string - Token Token - Webhook Webhook + Token token + Webhook webhook } -type SonarQubeConfig struct { +type sonarQubeConfig struct { Url string - Token Token - Webhook Webhook + Token token + Webhook webhook } type Project struct { SonarQube struct { Key string } `mapstructure:"sonarqube"` - Gitea GiteaRepository + Gitea giteaRepository +} + +type fullConfig struct { + Gitea giteaConfig + SonarQube sonarQubeConfig `mapstructure:"sonarqube"` + Projects []Project } var ( - Gitea GiteaConfig - SonarQube SonarQubeConfig + Gitea giteaConfig + SonarQube sonarQubeConfig Projects []Project ) -func ReadSecretFile(file string, defaultValue string) (string) { +func readSecretFile(file string, defaultValue string) (string) { if file == "" { return defaultValue } @@ -61,7 +67,7 @@ func ReadSecretFile(file string, defaultValue string) (string) { return string(content) } -func NewConfigReader() *viper.Viper { +func newConfigReader() *viper.Viper { v := viper.New() v.SetConfigName("config.yaml") v.SetConfigType("yaml") @@ -86,7 +92,7 @@ func NewConfigReader() *viper.Viper { } func Load(configPath string) { - r := NewConfigReader() + r := newConfigReader() r.AddConfigPath(configPath) err := r.ReadInConfig() @@ -94,27 +100,23 @@ func Load(configPath string) { panic(fmt.Errorf("Fatal error while reading config file: %w \n", err)) } - var fullConfig struct { - Gitea GiteaConfig - SonarQube SonarQubeConfig `mapstructure:"sonarqube"` - Projects []Project - } + var configuration fullConfig - err = r.Unmarshal(&fullConfig) + err = r.Unmarshal(&configuration) if err != nil { panic(fmt.Errorf("Unable to load config into struct, %v", err)) } - if len(fullConfig.Projects) == 0 { + if len(configuration.Projects) == 0 { panic("Invalid configuration. At least one project mapping is necessary.") } - Gitea = fullConfig.Gitea - SonarQube = fullConfig.SonarQube - Projects = fullConfig.Projects + Gitea = configuration.Gitea + SonarQube = configuration.SonarQube + Projects = configuration.Projects - Gitea.Webhook.Secret = ReadSecretFile(Gitea.Webhook.SecretFile, Gitea.Webhook.Secret) - Gitea.Token.Value = ReadSecretFile(Gitea.Token.File, Gitea.Token.Value) - SonarQube.Webhook.Secret = ReadSecretFile(SonarQube.Webhook.SecretFile, SonarQube.Webhook.Secret) - SonarQube.Token.Value = ReadSecretFile(SonarQube.Token.File, SonarQube.Token.Value) + Gitea.Webhook.Secret = readSecretFile(Gitea.Webhook.SecretFile, Gitea.Webhook.Secret) + Gitea.Token.Value = readSecretFile(Gitea.Token.File, Gitea.Token.Value) + SonarQube.Webhook.Secret = readSecretFile(SonarQube.Webhook.SecretFile, SonarQube.Webhook.Secret) + SonarQube.Token.Value = readSecretFile(SonarQube.Token.File, SonarQube.Token.Value) } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index a2631eb..20f8672 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -55,12 +55,12 @@ func TestLoadGiteaStructure(t *testing.T) { WriteConfigFile(t, defaultConfig) Load(os.TempDir()) - expected := GiteaConfig{ + expected := giteaConfig{ Url: "https://example.com/gitea", - Token: Token{ + Token: token{ Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", }, - Webhook: Webhook{ + Webhook: webhook{ Secret: "haxxor-gitea-secret", }, } @@ -74,12 +74,12 @@ func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { WriteConfigFile(t, defaultConfig) Load(os.TempDir()) - expected := GiteaConfig{ + expected := giteaConfig{ Url: "https://example.com/gitea", - Token: Token{ + Token: token{ Value: "injected-token", }, - Webhook: Webhook{ + Webhook: webhook{ Secret: "injected-webhook-secret", }, } @@ -96,12 +96,12 @@ func TestLoadSonarQubeStructure(t *testing.T) { WriteConfigFile(t, defaultConfig) Load(os.TempDir()) - expected := SonarQubeConfig{ + expected := sonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: Token{ + Token: token{ Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", }, - Webhook: Webhook{ + Webhook: webhook{ Secret: "haxxor-sonarqube-secret", }, } @@ -115,12 +115,12 @@ func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { WriteConfigFile(t, defaultConfig) Load(os.TempDir()) - expected := SonarQubeConfig{ + expected := sonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: Token{ + Token: token{ Value: "injected-token", }, - Webhook: Webhook{ + Webhook: webhook{ Secret: "injected-webhook-secret", }, } @@ -167,25 +167,25 @@ projects: os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRETFILE", sonarqubeWebhookSecretFile) os.Setenv("PRBOT_SONARQUBE_TOKEN_FILE", sonarqubeTokenFile) - expectedGitea := GiteaConfig{ + expectedGitea := giteaConfig{ Url: "https://example.com/gitea", - Token: Token{ + Token: token{ Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", File: giteaTokenFile, }, - Webhook: Webhook{ + Webhook: webhook{ Secret: "gitea-totally-secret", SecretFile: giteaWebhookSecretFile, }, } - expectedSonarQube := SonarQubeConfig{ + expectedSonarQube := sonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: Token{ + Token: token{ Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", File: sonarqubeTokenFile, }, - Webhook: Webhook{ + Webhook: webhook{ Secret: "sonarqube-totally-secret", SecretFile: sonarqubeWebhookSecretFile, }, @@ -216,7 +216,7 @@ func TestLoadProjectsStructure(t *testing.T) { SonarQube: struct {Key string}{ Key: "gitea-sonarqube-pr-bot", }, - Gitea: GiteaRepository{ + Gitea: giteaRepository{ Owner: "example-organization", Name: "pr-bot", }, diff --git a/internal/webhooks/sonarqube/sonarqube.go b/internal/webhooks/sonarqube/sonarqube.go index 3ea4340..996d85d 100644 --- a/internal/webhooks/sonarqube/sonarqube.go +++ b/internal/webhooks/sonarqube/sonarqube.go @@ -8,7 +8,7 @@ import ( "net/http" "strings" - "github.com/justusbunsi/gitea-sonarqube-pr-bot/internal/settings" + "gitea-sonarqube-pr-bot/internal/settings" ) func inProjectsMapping(p []settings.Project, n string) bool { diff --git a/internal/webhooks/sonarqube/sonarqube_test.go b/internal/webhooks/sonarqube/sonarqube_test.go index 5af8e4b..60fc9f0 100644 --- a/internal/webhooks/sonarqube/sonarqube_test.go +++ b/internal/webhooks/sonarqube/sonarqube_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "testing" - "github.com/justusbunsi/gitea-sonarqube-pr-bot/internal/settings" + "gitea-sonarqube-pr-bot/internal/settings" "github.com/stretchr/testify/assert" ) diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index 0e86693..acf34d4 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -10,22 +10,22 @@ import ( type Webhook struct { ServerUrl string `mapstructure:"serverUrl"` Revision string - Branch Branch - QualityGate QualityGate `mapstructure:"qualityGate"` + Branch branch + QualityGate qualityGate `mapstructure:"qualityGate"` } -type Branch struct { +type branch struct { Name string Type string Url string } -type QualityGate struct { +type qualityGate struct { Status string - Conditions []QualityGateCondition + Conditions []condition } -type QualityGateCondition struct { +type condition struct { Metric string Status string } From 86a644f31fc27c6ee7da3e105b1fb07f306f3597 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Tue, 29 Jun 2021 10:29:20 +0200 Subject: [PATCH 024/128] Use OOP-ish style for configuration loading Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- .gitignore | 2 + README.md | 1 + internal/settings/settings.go | 66 ++++++++++-------------------- internal/settings/settings_test.go | 32 +++++++-------- internal/settings/token.go | 38 +++++++++++++++++ internal/settings/webhook.go | 38 +++++++++++++++++ 6 files changed, 116 insertions(+), 61 deletions(-) create mode 100644 internal/settings/token.go create mode 100644 internal/settings/webhook.go diff --git a/.gitignore b/.gitignore index bb63e77..e30c4d3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /config/ /vendor/ /gitea-sonarqube-bot +/coverage.html +/*.log diff --git a/README.md b/README.md index 4ae98ab..6d22846 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Luckily, both endpoints have a proper REST API to communicate with each others. ## TODOs +- [ ] Validate configuration on startup - [ ] Maybe drop `PRBOT_CONFIG_PATH` environment variable in favor of `--config path/to/config.yaml` cli attribute - [ ] Configure SonarQube PR branch naming pattern for more flexibility (currently focused on Jenkins with [Gitea Plugin](https://github.com/jenkinsci/gitea-plugin)) - [ ] Configuration live reloading diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 7b4a0f8..13f5eab 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -2,7 +2,6 @@ package settings import ( "fmt" - "io/ioutil" "strings" "github.com/spf13/viper" @@ -13,26 +12,16 @@ type giteaRepository struct { Name string } -type token struct { - Value string - File string -} - -type webhook struct { - Secret string - SecretFile string -} - type giteaConfig struct { Url string - Token token - Webhook webhook + Token *token + Webhook *webhook } type sonarQubeConfig struct { Url string - Token token - Webhook webhook + Token *token + Webhook *webhook } type Project struct { @@ -42,31 +31,12 @@ type Project struct { Gitea giteaRepository } -type fullConfig struct { - Gitea giteaConfig - SonarQube sonarQubeConfig `mapstructure:"sonarqube"` - Projects []Project -} - var ( Gitea giteaConfig SonarQube sonarQubeConfig Projects []Project ) -func readSecretFile(file string, defaultValue string) (string) { - if file == "" { - return defaultValue - } - - content, err := ioutil.ReadFile(file) - if err != nil { - panic(fmt.Errorf("Cannot read '%s' or it is no regular file. %w", file, err)) - } - - return string(content) -} - func newConfigReader() *viper.Viper { v := viper.New() v.SetConfigName("config.yaml") @@ -100,23 +70,29 @@ func Load(configPath string) { panic(fmt.Errorf("Fatal error while reading config file: %w \n", err)) } - var configuration fullConfig + var projects []Project - err = r.Unmarshal(&configuration) + err = r.UnmarshalKey("projects", &projects) if err != nil { - panic(fmt.Errorf("Unable to load config into struct, %v", err)) + panic(fmt.Errorf("Unable to load project mapping: %s", err.Error())) } - if len(configuration.Projects) == 0 { + if len(projects) == 0 { panic("Invalid configuration. At least one project mapping is necessary.") } - Gitea = configuration.Gitea - SonarQube = configuration.SonarQube - Projects = configuration.Projects + Projects = projects - Gitea.Webhook.Secret = readSecretFile(Gitea.Webhook.SecretFile, Gitea.Webhook.Secret) - Gitea.Token.Value = readSecretFile(Gitea.Token.File, Gitea.Token.Value) - SonarQube.Webhook.Secret = readSecretFile(SonarQube.Webhook.SecretFile, SonarQube.Webhook.Secret) - SonarQube.Token.Value = readSecretFile(SonarQube.Token.File, SonarQube.Token.Value) + errCallback := func(msg string) {panic(msg)} + + Gitea = giteaConfig{ + Url: r.GetString("gitea.url"), + Token: NewToken(r, "gitea", errCallback), + Webhook: NewWebhook(r, "gitea", errCallback), + } + SonarQube = sonarQubeConfig{ + Url: r.GetString("sonarqube.url"), + Token: NewToken(r, "sonarqube", errCallback), + Webhook: NewWebhook(r, "sonarqube", errCallback), + } } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 20f8672..14f2154 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -57,10 +57,10 @@ func TestLoadGiteaStructure(t *testing.T) { expected := giteaConfig{ Url: "https://example.com/gitea", - Token: token{ + Token: &token{ Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", }, - Webhook: webhook{ + Webhook: &webhook{ Secret: "haxxor-gitea-secret", }, } @@ -76,10 +76,10 @@ func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { expected := giteaConfig{ Url: "https://example.com/gitea", - Token: token{ + Token: &token{ Value: "injected-token", }, - Webhook: webhook{ + Webhook: &webhook{ Secret: "injected-webhook-secret", }, } @@ -98,10 +98,10 @@ func TestLoadSonarQubeStructure(t *testing.T) { expected := sonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: token{ + Token: &token{ Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", }, - Webhook: webhook{ + Webhook: &webhook{ Secret: "haxxor-sonarqube-secret", }, } @@ -117,10 +117,10 @@ func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { expected := sonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: token{ + Token: &token{ Value: "injected-token", }, - Webhook: webhook{ + Webhook: &webhook{ Secret: "injected-webhook-secret", }, } @@ -169,25 +169,25 @@ projects: expectedGitea := giteaConfig{ Url: "https://example.com/gitea", - Token: token{ + Token: &token{ Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", - File: giteaTokenFile, + file: giteaTokenFile, }, - Webhook: webhook{ + Webhook: &webhook{ Secret: "gitea-totally-secret", - SecretFile: giteaWebhookSecretFile, + secretFile: giteaWebhookSecretFile, }, } expectedSonarQube := sonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: token{ + Token: &token{ Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", - File: sonarqubeTokenFile, + file: sonarqubeTokenFile, }, - Webhook: webhook{ + Webhook: &webhook{ Secret: "sonarqube-totally-secret", - SecretFile: sonarqubeWebhookSecretFile, + secretFile: sonarqubeWebhookSecretFile, }, } diff --git a/internal/settings/token.go b/internal/settings/token.go new file mode 100644 index 0000000..447c1dc --- /dev/null +++ b/internal/settings/token.go @@ -0,0 +1,38 @@ +package settings + +import ( + "fmt" + "io/ioutil" + + "github.com/spf13/viper" +) + +type token struct { + Value string + file string +} + +func (t *token) lookupSecret(errCallback func(string)) { + if t.file == "" { + return + } + + content, err := ioutil.ReadFile(t.file) + if err != nil { + errCallback(fmt.Sprintf("Cannot read '%s' or it is no regular file: %s", t.file, err.Error())) + return + } + + t.Value = string(content) +} + +func NewToken(v *viper.Viper, confContainer string, errCallback func(string)) *token { + t := &token{ + Value: v.GetString(fmt.Sprintf("%s.token.value", confContainer)), + file: v.GetString(fmt.Sprintf("%s.token.file", confContainer)), + } + + t.lookupSecret(errCallback) + + return t +} diff --git a/internal/settings/webhook.go b/internal/settings/webhook.go new file mode 100644 index 0000000..a52d495 --- /dev/null +++ b/internal/settings/webhook.go @@ -0,0 +1,38 @@ +package settings + +import ( + "fmt" + "io/ioutil" + + "github.com/spf13/viper" +) + +type webhook struct { + Secret string + secretFile string +} + +func (w *webhook) lookupSecret(errCallback func(string)) { + if w.secretFile == "" { + return + } + + content, err := ioutil.ReadFile(w.secretFile) + if err != nil { + errCallback(fmt.Sprintf("Cannot read '%s' or it is no regular file: %s", w.secretFile, err.Error())) + return + } + + w.Secret = string(content) +} + +func NewWebhook(v *viper.Viper, confContainer string, errCallback func(string)) *webhook { + w := &webhook{ + Secret: v.GetString(fmt.Sprintf("%s.webhook.secret", confContainer)), + secretFile: v.GetString(fmt.Sprintf("%s.webhook.secretFile", confContainer)), + } + + w.lookupSecret(errCallback) + + return w +} From 5082e5d3f300ffaf05c27a9c2369eef47a674473 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Tue, 29 Jun 2021 11:59:49 +0200 Subject: [PATCH 025/128] Use OOP-ish style for SonarQube webhook handling Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/webhook_handler/main_test.go | 14 +++++++ .../sonarqube.go | 30 ++++++++++++--- .../sonarqube_test.go | 21 ++++++---- internal/webhooks/sonarqube/webhook.go | 38 ++++++++----------- internal/webhooks/sonarqube/webhook_test.go | 5 +-- 5 files changed, 70 insertions(+), 38 deletions(-) create mode 100644 internal/webhook_handler/main_test.go rename internal/{webhooks/sonarqube => webhook_handler}/sonarqube.go (66%) rename internal/{webhooks/sonarqube => webhook_handler}/sonarqube_test.go (83%) diff --git a/internal/webhook_handler/main_test.go b/internal/webhook_handler/main_test.go new file mode 100644 index 0000000..9814e29 --- /dev/null +++ b/internal/webhook_handler/main_test.go @@ -0,0 +1,14 @@ +package webhook_handler + +import ( + "log" + "io/ioutil" + "os" + "testing" +) + +// SETUP: mute logs +func TestMain(m *testing.M) { + log.SetOutput(ioutil.Discard) + os.Exit(m.Run()) +} diff --git a/internal/webhooks/sonarqube/sonarqube.go b/internal/webhook_handler/sonarqube.go similarity index 66% rename from internal/webhooks/sonarqube/sonarqube.go rename to internal/webhook_handler/sonarqube.go index 996d85d..6422ec7 100644 --- a/internal/webhooks/sonarqube/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -1,4 +1,4 @@ -package sonarqube +package webhook_handler import ( "fmt" @@ -9,9 +9,16 @@ import ( "strings" "gitea-sonarqube-pr-bot/internal/settings" + webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" ) -func inProjectsMapping(p []settings.Project, n string) bool { +type fetchDetailsType func(w *webhook.Webhook) + +type SonarQubeWebhookHandler struct { + fetchDetails fetchDetailsType +} + +func (_ *SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) bool { for _, proj := range p { if proj.SonarQube.Key == n { return true @@ -21,11 +28,11 @@ func inProjectsMapping(p []settings.Project, n string) bool { return false } -func HandleWebhook(rw http.ResponseWriter, r *http.Request) { +func (h *SonarQubeWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") project := r.Header.Get("X-SonarQube-Project") - if !inProjectsMapping(settings.Projects, project) { + if !h.inProjectsMapping(settings.Projects, project) { log.Printf("Received hook for project '%s' which is not configured. Request ignored.", project) rw.WriteHeader(http.StatusOK) @@ -44,7 +51,7 @@ func HandleWebhook(rw http.ResponseWriter, r *http.Request) { return } - w, ok := NewWebhook(raw) + w, ok := webhook.New(raw) if !ok { rw.WriteHeader(http.StatusUnprocessableEntity) io.WriteString(rw, `{"message": "Error parsing POST body."}`) @@ -60,5 +67,16 @@ func HandleWebhook(rw http.ResponseWriter, r *http.Request) { return } - log.Printf("%s", w) + h.fetchDetails(w) +} + + +func fetchDetails(w *webhook.Webhook) { + log.Printf("Hello from the original one: %s", w) +} + +func NewSonarQubeWebhookHandler() *SonarQubeWebhookHandler { + return &SonarQubeWebhookHandler{ + fetchDetails: fetchDetails, + } } diff --git a/internal/webhooks/sonarqube/sonarqube_test.go b/internal/webhook_handler/sonarqube_test.go similarity index 83% rename from internal/webhooks/sonarqube/sonarqube_test.go rename to internal/webhook_handler/sonarqube_test.go index 60fc9f0..78c0bd0 100644 --- a/internal/webhooks/sonarqube/sonarqube_test.go +++ b/internal/webhook_handler/sonarqube_test.go @@ -1,16 +1,23 @@ -package sonarqube +package webhook_handler import ( "bytes" + "log" "net/http" "net/http/httptest" "testing" - "gitea-sonarqube-pr-bot/internal/settings" "github.com/stretchr/testify/assert" + "gitea-sonarqube-pr-bot/internal/settings" + webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" ) func withValidRequestData(t *testing.T) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) { + webhookHandler := NewSonarQubeWebhookHandler() + webhookHandler.fetchDetails = func(w *webhook.Webhook) { + log.Printf("Overridden fetchDetails") + } + jsonBody := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) req, err := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer(jsonBody)) if err != nil { @@ -19,12 +26,12 @@ func withValidRequestData(t *testing.T) (*http.Request, *httptest.ResponseRecord req.Header.Set("X-SonarQube-Project", "pr-bot") rr := httptest.NewRecorder() - handler := http.HandlerFunc(HandleWebhook) + handler := http.HandlerFunc(webhookHandler.Handle) return req, rr, handler } -func TestHandleWebhookProjectMapped(t *testing.T) { +func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { settings.Projects = []settings.Project{ settings.Project{ SonarQube: struct{Key string}{ @@ -39,7 +46,7 @@ func TestHandleWebhookProjectMapped(t *testing.T) { assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) } -func TestHandleWebhookProjectNotMapped(t *testing.T) { +func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { settings.Projects = []settings.Project{ settings.Project{ SonarQube: struct{Key string}{ @@ -54,7 +61,7 @@ func TestHandleWebhookProjectNotMapped(t *testing.T) { assert.Equal(t, `{"message": "Project 'pr-bot' not in configured list. Request ignored."}`, rr.Body.String()) } -func TestHandleWebhookInvalidJSONBody(t *testing.T) { +func TestHandleSonarQubeWebhookInvalidJSONBody(t *testing.T) { settings.Projects = []settings.Project{ settings.Project{ SonarQube: struct{Key string}{ @@ -71,7 +78,7 @@ func TestHandleWebhookInvalidJSONBody(t *testing.T) { req.Header.Set("X-SonarQube-Project", "pr-bot") rr := httptest.NewRecorder() - handler := http.HandlerFunc(HandleWebhook) + handler := http.HandlerFunc(NewSonarQubeWebhookHandler().Handle) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index acf34d4..16010b3 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -10,38 +10,32 @@ import ( type Webhook struct { ServerUrl string `mapstructure:"serverUrl"` Revision string - Branch branch - QualityGate qualityGate `mapstructure:"qualityGate"` + Branch struct { + Name string + Type string + Url string + } + QualityGate struct { + Status string + Conditions []struct { + Metric string + Status string + } + } `mapstructure:"qualityGate"` } -type branch struct { - Name string - Type string - Url string -} - -type qualityGate struct { - Status string - Conditions []condition -} - -type condition struct { - Metric string - Status string -} - -func NewWebhook(raw []byte) (*Webhook, bool) { +func New(raw []byte) (*Webhook, bool) { v := viper.New() v.SetConfigType("json") v.ReadConfig(bytes.NewBuffer(raw)) - w := Webhook{} + w := &Webhook{} err := v.Unmarshal(&w) if err != nil { log.Printf("Error parsing SonarQube webhook: %s", err.Error()) - return nil, false + return w, false } - return &w, true + return w, true } diff --git a/internal/webhooks/sonarqube/webhook_test.go b/internal/webhooks/sonarqube/webhook_test.go index 30164f8..020b543 100644 --- a/internal/webhooks/sonarqube/webhook_test.go +++ b/internal/webhooks/sonarqube/webhook_test.go @@ -8,7 +8,7 @@ import ( func TestNewWebhook(t *testing.T) { raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) - response, ok := NewWebhook(raw) + response, ok := New(raw) assert.NotNil(t, response) assert.True(t, ok) @@ -16,8 +16,7 @@ func TestNewWebhook(t *testing.T) { func TestNewWebhookInvalidJSON(t *testing.T) { raw := []byte(`{ "serverUrl": ["invalid-server-url-content"] }`) - response, ok := NewWebhook(raw) + _, ok := New(raw) - assert.Nil(t, response) assert.False(t, ok) } From b7fa2b77f9f6ff2b519b5bdfe72f0a01a5c1d337 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 11 Jul 2021 12:28:00 +0200 Subject: [PATCH 026/128] Add tests for PR-only handling Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- go.sum | 1 + internal/webhook_handler/sonarqube.go | 4 +- internal/webhook_handler/sonarqube_test.go | 76 ++++++++++++++++------ 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/go.sum b/go.sum index 0bb7549..b9b6def 100644 --- a/go.sum +++ b/go.sum @@ -221,6 +221,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.8.0 h1:QRwDgoG8xX+kp69di68D+YYTCWfYEckbZRfUlEIAal0= github.com/spf13/viper v1.8.0/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/internal/webhook_handler/sonarqube.go b/internal/webhook_handler/sonarqube.go index 6422ec7..71e1a50 100644 --- a/internal/webhook_handler/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -12,10 +12,8 @@ import ( webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" ) -type fetchDetailsType func(w *webhook.Webhook) - type SonarQubeWebhookHandler struct { - fetchDetails fetchDetailsType + fetchDetails func(w *webhook.Webhook) } func (_ *SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) bool { diff --git a/internal/webhook_handler/sonarqube_test.go b/internal/webhook_handler/sonarqube_test.go index 78c0bd0..42bb915 100644 --- a/internal/webhook_handler/sonarqube_test.go +++ b/internal/webhook_handler/sonarqube_test.go @@ -2,23 +2,35 @@ package webhook_handler import ( "bytes" - "log" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" ) -func withValidRequestData(t *testing.T) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) { - webhookHandler := NewSonarQubeWebhookHandler() - webhookHandler.fetchDetails = func(w *webhook.Webhook) { - log.Printf("Overridden fetchDetails") - } +type HandlerPartialMock struct { + mock.Mock +} + +func (h *HandlerPartialMock) fetchDetails(w *webhook.Webhook) { + h.Called(w) +} + +func defaultMockPreparation(h *HandlerPartialMock) { + h.On("fetchDetails", mock.Anything).Return(nil) +} + +func withValidRequestData(t *testing.T, mockPreparation func(*HandlerPartialMock), jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc, *HandlerPartialMock) { + partialMock := new(HandlerPartialMock) + mockPreparation(partialMock) + + webhookHandler := NewSonarQubeWebhookHandler() + webhookHandler.fetchDetails = partialMock.fetchDetails - jsonBody := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) req, err := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer(jsonBody)) if err != nil { t.Fatal(err) @@ -28,7 +40,7 @@ func withValidRequestData(t *testing.T) (*http.Request, *httptest.ResponseRecord rr := httptest.NewRecorder() handler := http.HandlerFunc(webhookHandler.Handle) - return req, rr, handler + return req, rr, handler, partialMock } func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { @@ -39,7 +51,7 @@ func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { }, }, } - req, rr, handler := withValidRequestData(t) + req, rr, handler, _ := withValidRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) @@ -54,7 +66,7 @@ func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { }, }, } - req, rr, handler := withValidRequestData(t) + req, rr, handler, _ := withValidRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) @@ -70,17 +82,43 @@ func TestHandleSonarQubeWebhookInvalidJSONBody(t *testing.T) { }, } - jsonBody := []byte(`{ "serverUrl": ["invalid-server-url-content"] }`) - req, err := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer(jsonBody)) - if err != nil { - t.Fatal(err) - } - req.Header.Set("X-SonarQube-Project", "pr-bot") - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(NewSonarQubeWebhookHandler().Handle) + req, rr, handler, _ := withValidRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": ["invalid-server-url-content"] }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) } + +func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { + settings.Projects = []settings.Project{ + settings.Project{ + SonarQube: struct{Key string}{ + Key: "pr-bot", + }, + }, + } + + req, rr, handler, partialMock := withValidRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + partialMock.AssertNumberOfCalls(t, "fetchDetails", 1) +} + +func TestHandleSonarQubeWebhookForBranch(t *testing.T) { + settings.Projects = []settings.Project{ + settings.Project{ + SonarQube: struct{Key string}{ + Key: "pr-bot", + }, + }, + } + + req, rr, handler, partialMock := withValidRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "BRANCH", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + partialMock.AssertNumberOfCalls(t, "fetchDetails", 0) +} From 230e85875b6d3f4221839186e9085e0d1cae5b17 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 11 Jul 2021 13:54:32 +0200 Subject: [PATCH 027/128] Parse pull request index from SQ branch name Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/webhook_handler/sonarqube.go | 3 +++ internal/webhooks/sonarqube/webhook.go | 13 +++++++++++++ internal/webhooks/sonarqube/webhook_test.go | 20 ++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/internal/webhook_handler/sonarqube.go b/internal/webhook_handler/sonarqube.go index 71e1a50..06b56c7 100644 --- a/internal/webhook_handler/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -66,6 +66,9 @@ func (h *SonarQubeWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request } h.fetchDetails(w) + if idx, err1 := w.GetPRIndex(); err1 == nil { + log.Printf("New details for Gitea PR %d", idx) + } } diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index 16010b3..3dee3e4 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -2,7 +2,10 @@ package sonarqube import ( "bytes" + "fmt" "log" + "regexp" + "strconv" "github.com/spf13/viper" ) @@ -24,6 +27,16 @@ type Webhook struct { } `mapstructure:"qualityGate"` } +func (w *Webhook) GetPRIndex() (int, error) { + re := regexp.MustCompile(`^PR-(\d+)$`) + res := re.FindSubmatch([]byte(w.Branch.Name)) + if len(res) != 2 { + return 0, fmt.Errorf("Branch name '%s' does not match regex '%s'. Extracting PR index not possible.", w.Branch.Name, re.String()) + } + + return strconv.Atoi(fmt.Sprintf("%s", res[1])) +} + func New(raw []byte) (*Webhook, bool) { v := viper.New() v.SetConfigType("json") diff --git a/internal/webhooks/sonarqube/webhook_test.go b/internal/webhooks/sonarqube/webhook_test.go index 020b543..ef31b8f 100644 --- a/internal/webhooks/sonarqube/webhook_test.go +++ b/internal/webhooks/sonarqube/webhook_test.go @@ -20,3 +20,23 @@ func TestNewWebhookInvalidJSON(t *testing.T) { assert.False(t, ok) } + +func TestGetPRIndex(t *testing.T) { + raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) + w, _ := New(raw) + + idx, err := w.GetPRIndex() + + assert.Equal(t, 1337, idx) + assert.Nil(t, err) +} + +func TestGetPRIndexInvalidBranchName(t *testing.T) { + raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "invalid", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) + w, _ := New(raw) + + idx, err := w.GetPRIndex() + + assert.Equal(t, 0, idx) + assert.NotNil(t, err) +} From 630519f63a158a527119aefa2d07e08a1230c901 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 11 Jul 2021 15:16:18 +0200 Subject: [PATCH 028/128] Fix endpoint documentation Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- config/config.example.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.example.yaml b/config/config.example.yaml index 5c845c6..951ac4d 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -1,7 +1,7 @@ # Gitea related configuration. Necessary for adding/updating comments on repository pull requests gitea: - # API endpoint of your Gitea instance. Must be the API base path as shown in Swagger UI. - url: https://try.gitea.io/api/v1 + # Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. + url: https://try.gitea.io # Created access token for the user that shall be used as bot account. # User needs "Read project" permissions with access to "Pull Requests" @@ -21,8 +21,8 @@ gitea: # SonarQube related configuration. Necessary for requesting data from the API and processing the webhook. sonarqube: - # API endpoint of your SonarQube instance. - url: https://sonarcloud.io/api + # Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. + url: https://sonarcloud.io # Created access token for the user that shall be used as bot account. # User needs "Browse on project" permissions From 2af7ba3da113ae4ac17b7a39799ba241583cbdc3 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 11 Jul 2021 15:17:03 +0200 Subject: [PATCH 029/128] Make application runnable Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- cmd/gitea-sonarqube-bot/main.go | 15 +++++++++ go.mod | 1 + go.sum | 2 ++ internal/webhook_handler/main.go | 57 ++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 internal/webhook_handler/main.go diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index cdc33c0..c69dc47 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -1,10 +1,13 @@ package main import ( + "log" "os" "path" "gitea-sonarqube-pr-bot/internal/settings" + handler "gitea-sonarqube-pr-bot/internal/webhook_handler" + "github.com/urfave/cli/v2" ) func getConfigLocation() string { @@ -18,4 +21,16 @@ func getConfigLocation() string { func main() { settings.Load(getConfigLocation()) + + app := &cli.App{ + Name: "gitea-sonarqube-pr-bot", + Usage: "Improve your experience with SonarQube and Gitea", + Description: `By default, gitea-sonarqube-pr-bot will start running the webserver if no arguments are passed.`, + Action: handler.Serve, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } diff --git a/go.mod b/go.mod index abef9c1..2f1864f 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect github.com/coreos/etcd v3.3.10+incompatible // indirect github.com/coreos/go-etcd v2.0.0+incompatible // indirect + github.com/gorilla/mux v1.8.0 // indirect github.com/spf13/viper v1.8.0 // indirect github.com/stretchr/testify v1.7.0 // indirect github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect diff --git a/go.sum b/go.sum index b9b6def..df5b50f 100644 --- a/go.sum +++ b/go.sum @@ -141,6 +141,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= diff --git a/internal/webhook_handler/main.go b/internal/webhook_handler/main.go new file mode 100644 index 0000000..016d12a --- /dev/null +++ b/internal/webhook_handler/main.go @@ -0,0 +1,57 @@ +package webhook_handler + +import ( + "context" + "flag" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "time" + + "github.com/gorilla/mux" + "github.com/urfave/cli/v2" +) + +func Serve(c *cli.Context) error { + fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") + + var wait time.Duration + flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish") + flag.Parse() + + r := mux.NewRouter() + r.HandleFunc("/hooks/sonarqube", NewSonarQubeWebhookHandler().Handle).Methods("POST").Headers("X-SonarQube-Project", "") + + srv := &http.Server{ + Addr: "0.0.0.0:8080", + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: r, + } + + go func() { + log.Println("Listen on :8080") + if err := srv.ListenAndServe(); err != nil { + log.Println(err) + } + }() + + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt) + + // Block until we receive our signal. + <-ch + + // Create a deadline to wait for. + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() + srv.Shutdown(ctx) + log.Println("Shutting down webhook server") + os.Exit(0) + + return nil +} From a0f2684029ec4b710023644675c16f4cd8c16c3f Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 12 Jul 2021 12:36:22 +0200 Subject: [PATCH 030/128] Add Gitea SDK to actually post a comment Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- go.mod | 14 ++---- go.sum | 16 ++++-- internal/gitea_sdk/gitea_sdk.go | 36 +++++++++++++ internal/settings/settings.go | 4 +- internal/settings/settings_test.go | 2 +- internal/webhook_handler/main.go | 3 +- internal/webhook_handler/sonarqube.go | 56 ++++++++++++--------- internal/webhook_handler/sonarqube_test.go | 10 +++- internal/webhooks/sonarqube/webhook.go | 34 +++++++++---- internal/webhooks/sonarqube/webhook_test.go | 20 ++------ 10 files changed, 126 insertions(+), 69 deletions(-) create mode 100644 internal/gitea_sdk/gitea_sdk.go diff --git a/go.mod b/go.mod index 2f1864f..62a80bf 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,9 @@ module gitea-sonarqube-pr-bot go 1.16 require ( - github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect - github.com/coreos/etcd v3.3.10+incompatible // indirect - github.com/coreos/go-etcd v2.0.0+incompatible // indirect - github.com/gorilla/mux v1.8.0 // indirect - github.com/spf13/viper v1.8.0 // indirect - github.com/stretchr/testify v1.7.0 // indirect - github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect - github.com/urfave/cli/v2 v2.3.0 // indirect - github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 // indirect + code.gitea.io/sdk/gitea v0.14.1 + github.com/gorilla/mux v1.8.0 + github.com/spf13/viper v1.8.0 + github.com/stretchr/testify v1.7.0 + github.com/urfave/cli/v2 v2.3.0 ) diff --git a/go.sum b/go.sum index df5b50f..f96408f 100644 --- a/go.sum +++ b/go.sum @@ -36,12 +36,13 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +code.gitea.io/sdk/gitea v0.14.1 h1:NaRluse+dAxVM5RmHC7Xktfas5a8WWmcnUBlJLhJycA= +code.gitea.io/sdk/gitea v0.14.1/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= @@ -54,8 +55,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= @@ -140,6 +139,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= @@ -156,6 +156,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -170,12 +172,15 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= @@ -211,7 +216,9 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= @@ -234,10 +241,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -569,6 +574,7 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= diff --git a/internal/gitea_sdk/gitea_sdk.go b/internal/gitea_sdk/gitea_sdk.go new file mode 100644 index 0000000..0daac70 --- /dev/null +++ b/internal/gitea_sdk/gitea_sdk.go @@ -0,0 +1,36 @@ +package gitea_sdk + +import ( + "fmt" + "log" + "gitea-sonarqube-pr-bot/internal/settings" + "code.gitea.io/sdk/gitea" +) + +type GiteaSdkInterface interface { + PostComment(settings.GiteaRepository, int, string) error +} + +type GiteaSdk struct { + client *gitea.Client +} + +func (sdk *GiteaSdk) PostComment(repo settings.GiteaRepository, idx int, msg string) error { + opt := gitea.CreateIssueCommentOption{ + Body: msg, + } + + log.Printf("Owner: %s | Repo: %s | Index: %d | Options: %s", repo.Owner, repo.Name, idx, opt) + _, _, err := sdk.client.CreateIssueComment(repo.Owner, repo.Name, int64(idx), opt) + + return err +} + +func New() *GiteaSdk { + client, err := gitea.NewClient(settings.Gitea.Url, gitea.SetToken(settings.Gitea.Token.Value)) + if err != nil { + panic(fmt.Errorf("Cannot initialize Gitea client: %w", err)) + } + + return &GiteaSdk{client} +} diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 13f5eab..7005fd6 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/viper" ) -type giteaRepository struct { +type GiteaRepository struct { Owner string Name string } @@ -28,7 +28,7 @@ type Project struct { SonarQube struct { Key string } `mapstructure:"sonarqube"` - Gitea giteaRepository + Gitea GiteaRepository } var ( diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 14f2154..fa67a9d 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -216,7 +216,7 @@ func TestLoadProjectsStructure(t *testing.T) { SonarQube: struct {Key string}{ Key: "gitea-sonarqube-pr-bot", }, - Gitea: giteaRepository{ + Gitea: GiteaRepository{ Owner: "example-organization", Name: "pr-bot", }, diff --git a/internal/webhook_handler/main.go b/internal/webhook_handler/main.go index 016d12a..9f09067 100644 --- a/internal/webhook_handler/main.go +++ b/internal/webhook_handler/main.go @@ -10,6 +10,7 @@ import ( "os/signal" "time" + sdk "gitea-sonarqube-pr-bot/internal/gitea_sdk" "github.com/gorilla/mux" "github.com/urfave/cli/v2" ) @@ -22,7 +23,7 @@ func Serve(c *cli.Context) error { flag.Parse() r := mux.NewRouter() - r.HandleFunc("/hooks/sonarqube", NewSonarQubeWebhookHandler().Handle).Methods("POST").Headers("X-SonarQube-Project", "") + r.HandleFunc("/hooks/sonarqube", NewSonarQubeWebhookHandler(sdk.New()).Handle).Methods("POST").Headers("X-SonarQube-Project", "") srv := &http.Server{ Addr: "0.0.0.0:8080", diff --git a/internal/webhook_handler/sonarqube.go b/internal/webhook_handler/sonarqube.go index 06b56c7..8996ed5 100644 --- a/internal/webhook_handler/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -9,36 +9,55 @@ import ( "strings" "gitea-sonarqube-pr-bot/internal/settings" + sdk "gitea-sonarqube-pr-bot/internal/gitea_sdk" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" ) type SonarQubeWebhookHandler struct { fetchDetails func(w *webhook.Webhook) + giteaSdk sdk.GiteaSdkInterface } -func (_ *SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) bool { - for _, proj := range p { +func (h *SonarQubeWebhookHandler) composeGiteaComment(w *webhook.Webhook) string { + return fmt.Sprintf("Hello from pr-bot. SonarQube data for '%s' has been processed.", w.Project.Key) +} + +func (_ *SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) (bool, int) { + for idx, proj := range p { if proj.SonarQube.Key == n { - return true + return true, idx } } - return false + return false, 0 +} + +func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings.GiteaRepository) { + if strings.ToLower(w.Branch.Type) != "pull_request" { + log.Println("Ignore Hook for non-PR") + return + } + + h.fetchDetails(w) + + comment := h.composeGiteaComment(w) + h.giteaSdk.PostComment(repo, w.PRIndex, comment) } func (h *SonarQubeWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") - project := r.Header.Get("X-SonarQube-Project") - if !h.inProjectsMapping(settings.Projects, project) { - log.Printf("Received hook for project '%s' which is not configured. Request ignored.", project) + projectName := r.Header.Get("X-SonarQube-Project") + found, pIdx := h.inProjectsMapping(settings.Projects, projectName) + if !found { + log.Printf("Received hook for project '%s' which is not configured. Request ignored.", projectName) rw.WriteHeader(http.StatusOK) - io.WriteString(rw, fmt.Sprintf(`{"message": "Project '%s' not in configured list. Request ignored."}`, project)) + io.WriteString(rw, fmt.Sprintf(`{"message": "Project '%s' not in configured list. Request ignored."}`, projectName)) return } - log.Printf("Received hook for project '%s'. Processing data.", project) + log.Printf("Received hook for project '%s'. Processing data.", projectName) raw, err := ioutil.ReadAll(r.Body) defer r.Body.Close() @@ -60,24 +79,13 @@ func (h *SonarQubeWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request rw.WriteHeader(http.StatusOK) io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`) - if strings.ToLower(w.Branch.Type) != "pull_request" { - log.Print("Ignore Hook for non-PR") - return - } - - h.fetchDetails(w) - if idx, err1 := w.GetPRIndex(); err1 == nil { - log.Printf("New details for Gitea PR %d", idx) - } + h.processData(w, settings.Projects[pIdx].Gitea) } - func fetchDetails(w *webhook.Webhook) { - log.Printf("Hello from the original one: %s", w) + log.Printf("This method will load additional data from SonarQube based on PR %d", w.PRIndex) } -func NewSonarQubeWebhookHandler() *SonarQubeWebhookHandler { - return &SonarQubeWebhookHandler{ - fetchDetails: fetchDetails, - } +func NewSonarQubeWebhookHandler(giteaSdk sdk.GiteaSdkInterface) *SonarQubeWebhookHandler { + return &SonarQubeWebhookHandler{fetchDetails, giteaSdk} } diff --git a/internal/webhook_handler/sonarqube_test.go b/internal/webhook_handler/sonarqube_test.go index 42bb915..c79d76b 100644 --- a/internal/webhook_handler/sonarqube_test.go +++ b/internal/webhook_handler/sonarqube_test.go @@ -20,6 +20,14 @@ func (h *HandlerPartialMock) fetchDetails(w *webhook.Webhook) { h.Called(w) } +type GiteaSdkMock struct { + mock.Mock +} + +func (h *GiteaSdkMock) PostComment(_ settings.GiteaRepository, _ int, _ string) error { + return nil +} + func defaultMockPreparation(h *HandlerPartialMock) { h.On("fetchDetails", mock.Anything).Return(nil) } @@ -28,7 +36,7 @@ func withValidRequestData(t *testing.T, mockPreparation func(*HandlerPartialMock partialMock := new(HandlerPartialMock) mockPreparation(partialMock) - webhookHandler := NewSonarQubeWebhookHandler() + webhookHandler := NewSonarQubeWebhookHandler(new(GiteaSdkMock)) webhookHandler.fetchDetails = partialMock.fetchDetails req, err := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer(jsonBody)) diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index 3dee3e4..5dd0cc9 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -13,6 +13,11 @@ import ( type Webhook struct { ServerUrl string `mapstructure:"serverUrl"` Revision string + Project struct { + Key string + Name string + Url string + } Branch struct { Name string Type string @@ -25,16 +30,7 @@ type Webhook struct { Status string } } `mapstructure:"qualityGate"` -} - -func (w *Webhook) GetPRIndex() (int, error) { - re := regexp.MustCompile(`^PR-(\d+)$`) - res := re.FindSubmatch([]byte(w.Branch.Name)) - if len(res) != 2 { - return 0, fmt.Errorf("Branch name '%s' does not match regex '%s'. Extracting PR index not possible.", w.Branch.Name, re.String()) - } - - return strconv.Atoi(fmt.Sprintf("%s", res[1])) + PRIndex int } func New(raw []byte) (*Webhook, bool) { @@ -50,5 +46,23 @@ func New(raw []byte) (*Webhook, bool) { return w, false } + idx, err1 := parsePRIndex(w) + if err1 != nil { + log.Printf("Error parsing PR index: %s", err1.Error()) + return w, false + } + + w.PRIndex = idx + return w, true } + +func parsePRIndex(w *Webhook) (int, error) { + re := regexp.MustCompile(`^PR-(\d+)$`) + res := re.FindSubmatch([]byte(w.Branch.Name)) + if len(res) != 2 { + return 0, fmt.Errorf("Branch name '%s' does not match regex '%s'.", w.Branch.Name, re.String()) + } + + return strconv.Atoi(fmt.Sprintf("%s", res[1])) +} diff --git a/internal/webhooks/sonarqube/webhook_test.go b/internal/webhooks/sonarqube/webhook_test.go index ef31b8f..941e48f 100644 --- a/internal/webhooks/sonarqube/webhook_test.go +++ b/internal/webhooks/sonarqube/webhook_test.go @@ -11,6 +11,7 @@ func TestNewWebhook(t *testing.T) { response, ok := New(raw) assert.NotNil(t, response) + assert.Equal(t, 1337, response.PRIndex) assert.True(t, ok) } @@ -21,22 +22,9 @@ func TestNewWebhookInvalidJSON(t *testing.T) { assert.False(t, ok) } -func TestGetPRIndex(t *testing.T) { - raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) - w, _ := New(raw) - - idx, err := w.GetPRIndex() - - assert.Equal(t, 1337, idx) - assert.Nil(t, err) -} - -func TestGetPRIndexInvalidBranchName(t *testing.T) { +func TestNewWebhookInvalidBranchName(t *testing.T) { raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "invalid", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) - w, _ := New(raw) + _, ok := New(raw) - idx, err := w.GetPRIndex() - - assert.Equal(t, 0, idx) - assert.NotNil(t, err) + assert.False(t, ok) } From 1dab92385f3855604be28794c3f4b84562ca63da Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 12 Jul 2021 16:58:48 +0200 Subject: [PATCH 031/128] Fetch raw measures from SonarQube Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/{ => clients}/gitea_sdk/gitea_sdk.go | 2 - .../clients/sonarqube_sdk/sonarqube_sdk.go | 50 +++++++++++++++++++ internal/webhook_handler/main.go | 5 +- internal/webhook_handler/sonarqube.go | 39 +++++++++++++-- 4 files changed, 87 insertions(+), 9 deletions(-) rename internal/{ => clients}/gitea_sdk/gitea_sdk.go (87%) create mode 100644 internal/clients/sonarqube_sdk/sonarqube_sdk.go diff --git a/internal/gitea_sdk/gitea_sdk.go b/internal/clients/gitea_sdk/gitea_sdk.go similarity index 87% rename from internal/gitea_sdk/gitea_sdk.go rename to internal/clients/gitea_sdk/gitea_sdk.go index 0daac70..6d5ea01 100644 --- a/internal/gitea_sdk/gitea_sdk.go +++ b/internal/clients/gitea_sdk/gitea_sdk.go @@ -2,7 +2,6 @@ package gitea_sdk import ( "fmt" - "log" "gitea-sonarqube-pr-bot/internal/settings" "code.gitea.io/sdk/gitea" ) @@ -20,7 +19,6 @@ func (sdk *GiteaSdk) PostComment(repo settings.GiteaRepository, idx int, msg str Body: msg, } - log.Printf("Owner: %s | Repo: %s | Index: %d | Options: %s", repo.Owner, repo.Name, idx, opt) _, _, err := sdk.client.CreateIssueComment(repo.Owner, repo.Name, int64(idx), opt) return err diff --git a/internal/clients/sonarqube_sdk/sonarqube_sdk.go b/internal/clients/sonarqube_sdk/sonarqube_sdk.go new file mode 100644 index 0000000..5556a4b --- /dev/null +++ b/internal/clients/sonarqube_sdk/sonarqube_sdk.go @@ -0,0 +1,50 @@ +package sonarqube_sdk + +import ( + "net/http" + "fmt" + "log" + "encoding/base64" + "io" + + "gitea-sonarqube-pr-bot/internal/settings" +) + +type SonarQubeSdkInterface interface { + GetMeasures(string, string) (string, error) +} + +type SonarQubeSdk struct { + client *http.Client + baseUrl string + token string +} + +func (sdk *SonarQubeSdk) GetMeasures(project string, branch string) (string, error) { + url := fmt.Sprintf("%s/api/measures/component?additionalFields=metrics&metricKeys=bugs,vulnerabilities,new_security_hotspots,violations&component=%s&pullRequest=%s", sdk.baseUrl, project, branch) + log.Println(url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + panic(fmt.Errorf("Cannot initialize Request: %w", err)) + } + req.Header.Add("Authorization", sdk.basicAuth()) + resp, _ := sdk.client.Do(req) + + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + return string(body), nil +} + +func (sdk *SonarQubeSdk) basicAuth() string { + auth := []byte(fmt.Sprintf("%s:", sdk.token)) + return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(auth)) +} + +func New() *SonarQubeSdk { + return &SonarQubeSdk{ + client: &http.Client{}, + baseUrl: settings.SonarQube.Url, + token: settings.SonarQube.Token.Value, + } +} diff --git a/internal/webhook_handler/main.go b/internal/webhook_handler/main.go index 9f09067..8a2b3b8 100644 --- a/internal/webhook_handler/main.go +++ b/internal/webhook_handler/main.go @@ -10,7 +10,8 @@ import ( "os/signal" "time" - sdk "gitea-sonarqube-pr-bot/internal/gitea_sdk" + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "github.com/gorilla/mux" "github.com/urfave/cli/v2" ) @@ -23,7 +24,7 @@ func Serve(c *cli.Context) error { flag.Parse() r := mux.NewRouter() - r.HandleFunc("/hooks/sonarqube", NewSonarQubeWebhookHandler(sdk.New()).Handle).Methods("POST").Headers("X-SonarQube-Project", "") + r.HandleFunc("/hooks/sonarqube", NewSonarQubeWebhookHandler(giteaSdk.New(), sqSdk.New()).Handle).Methods("POST").Headers("X-SonarQube-Project", "") srv := &http.Server{ Addr: "0.0.0.0:8080", diff --git a/internal/webhook_handler/sonarqube.go b/internal/webhook_handler/sonarqube.go index 8996ed5..9aa1cb9 100644 --- a/internal/webhook_handler/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -9,17 +9,42 @@ import ( "strings" "gitea-sonarqube-pr-bot/internal/settings" - sdk "gitea-sonarqube-pr-bot/internal/gitea_sdk" + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" ) type SonarQubeWebhookHandler struct { fetchDetails func(w *webhook.Webhook) - giteaSdk sdk.GiteaSdkInterface + giteaSdk giteaSdk.GiteaSdkInterface + sqSdk sqSdk.SonarQubeSdkInterface } func (h *SonarQubeWebhookHandler) composeGiteaComment(w *webhook.Webhook) string { - return fmt.Sprintf("Hello from pr-bot. SonarQube data for '%s' has been processed.", w.Project.Key) + a, _ := h.sqSdk.GetMeasures(w.Project.Key, w.Branch.Name) + + log.Println(a) + + status := ":white_check_mark:" + if w.QualityGate.Status != "OK" { + status = ":x:" + } + + measures := `| Metric | Current | +| -------- | -------- | +| Bugs | 123 | +| Code Smells | 1 | +| Vulnerabilities | 1 | +` + + msg := `**Quality Gate**: %s + +**Measures** + +%s + +See [SonarQube](https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1) for details.` + return fmt.Sprintf(msg, status, measures) } func (_ *SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) (bool, int) { @@ -86,6 +111,10 @@ func fetchDetails(w *webhook.Webhook) { log.Printf("This method will load additional data from SonarQube based on PR %d", w.PRIndex) } -func NewSonarQubeWebhookHandler(giteaSdk sdk.GiteaSdkInterface) *SonarQubeWebhookHandler { - return &SonarQubeWebhookHandler{fetchDetails, giteaSdk} +func NewSonarQubeWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) *SonarQubeWebhookHandler { + return &SonarQubeWebhookHandler{ + fetchDetails: fetchDetails, + giteaSdk: g, + sqSdk: sq, + } } From 16f545f179f3035491f591b6a9c36b79bf0cdeb2 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 3 Oct 2021 17:49:23 +0200 Subject: [PATCH 032/128] Fix failing tests Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- .gitignore | 1 + CONTRIBUTING.md | 11 +++++++--- internal/webhook_handler/sonarqube_test.go | 25 +++++++++++++++------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index e30c4d3..73c14b5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /gitea-sonarqube-bot /coverage.html /*.log +/cover.out diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7cdf12c..3fda7be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,9 +3,10 @@ ## Table of Contents - [Contribution Guidelines](#contribution-guidelines) - - [Setup development environment](#setup-development-environment) - - [Testing](#testing) - - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) + - [Table of Contents](#table-of-contents) + - [Setup development environment](#setup-development-environment) + - [Testing](#testing) + - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) ## Setup development environment @@ -26,7 +27,11 @@ go build ./cmd/gitea-sonarqube-bot ## Testing ```bash +# generic test execution go test ./... + +# or with coverage report +go test -coverprofile cover.out ./... ``` ## Developer Certificate of Origin (DCO) diff --git a/internal/webhook_handler/sonarqube_test.go b/internal/webhook_handler/sonarqube_test.go index c79d76b..dce3b3f 100644 --- a/internal/webhook_handler/sonarqube_test.go +++ b/internal/webhook_handler/sonarqube_test.go @@ -6,10 +6,11 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) type HandlerPartialMock struct { @@ -28,6 +29,14 @@ func (h *GiteaSdkMock) PostComment(_ settings.GiteaRepository, _ int, _ string) return nil } +type SQSdkMock struct { + mock.Mock +} + +func (h *SQSdkMock) GetMeasures(project string, branch string) (string, error) { + return "", nil +} + func defaultMockPreparation(h *HandlerPartialMock) { h.On("fetchDetails", mock.Anything).Return(nil) } @@ -36,7 +45,7 @@ func withValidRequestData(t *testing.T, mockPreparation func(*HandlerPartialMock partialMock := new(HandlerPartialMock) mockPreparation(partialMock) - webhookHandler := NewSonarQubeWebhookHandler(new(GiteaSdkMock)) + webhookHandler := NewSonarQubeWebhookHandler(new(GiteaSdkMock), new(SQSdkMock)) webhookHandler.fetchDetails = partialMock.fetchDetails req, err := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer(jsonBody)) @@ -54,7 +63,7 @@ func withValidRequestData(t *testing.T, mockPreparation func(*HandlerPartialMock func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { settings.Projects = []settings.Project{ settings.Project{ - SonarQube: struct{Key string}{ + SonarQube: struct{ Key string }{ Key: "pr-bot", }, }, @@ -69,7 +78,7 @@ func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { settings.Projects = []settings.Project{ settings.Project{ - SonarQube: struct{Key string}{ + SonarQube: struct{ Key string }{ Key: "another-project", }, }, @@ -84,7 +93,7 @@ func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { func TestHandleSonarQubeWebhookInvalidJSONBody(t *testing.T) { settings.Projects = []settings.Project{ settings.Project{ - SonarQube: struct{Key string}{ + SonarQube: struct{ Key string }{ Key: "pr-bot", }, }, @@ -100,7 +109,7 @@ func TestHandleSonarQubeWebhookInvalidJSONBody(t *testing.T) { func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { settings.Projects = []settings.Project{ settings.Project{ - SonarQube: struct{Key string}{ + SonarQube: struct{ Key string }{ Key: "pr-bot", }, }, @@ -117,7 +126,7 @@ func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { func TestHandleSonarQubeWebhookForBranch(t *testing.T) { settings.Projects = []settings.Project{ settings.Project{ - SonarQube: struct{Key string}{ + SonarQube: struct{ Key string }{ Key: "pr-bot", }, }, From a1990a60f4c8866afdf1cecfb69dc4cc5cd14303 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 3 Oct 2021 17:57:22 +0200 Subject: [PATCH 033/128] Format code Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- cmd/gitea-sonarqube-bot/main.go | 6 ++-- internal/clients/gitea_sdk/gitea_sdk.go | 1 + .../clients/sonarqube_sdk/sonarqube_sdk.go | 14 +++++----- internal/settings/settings.go | 24 ++++++++-------- internal/settings/settings_test.go | 28 +++++++++---------- internal/settings/token.go | 4 +-- internal/settings/webhook.go | 4 +-- internal/webhook_handler/main.go | 8 +++--- internal/webhook_handler/main_test.go | 2 +- internal/webhook_handler/sonarqube.go | 12 ++++---- internal/webhooks/sonarqube/main_test.go | 2 +- internal/webhooks/sonarqube/webhook.go | 16 +++++------ 12 files changed, 61 insertions(+), 60 deletions(-) diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index c69dc47..2d8fcd1 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -23,10 +23,10 @@ func main() { settings.Load(getConfigLocation()) app := &cli.App{ - Name: "gitea-sonarqube-pr-bot", - Usage: "Improve your experience with SonarQube and Gitea", + Name: "gitea-sonarqube-pr-bot", + Usage: "Improve your experience with SonarQube and Gitea", Description: `By default, gitea-sonarqube-pr-bot will start running the webserver if no arguments are passed.`, - Action: handler.Serve, + Action: handler.Serve, } err := app.Run(os.Args) diff --git a/internal/clients/gitea_sdk/gitea_sdk.go b/internal/clients/gitea_sdk/gitea_sdk.go index 6d5ea01..5486301 100644 --- a/internal/clients/gitea_sdk/gitea_sdk.go +++ b/internal/clients/gitea_sdk/gitea_sdk.go @@ -3,6 +3,7 @@ package gitea_sdk import ( "fmt" "gitea-sonarqube-pr-bot/internal/settings" + "code.gitea.io/sdk/gitea" ) diff --git a/internal/clients/sonarqube_sdk/sonarqube_sdk.go b/internal/clients/sonarqube_sdk/sonarqube_sdk.go index 5556a4b..b6d683b 100644 --- a/internal/clients/sonarqube_sdk/sonarqube_sdk.go +++ b/internal/clients/sonarqube_sdk/sonarqube_sdk.go @@ -1,11 +1,11 @@ package sonarqube_sdk import ( - "net/http" - "fmt" - "log" "encoding/base64" + "fmt" "io" + "log" + "net/http" "gitea-sonarqube-pr-bot/internal/settings" ) @@ -15,9 +15,9 @@ type SonarQubeSdkInterface interface { } type SonarQubeSdk struct { - client *http.Client + client *http.Client baseUrl string - token string + token string } func (sdk *SonarQubeSdk) GetMeasures(project string, branch string) (string, error) { @@ -43,8 +43,8 @@ func (sdk *SonarQubeSdk) basicAuth() string { func New() *SonarQubeSdk { return &SonarQubeSdk{ - client: &http.Client{}, + client: &http.Client{}, baseUrl: settings.SonarQube.Url, - token: settings.SonarQube.Token.Value, + token: settings.SonarQube.Token.Value, } } diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 7005fd6..bb031a6 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -9,18 +9,18 @@ import ( type GiteaRepository struct { Owner string - Name string + Name string } type giteaConfig struct { - Url string - Token *token + Url string + Token *token Webhook *webhook } type sonarQubeConfig struct { - Url string - Token *token + Url string + Token *token Webhook *webhook } @@ -32,9 +32,9 @@ type Project struct { } var ( - Gitea giteaConfig + Gitea giteaConfig SonarQube sonarQubeConfig - Projects []Project + Projects []Project ) func newConfigReader() *viper.Viper { @@ -83,16 +83,16 @@ func Load(configPath string) { Projects = projects - errCallback := func(msg string) {panic(msg)} + errCallback := func(msg string) { panic(msg) } Gitea = giteaConfig{ - Url: r.GetString("gitea.url"), - Token: NewToken(r, "gitea", errCallback), + Url: r.GetString("gitea.url"), + Token: NewToken(r, "gitea", errCallback), Webhook: NewWebhook(r, "gitea", errCallback), } SonarQube = sonarQubeConfig{ - Url: r.GetString("sonarqube.url"), - Token: NewToken(r, "sonarqube", errCallback), + Url: r.GetString("sonarqube.url"), + Token: NewToken(r, "sonarqube", errCallback), Webhook: NewWebhook(r, "sonarqube", errCallback), } } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index fa67a9d..82cd6cd 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -10,7 +10,7 @@ import ( ) var defaultConfig []byte = []byte( -`gitea: + `gitea: url: https://example.com/gitea token: value: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 @@ -38,7 +38,7 @@ func WriteConfigFile(t *testing.T, content []byte) { os.Remove(config) }) - _ = ioutil.WriteFile(config, content,0444) + _ = ioutil.WriteFile(config, content, 0444) } func TestLoadWithMissingFile(t *testing.T) { @@ -135,19 +135,19 @@ func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { func TestLoadStructureWithFileReferenceResolving(t *testing.T) { giteaWebhookSecretFile := path.Join(os.TempDir(), "webhook-secret-gitea") - _ = ioutil.WriteFile(giteaWebhookSecretFile, []byte(`gitea-totally-secret`),0444) + _ = ioutil.WriteFile(giteaWebhookSecretFile, []byte(`gitea-totally-secret`), 0444) giteaTokenFile := path.Join(os.TempDir(), "token-secret-gitea") - _ = ioutil.WriteFile(giteaTokenFile, []byte(`d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565`),0444) + _ = ioutil.WriteFile(giteaTokenFile, []byte(`d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565`), 0444) sonarqubeWebhookSecretFile := path.Join(os.TempDir(), "webhook-secret-sonarqube") - _ = ioutil.WriteFile(sonarqubeWebhookSecretFile, []byte(`sonarqube-totally-secret`),0444) + _ = ioutil.WriteFile(sonarqubeWebhookSecretFile, []byte(`sonarqube-totally-secret`), 0444) sonarqubeTokenFile := path.Join(os.TempDir(), "token-secret-sonarqube") - _ = ioutil.WriteFile(sonarqubeTokenFile, []byte(`a09eb5785b25bb2cbacf48808a677a0709f02d8e`),0444) + _ = ioutil.WriteFile(sonarqubeTokenFile, []byte(`a09eb5785b25bb2cbacf48808a677a0709f02d8e`), 0444) WriteConfigFile(t, []byte( -`gitea: + `gitea: url: https://example.com/gitea token: value: fake-gitea-token @@ -171,10 +171,10 @@ projects: Url: "https://example.com/gitea", Token: &token{ Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", - file: giteaTokenFile, + file: giteaTokenFile, }, Webhook: &webhook{ - Secret: "gitea-totally-secret", + Secret: "gitea-totally-secret", secretFile: giteaWebhookSecretFile, }, } @@ -183,10 +183,10 @@ projects: Url: "https://example.com/sonarqube", Token: &token{ Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", - file: sonarqubeTokenFile, + file: sonarqubeTokenFile, }, Webhook: &webhook{ - Secret: "sonarqube-totally-secret", + Secret: "sonarqube-totally-secret", secretFile: sonarqubeWebhookSecretFile, }, } @@ -213,12 +213,12 @@ func TestLoadProjectsStructure(t *testing.T) { expectedProjects := []Project{ Project{ - SonarQube: struct {Key string}{ + SonarQube: struct{ Key string }{ Key: "gitea-sonarqube-pr-bot", }, Gitea: GiteaRepository{ Owner: "example-organization", - Name: "pr-bot", + Name: "pr-bot", }, }, } @@ -228,7 +228,7 @@ func TestLoadProjectsStructure(t *testing.T) { func TestLoadProjectsStructureWithNoMapping(t *testing.T) { invalidConfig := []byte( -`gitea: + `gitea: url: https://example.com/gitea token: value: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 diff --git a/internal/settings/token.go b/internal/settings/token.go index 447c1dc..be7ce93 100644 --- a/internal/settings/token.go +++ b/internal/settings/token.go @@ -9,7 +9,7 @@ import ( type token struct { Value string - file string + file string } func (t *token) lookupSecret(errCallback func(string)) { @@ -29,7 +29,7 @@ func (t *token) lookupSecret(errCallback func(string)) { func NewToken(v *viper.Viper, confContainer string, errCallback func(string)) *token { t := &token{ Value: v.GetString(fmt.Sprintf("%s.token.value", confContainer)), - file: v.GetString(fmt.Sprintf("%s.token.file", confContainer)), + file: v.GetString(fmt.Sprintf("%s.token.file", confContainer)), } t.lookupSecret(errCallback) diff --git a/internal/settings/webhook.go b/internal/settings/webhook.go index a52d495..64333f4 100644 --- a/internal/settings/webhook.go +++ b/internal/settings/webhook.go @@ -8,7 +8,7 @@ import ( ) type webhook struct { - Secret string + Secret string secretFile string } @@ -28,7 +28,7 @@ func (w *webhook) lookupSecret(errCallback func(string)) { func NewWebhook(v *viper.Viper, confContainer string, errCallback func(string)) *webhook { w := &webhook{ - Secret: v.GetString(fmt.Sprintf("%s.webhook.secret", confContainer)), + Secret: v.GetString(fmt.Sprintf("%s.webhook.secret", confContainer)), secretFile: v.GetString(fmt.Sprintf("%s.webhook.secretFile", confContainer)), } diff --git a/internal/webhook_handler/main.go b/internal/webhook_handler/main.go index 8a2b3b8..db4c492 100644 --- a/internal/webhook_handler/main.go +++ b/internal/webhook_handler/main.go @@ -20,7 +20,7 @@ func Serve(c *cli.Context) error { fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") var wait time.Duration - flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish") + flag.DurationVar(&wait, "graceful-timeout", time.Second*15, "the duration for which the server gracefully wait for existing connections to finish") flag.Parse() r := mux.NewRouter() @@ -30,9 +30,9 @@ func Serve(c *cli.Context) error { Addr: "0.0.0.0:8080", // Good practice to set timeouts to avoid Slowloris attacks. WriteTimeout: time.Second * 15, - ReadTimeout: time.Second * 15, - IdleTimeout: time.Second * 60, - Handler: r, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: r, } go func() { diff --git a/internal/webhook_handler/main_test.go b/internal/webhook_handler/main_test.go index 9814e29..04d9c4e 100644 --- a/internal/webhook_handler/main_test.go +++ b/internal/webhook_handler/main_test.go @@ -1,8 +1,8 @@ package webhook_handler import ( - "log" "io/ioutil" + "log" "os" "testing" ) diff --git a/internal/webhook_handler/sonarqube.go b/internal/webhook_handler/sonarqube.go index 9aa1cb9..439ed9d 100644 --- a/internal/webhook_handler/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -2,22 +2,22 @@ package webhook_handler import ( "fmt" - "log" "io" "io/ioutil" + "log" "net/http" "strings" - "gitea-sonarqube-pr-bot/internal/settings" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" + "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" ) type SonarQubeWebhookHandler struct { fetchDetails func(w *webhook.Webhook) - giteaSdk giteaSdk.GiteaSdkInterface - sqSdk sqSdk.SonarQubeSdkInterface + giteaSdk giteaSdk.GiteaSdkInterface + sqSdk sqSdk.SonarQubeSdkInterface } func (h *SonarQubeWebhookHandler) composeGiteaComment(w *webhook.Webhook) string { @@ -114,7 +114,7 @@ func fetchDetails(w *webhook.Webhook) { func NewSonarQubeWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) *SonarQubeWebhookHandler { return &SonarQubeWebhookHandler{ fetchDetails: fetchDetails, - giteaSdk: g, - sqSdk: sq, + giteaSdk: g, + sqSdk: sq, } } diff --git a/internal/webhooks/sonarqube/main_test.go b/internal/webhooks/sonarqube/main_test.go index f3d2576..40fa850 100644 --- a/internal/webhooks/sonarqube/main_test.go +++ b/internal/webhooks/sonarqube/main_test.go @@ -1,8 +1,8 @@ package sonarqube import ( - "log" "io/ioutil" + "log" "os" "testing" ) diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index 5dd0cc9..4ccb3bf 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -12,19 +12,19 @@ import ( type Webhook struct { ServerUrl string `mapstructure:"serverUrl"` - Revision string - Project struct { - Key string + Revision string + Project struct { + Key string Name string - Url string + Url string } Branch struct { Name string Type string - Url string - } + Url string + } QualityGate struct { - Status string + Status string Conditions []struct { Metric string Status string @@ -42,7 +42,7 @@ func New(raw []byte) (*Webhook, bool) { err := v.Unmarshal(&w) if err != nil { - log.Printf("Error parsing SonarQube webhook: %s", err.Error()) + log.Printf("Error parsing SonarQube webhook: %s", err.Error()) return w, false } From 3e65387cd738e2575966d7f752abfc30b5a65c6c Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 3 Oct 2021 17:57:52 +0200 Subject: [PATCH 034/128] Bump Gitea-SDK to 1.15.0 Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 62a80bf..8797f3f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module gitea-sonarqube-pr-bot go 1.16 require ( - code.gitea.io/sdk/gitea v0.14.1 + code.gitea.io/sdk/gitea v0.15.0 github.com/gorilla/mux v1.8.0 github.com/spf13/viper v1.8.0 github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index f96408f..9ab03c2 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,11 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/sdk/gitea v0.14.1 h1:NaRluse+dAxVM5RmHC7Xktfas5a8WWmcnUBlJLhJycA= code.gitea.io/sdk/gitea v0.14.1/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs= +code.gitea.io/sdk/gitea v0.15.0 h1:tsNhxDM/2N1Ohv1Xq5UWrht/esg0WmtRj4wsHVHriTg= +code.gitea.io/sdk/gitea v0.15.0/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -450,6 +453,7 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= From c3566d920852739aac540e5212de3973f45c4512 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 3 Oct 2021 19:36:05 +0200 Subject: [PATCH 035/128] Update dependencies Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- go.mod | 7 ++- go.sum | 172 ++++++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 128 insertions(+), 51 deletions(-) diff --git a/go.mod b/go.mod index 8797f3f..5c8b698 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,13 @@ go 1.16 require ( code.gitea.io/sdk/gitea v0.15.0 + github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/gorilla/mux v1.8.0 - github.com/spf13/viper v1.8.0 + github.com/hashicorp/go-version v1.3.0 // indirect + github.com/spf13/viper v1.9.0 + github.com/stretchr/objx v0.3.0 // indirect github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 + golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c // indirect + golang.org/x/text v0.3.7 // indirect ) diff --git a/go.sum b/go.sum index 9ab03c2..c82a082 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,11 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -26,7 +31,7 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -37,20 +42,20 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= -code.gitea.io/sdk/gitea v0.14.1 h1:NaRluse+dAxVM5RmHC7Xktfas5a8WWmcnUBlJLhJycA= -code.gitea.io/sdk/gitea v0.14.1/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs= code.gitea.io/sdk/gitea v0.15.0 h1:tsNhxDM/2N1Ohv1Xq5UWrht/esg0WmtRj4wsHVHriTg= code.gitea.io/sdk/gitea v0.15.0/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -58,10 +63,12 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -71,10 +78,12 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -93,6 +102,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -110,6 +120,7 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -123,10 +134,12 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -138,103 +151,111 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= +github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.8.0 h1:QRwDgoG8xX+kp69di68D+YYTCWfYEckbZRfUlEIAal0= -github.com/spf13/viper v1.8.0/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk= +github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -261,6 +282,7 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= @@ -269,8 +291,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -309,7 +333,6 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -320,6 +343,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -342,6 +366,7 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -353,7 +378,10 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -369,6 +397,7 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -376,12 +405,17 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -405,8 +439,17 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c h1:EyJTLQbOxvk8V6oDdD8ILR1BOs3nEJXThD6aqsiPNkM= +golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -414,8 +457,10 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -425,7 +470,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -433,9 +477,9 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -470,7 +514,11 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -496,7 +544,12 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -544,7 +597,18 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -564,7 +628,13 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5 google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -577,12 +647,14 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c= +gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From e608a8f969ac14a458ea84e799c3643807f96498 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sat, 9 Oct 2021 18:09:54 +0200 Subject: [PATCH 036/128] Retrieve actual data from SonarQube for comment Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/clients/gitea_sdk/gitea_sdk.go | 2 +- internal/clients/sonarqube_sdk/measures.go | 53 +++++++++++++++++++ .../clients/sonarqube_sdk/sonarqube_sdk.go | 26 +++++---- internal/webhook_handler/main.go | 1 + internal/webhook_handler/sonarqube.go | 41 ++++++-------- internal/webhook_handler/sonarqube_test.go | 5 +- internal/webhooks/sonarqube/webhook.go | 13 ++++- 7 files changed, 102 insertions(+), 39 deletions(-) create mode 100644 internal/clients/sonarqube_sdk/measures.go diff --git a/internal/clients/gitea_sdk/gitea_sdk.go b/internal/clients/gitea_sdk/gitea_sdk.go index 5486301..e879761 100644 --- a/internal/clients/gitea_sdk/gitea_sdk.go +++ b/internal/clients/gitea_sdk/gitea_sdk.go @@ -28,7 +28,7 @@ func (sdk *GiteaSdk) PostComment(repo settings.GiteaRepository, idx int, msg str func New() *GiteaSdk { client, err := gitea.NewClient(settings.Gitea.Url, gitea.SetToken(settings.Gitea.Token.Value)) if err != nil { - panic(fmt.Errorf("Cannot initialize Gitea client: %w", err)) + panic(fmt.Errorf("cannot initialize Gitea client: %w", err)) } return &GiteaSdk{client} diff --git a/internal/clients/sonarqube_sdk/measures.go b/internal/clients/sonarqube_sdk/measures.go new file mode 100644 index 0000000..d0d20ce --- /dev/null +++ b/internal/clients/sonarqube_sdk/measures.go @@ -0,0 +1,53 @@ +package sonarqube_sdk + +import ( + "fmt" + "strings" +) + +type period struct { + Value string `json:"value"` +} + +type MeasuresComponentMeasure struct { + Metric string `json:"metric"` + Value string `json:"value"` + Period *period `json:"period,omitempty"` +} + +type MeasuresComponentMetric struct { + Key string `json:"key"` + Name string `json:"name"` +} + +type MeasuresComponent struct { + PullRequest string `json:"pullRequest"` + Measures []MeasuresComponentMeasure `json:"measures"` +} + +type MeasuresResponse struct { + Component MeasuresComponent `json:"component"` + Metrics []MeasuresComponentMetric `json:"metrics"` +} + +func (mr *MeasuresResponse) GetRenderedMarkdownTable() string { + metricsTranslations := map[string]string{} + for _, metric := range mr.Metrics { + metricsTranslations[metric.Key] = metric.Name + } + measures := make([]string, len(mr.Component.Measures)) + for i, measure := range mr.Component.Measures { + value := measure.Value + if measure.Period != nil { + value = measure.Period.Value + } + measures[i] = fmt.Sprintf("| %s | %s |", metricsTranslations[measure.Metric], value) + } + + table := ` +| Metric | Current | +| -------- | -------- | +%s` + + return fmt.Sprintf(table, strings.Join(measures, "\n")) +} diff --git a/internal/clients/sonarqube_sdk/sonarqube_sdk.go b/internal/clients/sonarqube_sdk/sonarqube_sdk.go index b6d683b..2fd1a3a 100644 --- a/internal/clients/sonarqube_sdk/sonarqube_sdk.go +++ b/internal/clients/sonarqube_sdk/sonarqube_sdk.go @@ -2,16 +2,16 @@ package sonarqube_sdk import ( "encoding/base64" + "encoding/json" "fmt" "io" - "log" "net/http" "gitea-sonarqube-pr-bot/internal/settings" ) type SonarQubeSdkInterface interface { - GetMeasures(string, string) (string, error) + GetMeasures(string, string) (*MeasuresResponse, error) } type SonarQubeSdk struct { @@ -20,20 +20,26 @@ type SonarQubeSdk struct { token string } -func (sdk *SonarQubeSdk) GetMeasures(project string, branch string) (string, error) { +func (sdk *SonarQubeSdk) GetMeasures(project string, branch string) (*MeasuresResponse, error) { url := fmt.Sprintf("%s/api/measures/component?additionalFields=metrics&metricKeys=bugs,vulnerabilities,new_security_hotspots,violations&component=%s&pullRequest=%s", sdk.baseUrl, project, branch) - log.Println(url) - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - panic(fmt.Errorf("Cannot initialize Request: %w", err)) + return nil, fmt.Errorf("cannot initialize Request: %w", err) } req.Header.Add("Authorization", sdk.basicAuth()) - resp, _ := sdk.client.Do(req) + rawResp, _ := sdk.client.Do(req) + if rawResp.Body != nil { + defer rawResp.Body.Close() + } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) + body, _ := io.ReadAll(rawResp.Body) + response := &MeasuresResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + return nil, fmt.Errorf("cannot parse response from SonarQube: %w", err) + } - return string(body), nil + return response, nil } func (sdk *SonarQubeSdk) basicAuth() string { diff --git a/internal/webhook_handler/main.go b/internal/webhook_handler/main.go index db4c492..9977d2a 100644 --- a/internal/webhook_handler/main.go +++ b/internal/webhook_handler/main.go @@ -12,6 +12,7 @@ import ( giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" + "github.com/gorilla/mux" "github.com/urfave/cli/v2" ) diff --git a/internal/webhook_handler/sonarqube.go b/internal/webhook_handler/sonarqube.go index 439ed9d..2cb6f8a 100644 --- a/internal/webhook_handler/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -20,34 +20,23 @@ type SonarQubeWebhookHandler struct { sqSdk sqSdk.SonarQubeSdkInterface } -func (h *SonarQubeWebhookHandler) composeGiteaComment(w *webhook.Webhook) string { - a, _ := h.sqSdk.GetMeasures(w.Project.Key, w.Branch.Name) - - log.Println(a) - - status := ":white_check_mark:" - if w.QualityGate.Status != "OK" { - status = ":x:" +func (h *SonarQubeWebhookHandler) composeGiteaComment(w *webhook.Webhook) (string, error) { + m, err := h.sqSdk.GetMeasures(w.Project.Key, w.Branch.Name) + if err != nil { + return "", err } - measures := `| Metric | Current | -| -------- | -------- | -| Bugs | 123 | -| Code Smells | 1 | -| Vulnerabilities | 1 | -` + message := make([]string, 5) + message[0] = w.GetRenderedQualityGate() + message[1] = m.GetRenderedMarkdownTable() + message[2] = fmt.Sprintf("See [SonarQube](%s) for details.", w.Branch.Url) + message[3] = "---" + message[4] = "- If you want the bot to check again, post `/sqbot review`" - msg := `**Quality Gate**: %s - -**Measures** - -%s - -See [SonarQube](https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1) for details.` - return fmt.Sprintf(msg, status, measures) + return strings.Join(message, "\n\n"), nil } -func (_ *SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) (bool, int) { +func (*SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) (bool, int) { for idx, proj := range p { if proj.SonarQube.Key == n { return true, idx @@ -65,7 +54,11 @@ func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings. h.fetchDetails(w) - comment := h.composeGiteaComment(w) + comment, err := h.composeGiteaComment(w) + if err != nil { + log.Printf("Error composing Gitea comment: %s", err.Error()) + return + } h.giteaSdk.PostComment(repo, w.PRIndex, comment) } diff --git a/internal/webhook_handler/sonarqube_test.go b/internal/webhook_handler/sonarqube_test.go index dce3b3f..91c594c 100644 --- a/internal/webhook_handler/sonarqube_test.go +++ b/internal/webhook_handler/sonarqube_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "testing" + sqSDK "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" @@ -33,8 +34,8 @@ type SQSdkMock struct { mock.Mock } -func (h *SQSdkMock) GetMeasures(project string, branch string) (string, error) { - return "", nil +func (h *SQSdkMock) GetMeasures(project string, branch string) (*sqSDK.MeasuresResponse, error) { + return &sqSDK.MeasuresResponse{}, nil } func defaultMockPreparation(h *HandlerPartialMock) { diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index 4ccb3bf..d05117d 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -33,6 +33,15 @@ type Webhook struct { PRIndex int } +func (w *Webhook) GetRenderedQualityGate() string { + status := ":white_check_mark:" + if w.QualityGate.Status != "OK" { + status = ":x:" + } + + return fmt.Sprintf("**Quality Gate**: %s", status) +} + func New(raw []byte) (*Webhook, bool) { v := viper.New() v.SetConfigType("json") @@ -61,8 +70,8 @@ func parsePRIndex(w *Webhook) (int, error) { re := regexp.MustCompile(`^PR-(\d+)$`) res := re.FindSubmatch([]byte(w.Branch.Name)) if len(res) != 2 { - return 0, fmt.Errorf("Branch name '%s' does not match regex '%s'.", w.Branch.Name, re.String()) + return 0, fmt.Errorf("branch name '%s' does not match regex '%s'", w.Branch.Name, re.String()) } - return strconv.Atoi(fmt.Sprintf("%s", res[1])) + return strconv.Atoi(string(res[1])) } From d24bfdad4f4e500a1fe2926e477b3fe6cec840a4 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sat, 9 Oct 2021 20:44:09 +0200 Subject: [PATCH 037/128] Replace violations with code_smells Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/clients/sonarqube_sdk/sonarqube_sdk.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/clients/sonarqube_sdk/sonarqube_sdk.go b/internal/clients/sonarqube_sdk/sonarqube_sdk.go index 2fd1a3a..e165255 100644 --- a/internal/clients/sonarqube_sdk/sonarqube_sdk.go +++ b/internal/clients/sonarqube_sdk/sonarqube_sdk.go @@ -21,7 +21,7 @@ type SonarQubeSdk struct { } func (sdk *SonarQubeSdk) GetMeasures(project string, branch string) (*MeasuresResponse, error) { - url := fmt.Sprintf("%s/api/measures/component?additionalFields=metrics&metricKeys=bugs,vulnerabilities,new_security_hotspots,violations&component=%s&pullRequest=%s", sdk.baseUrl, project, branch) + url := fmt.Sprintf("%s/api/measures/component?additionalFields=metrics&metricKeys=bugs,vulnerabilities,new_security_hotspots,code_smells&component=%s&pullRequest=%s", sdk.baseUrl, project, branch) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("cannot initialize Request: %w", err) From 34dbd4f609fd84cffe25a8c0de12e975ff163a7b Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sat, 9 Oct 2021 20:45:21 +0200 Subject: [PATCH 038/128] Add status-check to PR/commit Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/clients/gitea_sdk/gitea_sdk.go | 22 ++++++++++++++++++++++ internal/webhook_handler/sonarqube.go | 5 +++++ 2 files changed, 27 insertions(+) diff --git a/internal/clients/gitea_sdk/gitea_sdk.go b/internal/clients/gitea_sdk/gitea_sdk.go index e879761..068bd9a 100644 --- a/internal/clients/gitea_sdk/gitea_sdk.go +++ b/internal/clients/gitea_sdk/gitea_sdk.go @@ -3,12 +3,14 @@ package gitea_sdk import ( "fmt" "gitea-sonarqube-pr-bot/internal/settings" + webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" "code.gitea.io/sdk/gitea" ) type GiteaSdkInterface interface { PostComment(settings.GiteaRepository, int, string) error + UpdateStatus(settings.GiteaRepository, *webhook.Webhook) error } type GiteaSdk struct { @@ -25,6 +27,26 @@ func (sdk *GiteaSdk) PostComment(repo settings.GiteaRepository, idx int, msg str return err } +func (sdk *GiteaSdk) UpdateStatus(repo settings.GiteaRepository, w *webhook.Webhook) error { + status := gitea.StatusPending + switch w.QualityGate.Status { + case "OK": + status = gitea.StatusSuccess + case "ERROR": + status = gitea.StatusFailure + } + opt := gitea.CreateStatusOption{ + TargetURL: w.Branch.Url, + Context: "gitea-sonarqube-pr-bot", + Description: w.QualityGate.Status, + State: status, + } + + _, _, err := sdk.client.CreateStatus(repo.Owner, repo.Name, w.Revision, opt) + + return err +} + func New() *GiteaSdk { client, err := gitea.NewClient(settings.Gitea.Url, gitea.SetToken(settings.Gitea.Token.Value)) if err != nil { diff --git a/internal/webhook_handler/sonarqube.go b/internal/webhook_handler/sonarqube.go index 2cb6f8a..e901cbf 100644 --- a/internal/webhook_handler/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -54,6 +54,11 @@ func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings. h.fetchDetails(w) + err := h.giteaSdk.UpdateStatus(repo, w) + if err != nil { + log.Printf("Error updating status: %s", err.Error()) + } + comment, err := h.composeGiteaComment(w) if err != nil { log.Printf("Error composing Gitea comment: %s", err.Error()) From c114d8ee0f7e3437e2f8f4ac5c9ca85aaebcd5ed Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sat, 9 Oct 2021 20:46:12 +0200 Subject: [PATCH 039/128] Care about documentation Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3fda7be..cc175e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ docker build -t gitea-sonarqube-pr-bot/dev -f contrib/Dockerfile contrib # Start the environment -docker run --rm -it -p 9100:8080 -v "$(pwd):/projects" gitea-sonarqube-pr-bot/dev +docker run --rm -it -p 49182:8080 -v "$(pwd):/projects" gitea-sonarqube-pr-bot/dev # Build the binary go build ./cmd/gitea-sonarqube-bot diff --git a/README.md b/README.md index 6d22846..30171d9 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ this [won't be added in near future](https://github.com/SonarSource/sonarqube/pu _Gitea SonarQube PR Bot_ aims to fill the gap between working on pull requests and being notified on quality changes. Luckily, both endpoints have a proper REST API to communicate with each others. -## Table of Contents - - [Gitea SonarQube PR Bot](#gitea-sonarqube-pr-bot) - [TODOs](#todos) - [Workflow](#workflow) @@ -19,6 +17,8 @@ Luckily, both endpoints have a proper REST API to communicate with each others. ## TODOs - [ ] Validate configuration on startup +- [ ] Verify webhook secrets +- [ ] Only post status-check (Opt-in/out) - [ ] Maybe drop `PRBOT_CONFIG_PATH` environment variable in favor of `--config path/to/config.yaml` cli attribute - [ ] Configure SonarQube PR branch naming pattern for more flexibility (currently focused on Jenkins with [Gitea Plugin](https://github.com/jenkinsci/gitea-plugin)) - [ ] Configuration live reloading From aac7f5743d77a0c93734516158d4bf417c057ee6 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sat, 9 Oct 2021 21:09:23 +0200 Subject: [PATCH 040/128] Fix tests Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/webhook_handler/sonarqube_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/webhook_handler/sonarqube_test.go b/internal/webhook_handler/sonarqube_test.go index 91c594c..631936a 100644 --- a/internal/webhook_handler/sonarqube_test.go +++ b/internal/webhook_handler/sonarqube_test.go @@ -30,6 +30,10 @@ func (h *GiteaSdkMock) PostComment(_ settings.GiteaRepository, _ int, _ string) return nil } +func (h *GiteaSdkMock) UpdateStatus(_ settings.GiteaRepository, w *webhook.Webhook) error { + return nil +} + type SQSdkMock struct { mock.Mock } From 45fbfed51ba1c09e3bad9573ea9827e748f0d8a7 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sat, 9 Oct 2021 21:10:30 +0200 Subject: [PATCH 041/128] Split settings structs into separate files Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/settings/gitea.go | 12 ++++++++++++ internal/settings/project.go | 8 ++++++++ internal/settings/settings.go | 24 ------------------------ internal/settings/sonarqube.go | 7 +++++++ 4 files changed, 27 insertions(+), 24 deletions(-) create mode 100644 internal/settings/gitea.go create mode 100644 internal/settings/project.go create mode 100644 internal/settings/sonarqube.go diff --git a/internal/settings/gitea.go b/internal/settings/gitea.go new file mode 100644 index 0000000..375efcc --- /dev/null +++ b/internal/settings/gitea.go @@ -0,0 +1,12 @@ +package settings + +type GiteaRepository struct { + Owner string + Name string +} + +type giteaConfig struct { + Url string + Token *token + Webhook *webhook +} diff --git a/internal/settings/project.go b/internal/settings/project.go new file mode 100644 index 0000000..9be2cd9 --- /dev/null +++ b/internal/settings/project.go @@ -0,0 +1,8 @@ +package settings + +type Project struct { + SonarQube struct { + Key string + } `mapstructure:"sonarqube"` + Gitea GiteaRepository +} diff --git a/internal/settings/settings.go b/internal/settings/settings.go index bb031a6..44f9850 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -7,30 +7,6 @@ import ( "github.com/spf13/viper" ) -type GiteaRepository struct { - Owner string - Name string -} - -type giteaConfig struct { - Url string - Token *token - Webhook *webhook -} - -type sonarQubeConfig struct { - Url string - Token *token - Webhook *webhook -} - -type Project struct { - SonarQube struct { - Key string - } `mapstructure:"sonarqube"` - Gitea GiteaRepository -} - var ( Gitea giteaConfig SonarQube sonarQubeConfig diff --git a/internal/settings/sonarqube.go b/internal/settings/sonarqube.go new file mode 100644 index 0000000..b7ef864 --- /dev/null +++ b/internal/settings/sonarqube.go @@ -0,0 +1,7 @@ +package settings + +type sonarQubeConfig struct { + Url string + Token *token + Webhook *webhook +} From 758e3c75cc33c5f0fcf4477ea5ef48970f034345 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sat, 9 Oct 2021 21:17:54 +0200 Subject: [PATCH 042/128] Switch to another default port Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- contrib/Dockerfile | 2 +- internal/webhook_handler/main.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc175e2..5c7ff03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ docker build -t gitea-sonarqube-pr-bot/dev -f contrib/Dockerfile contrib # Start the environment -docker run --rm -it -p 49182:8080 -v "$(pwd):/projects" gitea-sonarqube-pr-bot/dev +docker run --rm -it -p 49182:3000 -v "$(pwd):/projects" gitea-sonarqube-pr-bot/dev # Build the binary go build ./cmd/gitea-sonarqube-bot diff --git a/contrib/Dockerfile b/contrib/Dockerfile index c418967..bcc691f 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -5,6 +5,6 @@ RUN apk --no-cache add build-base git bash WORKDIR /projects VOLUME ["/projects"] -EXPOSE 8080 +EXPOSE 3000 CMD ["bash"] diff --git a/internal/webhook_handler/main.go b/internal/webhook_handler/main.go index 9977d2a..3fcab76 100644 --- a/internal/webhook_handler/main.go +++ b/internal/webhook_handler/main.go @@ -28,7 +28,7 @@ func Serve(c *cli.Context) error { r.HandleFunc("/hooks/sonarqube", NewSonarQubeWebhookHandler(giteaSdk.New(), sqSdk.New()).Handle).Methods("POST").Headers("X-SonarQube-Project", "") srv := &http.Server{ - Addr: "0.0.0.0:8080", + Addr: "0.0.0.0:3000", // Good practice to set timeouts to avoid Slowloris attacks. WriteTimeout: time.Second * 15, ReadTimeout: time.Second * 15, @@ -37,7 +37,7 @@ func Serve(c *cli.Context) error { } go func() { - log.Println("Listen on :8080") + log.Println("Listen on :3000") if err := srv.ListenAndServe(); err != nil { log.Println(err) } From 7c9fec06f9a9239625df2c4c17762be3d9d92d7f Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sat, 9 Oct 2021 21:26:47 +0200 Subject: [PATCH 043/128] Bump go version to 1.17 Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- contrib/Dockerfile | 2 +- go.mod | 27 +++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/contrib/Dockerfile b/contrib/Dockerfile index bcc691f..8d06f64 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.16-alpine3.13 +FROM golang:1.17-alpine3.14 RUN apk --no-cache add build-base git bash diff --git a/go.mod b/go.mod index 5c8b698..3ded67e 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,35 @@ module gitea-sonarqube-pr-bot -go 1.16 +go 1.17 require ( code.gitea.io/sdk/gitea v0.15.0 - github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/gorilla/mux v1.8.0 - github.com/hashicorp/go-version v1.3.0 // indirect github.com/spf13/viper v1.9.0 - github.com/stretchr/objx v0.3.0 // indirect github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/hashicorp/go-version v1.3.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.5 // indirect + github.com/mitchellh/mapstructure v1.4.2 // indirect + github.com/pelletier/go-toml v1.9.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/afero v1.6.0 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.3.0 // indirect + github.com/subosito/gotenv v1.2.0 // indirect golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c // indirect golang.org/x/text v0.3.7 // indirect + gopkg.in/ini.v1 v1.63.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) From 2873eb51595df8d4c7dc72c73ea0c7cb0734b1cc Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sat, 9 Oct 2021 21:48:51 +0200 Subject: [PATCH 044/128] Switch to gin-gonic as server Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- go.mod | 16 ++++++- go.sum | 32 +++++++++++++- internal/webhook_handler/main.go | 74 ++++++++++++++------------------ 3 files changed, 77 insertions(+), 45 deletions(-) diff --git a/go.mod b/go.mod index 3ded67e..7b7d237 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,8 @@ go 1.17 require ( code.gitea.io/sdk/gitea v0.15.0 - github.com/gorilla/mux v1.8.0 + github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 + github.com/gin-gonic/gin v1.7.4 github.com/spf13/viper v1.9.0 github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 @@ -14,10 +15,20 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/go-playground/validator/v10 v10.4.1 // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/go-version v1.3.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/leodido/go-urn v1.2.0 // indirect github.com/magiconair/properties v1.8.5 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect github.com/mitchellh/mapstructure v1.4.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -27,8 +38,11 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.3.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect + github.com/ugorji/go/codec v1.1.7 // indirect + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c // indirect golang.org/x/text v0.3.7 // indirect + google.golang.org/protobuf v1.27.1 // indirect gopkg.in/ini.v1 v1.63.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect diff --git a/go.sum b/go.sum index c82a082..4bdbd39 100644 --- a/go.sum +++ b/go.sum @@ -84,10 +84,24 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc= +github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= +github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -119,6 +133,7 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -134,6 +149,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -159,8 +175,6 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= @@ -189,6 +203,8 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -201,6 +217,8 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -210,6 +228,7 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= @@ -220,8 +239,10 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= @@ -265,6 +286,10 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -294,6 +319,7 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -522,6 +548,7 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -647,6 +674,7 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/webhook_handler/main.go b/internal/webhook_handler/main.go index 3fcab76..4f82395 100644 --- a/internal/webhook_handler/main.go +++ b/internal/webhook_handler/main.go @@ -1,60 +1,50 @@ package webhook_handler import ( - "context" - "flag" "fmt" - "log" "net/http" - "os" - "os/signal" - "time" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" - "github.com/gorilla/mux" + "github.com/fvbock/endless" + "github.com/gin-gonic/gin" "github.com/urfave/cli/v2" ) +func addPingApi(r *gin.Engine) { + r.GET("/ping", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) + }) +} + +type validSonarQubeEndpointHeader struct { + SonarQubeProject string `header:"X-SonarQube-Project"` +} + +func addSonarQubeEndpoint(r *gin.Engine) { + webhookHandler := NewSonarQubeWebhookHandler(giteaSdk.New(), sqSdk.New()) + r.POST("/hooks/sonarqube", func(c *gin.Context) { + h := validSonarQubeEndpointHeader{} + + if err := c.ShouldBindHeader(&h); err != nil { + c.Status(http.StatusNotFound) + return + } + + webhookHandler.Handle(c.Writer, c.Request) + }) +} + func Serve(c *cli.Context) error { fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") - var wait time.Duration - flag.DurationVar(&wait, "graceful-timeout", time.Second*15, "the duration for which the server gracefully wait for existing connections to finish") - flag.Parse() + r := gin.Default() - r := mux.NewRouter() - r.HandleFunc("/hooks/sonarqube", NewSonarQubeWebhookHandler(giteaSdk.New(), sqSdk.New()).Handle).Methods("POST").Headers("X-SonarQube-Project", "") + addPingApi(r) + addSonarQubeEndpoint(r) - srv := &http.Server{ - Addr: "0.0.0.0:3000", - // Good practice to set timeouts to avoid Slowloris attacks. - WriteTimeout: time.Second * 15, - ReadTimeout: time.Second * 15, - IdleTimeout: time.Second * 60, - Handler: r, - } - - go func() { - log.Println("Listen on :3000") - if err := srv.ListenAndServe(); err != nil { - log.Println(err) - } - }() - - ch := make(chan os.Signal, 1) - signal.Notify(ch, os.Interrupt) - - // Block until we receive our signal. - <-ch - - // Create a deadline to wait for. - ctx, cancel := context.WithTimeout(context.Background(), wait) - defer cancel() - srv.Shutdown(ctx) - log.Println("Shutting down webhook server") - os.Exit(0) - - return nil + return endless.ListenAndServe(":3000", r) } From 8458815efe8c7236893bd0665471fb8608579774 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sat, 9 Oct 2021 21:50:58 +0200 Subject: [PATCH 045/128] Update dependencies Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- README.md | 2 +- go.mod | 22 +++++++++++----------- go.sum | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 30171d9..8d4b7cd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Gitea SonarQube PR Bot -_Gitea SonarQube PR Bot_ is (obviously) a bot that receives messages from both SonarQube and Gitea to help developers +_Gitea SonarQube PR Bot_ is a bot that receives messages from both SonarQube and Gitea to help developers being productive. The idea behind this project is the missing ALM integration of Gitea in SonarQube. Unfortunately, this [won't be added in near future](https://github.com/SonarSource/sonarqube/pull/3248#issuecomment-701334327). _Gitea SonarQube PR Bot_ aims to fill the gap between working on pull requests and being notified on quality changes. diff --git a/go.mod b/go.mod index 7b7d237..d909abc 100644 --- a/go.mod +++ b/go.mod @@ -16,19 +16,19 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.13.0 // indirect - github.com/go-playground/universal-translator v0.17.0 // indirect - github.com/go-playground/validator/v10 v10.4.1 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.9.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/go-version v1.3.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/json-iterator/go v1.1.11 // indirect - github.com/leodido/go-urn v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/leodido/go-urn v1.2.1 // indirect github.com/magiconair/properties v1.8.5 // indirect - github.com/mattn/go-isatty v0.0.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/mapstructure v1.4.2 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -38,9 +38,9 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.3.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect - github.com/ugorji/go/codec v1.1.7 // indirect - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect - golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c // indirect + github.com/ugorji/go/codec v1.2.6 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/ini.v1 v1.63.2 // indirect diff --git a/go.sum b/go.sum index 4bdbd39..d896752 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,7 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -98,10 +99,16 @@ github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBY github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A= +github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -206,6 +213,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -214,11 +223,18 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -230,6 +246,8 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= @@ -241,12 +259,17 @@ github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7p github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -256,6 +279,8 @@ github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSg github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -288,8 +313,12 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= +github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= +github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -319,8 +348,11 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -476,6 +508,8 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c h1:EyJTLQbOxvk8V6oDdD8ILR1BOs3nEJXThD6aqsiPNkM= golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -680,6 +714,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c= gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= From 3bab05bb71bfdddc31d8932c2fb486da9b654042 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 13:07:10 +0200 Subject: [PATCH 046/128] Fix required header for SonarQube endpoint Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/webhook_handler/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/webhook_handler/main.go b/internal/webhook_handler/main.go index 4f82395..9e27466 100644 --- a/internal/webhook_handler/main.go +++ b/internal/webhook_handler/main.go @@ -21,7 +21,7 @@ func addPingApi(r *gin.Engine) { } type validSonarQubeEndpointHeader struct { - SonarQubeProject string `header:"X-SonarQube-Project"` + SonarQubeProject string `header:"X-SonarQube-Project" binding:"required"` } func addSonarQubeEndpoint(r *gin.Engine) { From 57fc8054b2ffc27462ee6dcd435c3851b86cd839 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 15:58:03 +0200 Subject: [PATCH 047/128] Bot listens to actions via Gitea comments Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/clients/gitea_sdk/gitea_sdk.go | 32 +++++--- internal/webhook_handler/gitea.go | 60 ++++++++++++++ internal/webhook_handler/main.go | 8 ++ internal/webhook_handler/sonarqube.go | 19 +++-- internal/webhook_handler/sonarqube_test.go | 7 +- internal/webhooks/gitea/webhook.go | 96 ++++++++++++++++++++++ 6 files changed, 203 insertions(+), 19 deletions(-) create mode 100644 internal/webhook_handler/gitea.go create mode 100644 internal/webhooks/gitea/webhook.go diff --git a/internal/clients/gitea_sdk/gitea_sdk.go b/internal/clients/gitea_sdk/gitea_sdk.go index 068bd9a..1b566c9 100644 --- a/internal/clients/gitea_sdk/gitea_sdk.go +++ b/internal/clients/gitea_sdk/gitea_sdk.go @@ -3,14 +3,15 @@ package gitea_sdk import ( "fmt" "gitea-sonarqube-pr-bot/internal/settings" - webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" + "log" "code.gitea.io/sdk/gitea" ) type GiteaSdkInterface interface { PostComment(settings.GiteaRepository, int, string) error - UpdateStatus(settings.GiteaRepository, *webhook.Webhook) error + UpdateStatus(settings.GiteaRepository, string, string, string, gitea.StatusState) error + DetermineHEAD(settings.GiteaRepository, int64) (string, error) } type GiteaSdk struct { @@ -27,26 +28,31 @@ func (sdk *GiteaSdk) PostComment(repo settings.GiteaRepository, idx int, msg str return err } -func (sdk *GiteaSdk) UpdateStatus(repo settings.GiteaRepository, w *webhook.Webhook) error { - status := gitea.StatusPending - switch w.QualityGate.Status { - case "OK": - status = gitea.StatusSuccess - case "ERROR": - status = gitea.StatusFailure - } +func (sdk *GiteaSdk) UpdateStatus(repo settings.GiteaRepository, ref string, targetUrl string, description string, status gitea.StatusState) error { opt := gitea.CreateStatusOption{ - TargetURL: w.Branch.Url, + TargetURL: targetUrl, Context: "gitea-sonarqube-pr-bot", - Description: w.QualityGate.Status, + Description: description, State: status, } - _, _, err := sdk.client.CreateStatus(repo.Owner, repo.Name, w.Revision, opt) + _, _, err := sdk.client.CreateStatus(repo.Owner, repo.Name, ref, opt) + if err != nil { + log.Printf("Error updating status: %s", err.Error()) + } return err } +func (sdk *GiteaSdk) DetermineHEAD(repo settings.GiteaRepository, idx int64) (string, error) { + pr, _, err := sdk.client.GetPullRequest(repo.Owner, repo.Name, idx) + if err != nil { + return "", err + } + + return pr.Head.Sha, nil +} + func New() *GiteaSdk { client, err := gitea.NewClient(settings.Gitea.Url, gitea.SetToken(settings.Gitea.Token.Value)) if err != nil { diff --git a/internal/webhook_handler/gitea.go b/internal/webhook_handler/gitea.go new file mode 100644 index 0000000..60737cf --- /dev/null +++ b/internal/webhook_handler/gitea.go @@ -0,0 +1,60 @@ +package webhook_handler + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" + webhook "gitea-sonarqube-pr-bot/internal/webhooks/gitea" +) + +type GiteaWebhookHandler struct { + giteaSdk giteaSdk.GiteaSdkInterface + sqSdk sqSdk.SonarQubeSdkInterface +} + +func (h *GiteaWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + + if r.Body != nil { + defer r.Body.Close() + } + + raw, err := ioutil.ReadAll(r.Body) + + if err != nil { + log.Printf("Error reading request body %s", err.Error()) + rw.WriteHeader(http.StatusInternalServerError) + io.WriteString(rw, fmt.Sprintf(`{"message": "%s"}`, err.Error())) + return + } + + w, ok := webhook.New(raw) + if !ok { + rw.WriteHeader(http.StatusUnprocessableEntity) + io.WriteString(rw, `{"message": "Error parsing POST body."}`) + return + } + + if err := w.Validate(); err != nil { + rw.WriteHeader(http.StatusOK) + io.WriteString(rw, fmt.Sprintf(`{"message": "%s"}`, err.Error())) + return + } + + rw.WriteHeader(http.StatusOK) + io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`) + + w.ProcessData(h.giteaSdk, h.sqSdk) +} + +func NewGiteaWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) *GiteaWebhookHandler { + return &GiteaWebhookHandler{ + giteaSdk: g, + sqSdk: sq, + } +} diff --git a/internal/webhook_handler/main.go b/internal/webhook_handler/main.go index 9e27466..873a117 100644 --- a/internal/webhook_handler/main.go +++ b/internal/webhook_handler/main.go @@ -38,6 +38,13 @@ func addSonarQubeEndpoint(r *gin.Engine) { }) } +func addGiteaEndpoint(r *gin.Engine) { + webhookHandler := NewGiteaWebhookHandler(giteaSdk.New(), sqSdk.New()) + r.POST("/hooks/gitea", func(c *gin.Context) { + webhookHandler.Handle(c.Writer, c.Request) + }) +} + func Serve(c *cli.Context) error { fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") @@ -45,6 +52,7 @@ func Serve(c *cli.Context) error { addPingApi(r) addSonarQubeEndpoint(r) + addGiteaEndpoint(r) return endless.ListenAndServe(":3000", r) } diff --git a/internal/webhook_handler/sonarqube.go b/internal/webhook_handler/sonarqube.go index e901cbf..723a365 100644 --- a/internal/webhook_handler/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -12,6 +12,8 @@ import ( sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" + + "code.gitea.io/sdk/gitea" ) type SonarQubeWebhookHandler struct { @@ -31,7 +33,7 @@ func (h *SonarQubeWebhookHandler) composeGiteaComment(w *webhook.Webhook) (strin message[1] = m.GetRenderedMarkdownTable() message[2] = fmt.Sprintf("See [SonarQube](%s) for details.", w.Branch.Url) message[3] = "---" - message[4] = "- If you want the bot to check again, post `/sqbot review`" + message[4] = "- If you want the bot to check again, post `/sq-bot review`" return strings.Join(message, "\n\n"), nil } @@ -54,10 +56,14 @@ func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings. h.fetchDetails(w) - err := h.giteaSdk.UpdateStatus(repo, w) - if err != nil { - log.Printf("Error updating status: %s", err.Error()) + status := gitea.StatusPending + switch w.QualityGate.Status { + case "OK": + status = gitea.StatusSuccess + case "ERROR": + status = gitea.StatusFailure } + _ = h.giteaSdk.UpdateStatus(repo, w.Revision, w.Branch.Url, w.QualityGate.Status, status) comment, err := h.composeGiteaComment(w) if err != nil { @@ -82,8 +88,11 @@ func (h *SonarQubeWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request log.Printf("Received hook for project '%s'. Processing data.", projectName) + if r.Body != nil { + defer r.Body.Close() + } + raw, err := ioutil.ReadAll(r.Body) - defer r.Body.Close() if err != nil { log.Printf("Error reading request body %s", err.Error()) rw.WriteHeader(http.StatusInternalServerError) diff --git a/internal/webhook_handler/sonarqube_test.go b/internal/webhook_handler/sonarqube_test.go index 631936a..b403277 100644 --- a/internal/webhook_handler/sonarqube_test.go +++ b/internal/webhook_handler/sonarqube_test.go @@ -10,6 +10,7 @@ import ( "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" + "code.gitea.io/sdk/gitea" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -30,7 +31,11 @@ func (h *GiteaSdkMock) PostComment(_ settings.GiteaRepository, _ int, _ string) return nil } -func (h *GiteaSdkMock) UpdateStatus(_ settings.GiteaRepository, w *webhook.Webhook) error { +func (h *GiteaSdkMock) DetermineHEAD(_ settings.GiteaRepository, _ int64) (string, error) { + return "", nil +} + +func (h *GiteaSdkMock) UpdateStatus(_ settings.GiteaRepository, _ string, _ string, _ string, _ gitea.StatusState) error { return nil } diff --git a/internal/webhooks/gitea/webhook.go b/internal/webhooks/gitea/webhook.go new file mode 100644 index 0000000..addfddf --- /dev/null +++ b/internal/webhooks/gitea/webhook.go @@ -0,0 +1,96 @@ +package gitea + +import ( + "encoding/json" + "fmt" + "log" + "strings" + + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" + "gitea-sonarqube-pr-bot/internal/settings" + + "code.gitea.io/sdk/gitea" +) + +type BotAction string + +const ( + ActionReview BotAction = "/pr-bot review" +) + +type issue struct { + Number int64 `json:"number"` + Repository settings.GiteaRepository `json:"repository"` +} + +type comment struct { + Body string `json:"body"` +} + +type Webhook struct { + Action string `json:"action"` + IsPR bool `json:"is_pull"` + Issue issue `json:"issue"` + Comment comment `json:"comment"` + ConfiguredProject settings.Project +} + +func (w *Webhook) inProjectsMapping(p []settings.Project) (bool, int) { + owner := w.Issue.Repository.Owner + name := w.Issue.Repository.Name + for idx, proj := range p { + if proj.Gitea.Owner == owner && proj.Gitea.Name == name { + return true, idx + } + } + + return false, 0 +} + +func (w *Webhook) Validate() error { + if !w.IsPR { + return fmt.Errorf("ignore non-PR hook") + } + + found, pIdx := w.inProjectsMapping(settings.Projects) + if !found { + return fmt.Errorf("ignore hook for non-configured project '%s/%s'", w.Issue.Repository.Owner, w.Issue.Repository.Name) + } + + if w.Action != "created" { + return fmt.Errorf("ignore hook for action others than created") + } + + if !strings.HasPrefix(w.Comment.Body, "/pr-bot") { + return fmt.Errorf("ignore hook for non-bot action comment") + } + + w.ConfiguredProject = settings.Projects[pIdx] + + return nil +} + +func (w *Webhook) ProcessData(gSDK giteaSdk.GiteaSdkInterface, sqSDK sqSdk.SonarQubeSdkInterface) { + headRef, err := gSDK.DetermineHEAD(w.ConfiguredProject.Gitea, w.Issue.Number) + if err != nil { + log.Printf("Error retrieving HEAD ref: %s", err.Error()) + return + } + _ = gSDK.UpdateStatus(w.ConfiguredProject.Gitea, headRef, "", "Analysis pending...", gitea.StatusPending) + + log.Printf("Fetching SonarQube data...") + + _ = gSDK.UpdateStatus(w.ConfiguredProject.Gitea, headRef, "", "OK", gitea.StatusSuccess) +} + +func New(raw []byte) (*Webhook, bool) { + w := &Webhook{} + err := json.Unmarshal(raw, &w) + if err != nil { + log.Printf("Error parsing Gitea webhook: %s", err.Error()) + return w, false + } + + return w, true +} From e20b1469d36f8d4f19ab4c1467cee59b5b32fc66 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 16:14:09 +0200 Subject: [PATCH 048/128] Reduce `UpdateStatus` parameter mess Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/clients/gitea_sdk/gitea_sdk.go | 12 +++++++----- internal/clients/gitea_sdk/status.go | 16 ++++++++++++++++ internal/webhook_handler/sonarqube.go | 17 ++++++++--------- internal/webhook_handler/sonarqube_test.go | 4 ++-- internal/webhooks/gitea/webhook.go | 10 +++++----- 5 files changed, 38 insertions(+), 21 deletions(-) create mode 100644 internal/clients/gitea_sdk/status.go diff --git a/internal/clients/gitea_sdk/gitea_sdk.go b/internal/clients/gitea_sdk/gitea_sdk.go index 1b566c9..c0f1d77 100644 --- a/internal/clients/gitea_sdk/gitea_sdk.go +++ b/internal/clients/gitea_sdk/gitea_sdk.go @@ -10,7 +10,7 @@ import ( type GiteaSdkInterface interface { PostComment(settings.GiteaRepository, int, string) error - UpdateStatus(settings.GiteaRepository, string, string, string, gitea.StatusState) error + UpdateStatus(settings.GiteaRepository, string, StatusDetails) error DetermineHEAD(settings.GiteaRepository, int64) (string, error) } @@ -28,14 +28,16 @@ func (sdk *GiteaSdk) PostComment(repo settings.GiteaRepository, idx int, msg str return err } -func (sdk *GiteaSdk) UpdateStatus(repo settings.GiteaRepository, ref string, targetUrl string, description string, status gitea.StatusState) error { +func (sdk *GiteaSdk) UpdateStatus(repo settings.GiteaRepository, ref string, details StatusDetails) error { opt := gitea.CreateStatusOption{ - TargetURL: targetUrl, + TargetURL: details.Url, Context: "gitea-sonarqube-pr-bot", - Description: description, - State: status, + Description: details.Message, + State: gitea.StatusState(details.State), } + opt.TargetURL = "gitea-sonarqube-pr-bot" + _, _, err := sdk.client.CreateStatus(repo.Owner, repo.Name, ref, opt) if err != nil { log.Printf("Error updating status: %s", err.Error()) diff --git a/internal/clients/gitea_sdk/status.go b/internal/clients/gitea_sdk/status.go new file mode 100644 index 0000000..c1bdb07 --- /dev/null +++ b/internal/clients/gitea_sdk/status.go @@ -0,0 +1,16 @@ +package gitea_sdk + +import "code.gitea.io/sdk/gitea" + +type State gitea.StatusState + +const ( + StatusOK State = State(gitea.StatusSuccess) + StatusFailure State = State(gitea.StatusFailure) +) + +type StatusDetails struct { + Url string + Message string + State State +} diff --git a/internal/webhook_handler/sonarqube.go b/internal/webhook_handler/sonarqube.go index 723a365..b893df2 100644 --- a/internal/webhook_handler/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -12,8 +12,6 @@ import ( sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" - - "code.gitea.io/sdk/gitea" ) type SonarQubeWebhookHandler struct { @@ -56,14 +54,15 @@ func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings. h.fetchDetails(w) - status := gitea.StatusPending - switch w.QualityGate.Status { - case "OK": - status = gitea.StatusSuccess - case "ERROR": - status = gitea.StatusFailure + status := giteaSdk.StatusOK + if w.QualityGate.Status != "OK" { + status = giteaSdk.StatusFailure } - _ = h.giteaSdk.UpdateStatus(repo, w.Revision, w.Branch.Url, w.QualityGate.Status, status) + _ = h.giteaSdk.UpdateStatus(repo, w.Revision, giteaSdk.StatusDetails{ + Url: w.Branch.Url, + Message: w.QualityGate.Status, + State: status, + }) comment, err := h.composeGiteaComment(w) if err != nil { diff --git a/internal/webhook_handler/sonarqube_test.go b/internal/webhook_handler/sonarqube_test.go index b403277..82648a3 100644 --- a/internal/webhook_handler/sonarqube_test.go +++ b/internal/webhook_handler/sonarqube_test.go @@ -6,11 +6,11 @@ import ( "net/http/httptest" "testing" + giteaSDK "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" sqSDK "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" - "code.gitea.io/sdk/gitea" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -35,7 +35,7 @@ func (h *GiteaSdkMock) DetermineHEAD(_ settings.GiteaRepository, _ int64) (strin return "", nil } -func (h *GiteaSdkMock) UpdateStatus(_ settings.GiteaRepository, _ string, _ string, _ string, _ gitea.StatusState) error { +func (h *GiteaSdkMock) UpdateStatus(_ settings.GiteaRepository, _ string, _ giteaSDK.StatusDetails) error { return nil } diff --git a/internal/webhooks/gitea/webhook.go b/internal/webhooks/gitea/webhook.go index addfddf..3e7212c 100644 --- a/internal/webhooks/gitea/webhook.go +++ b/internal/webhooks/gitea/webhook.go @@ -9,8 +9,6 @@ import ( giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "gitea-sonarqube-pr-bot/internal/settings" - - "code.gitea.io/sdk/gitea" ) type BotAction string @@ -77,11 +75,13 @@ func (w *Webhook) ProcessData(gSDK giteaSdk.GiteaSdkInterface, sqSDK sqSdk.Sonar log.Printf("Error retrieving HEAD ref: %s", err.Error()) return } - _ = gSDK.UpdateStatus(w.ConfiguredProject.Gitea, headRef, "", "Analysis pending...", gitea.StatusPending) - log.Printf("Fetching SonarQube data...") - _ = gSDK.UpdateStatus(w.ConfiguredProject.Gitea, headRef, "", "OK", gitea.StatusSuccess) + _ = gSDK.UpdateStatus(w.ConfiguredProject.Gitea, headRef, giteaSdk.StatusDetails{ + Url: "", + Message: "OK", + State: giteaSdk.StatusOK, + }) } func New(raw []byte) (*Webhook, bool) { From 46c5ab2aecfaf813523bdb654aaa1c58d67b09bf Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 16:18:14 +0200 Subject: [PATCH 049/128] Rename webhook_handler to api Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- cmd/gitea-sonarqube-bot/main.go | 5 +++-- internal/{webhook_handler => api}/gitea.go | 2 +- internal/{webhook_handler => api}/main.go | 2 +- internal/{webhook_handler => api}/main_test.go | 2 +- internal/{webhook_handler => api}/sonarqube.go | 2 +- internal/{webhook_handler => api}/sonarqube_test.go | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) rename internal/{webhook_handler => api}/gitea.go (98%) rename internal/{webhook_handler => api}/main.go (98%) rename internal/{webhook_handler => api}/main_test.go (86%) rename internal/{webhook_handler => api}/sonarqube.go (99%) rename internal/{webhook_handler => api}/sonarqube_test.go (99%) diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index 2d8fcd1..8c08d20 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -5,8 +5,9 @@ import ( "os" "path" + "gitea-sonarqube-pr-bot/internal/api" "gitea-sonarqube-pr-bot/internal/settings" - handler "gitea-sonarqube-pr-bot/internal/webhook_handler" + "github.com/urfave/cli/v2" ) @@ -26,7 +27,7 @@ func main() { Name: "gitea-sonarqube-pr-bot", Usage: "Improve your experience with SonarQube and Gitea", Description: `By default, gitea-sonarqube-pr-bot will start running the webserver if no arguments are passed.`, - Action: handler.Serve, + Action: api.Serve, } err := app.Run(os.Args) diff --git a/internal/webhook_handler/gitea.go b/internal/api/gitea.go similarity index 98% rename from internal/webhook_handler/gitea.go rename to internal/api/gitea.go index 60737cf..ea905bd 100644 --- a/internal/webhook_handler/gitea.go +++ b/internal/api/gitea.go @@ -1,4 +1,4 @@ -package webhook_handler +package api import ( "fmt" diff --git a/internal/webhook_handler/main.go b/internal/api/main.go similarity index 98% rename from internal/webhook_handler/main.go rename to internal/api/main.go index 873a117..49c689c 100644 --- a/internal/webhook_handler/main.go +++ b/internal/api/main.go @@ -1,4 +1,4 @@ -package webhook_handler +package api import ( "fmt" diff --git a/internal/webhook_handler/main_test.go b/internal/api/main_test.go similarity index 86% rename from internal/webhook_handler/main_test.go rename to internal/api/main_test.go index 04d9c4e..c950486 100644 --- a/internal/webhook_handler/main_test.go +++ b/internal/api/main_test.go @@ -1,4 +1,4 @@ -package webhook_handler +package api import ( "io/ioutil" diff --git a/internal/webhook_handler/sonarqube.go b/internal/api/sonarqube.go similarity index 99% rename from internal/webhook_handler/sonarqube.go rename to internal/api/sonarqube.go index b893df2..06e785f 100644 --- a/internal/webhook_handler/sonarqube.go +++ b/internal/api/sonarqube.go @@ -1,4 +1,4 @@ -package webhook_handler +package api import ( "fmt" diff --git a/internal/webhook_handler/sonarqube_test.go b/internal/api/sonarqube_test.go similarity index 99% rename from internal/webhook_handler/sonarqube_test.go rename to internal/api/sonarqube_test.go index 82648a3..0e0c941 100644 --- a/internal/webhook_handler/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -1,4 +1,4 @@ -package webhook_handler +package api import ( "bytes" From c6bc0d71ff875e20c56d37cf057b8cd1e22dee95 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 16:28:59 +0200 Subject: [PATCH 050/128] Move gitea sdk files Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/api/gitea.go | 2 +- internal/api/main.go | 2 +- internal/api/sonarqube.go | 2 +- internal/api/sonarqube_test.go | 4 ++-- internal/clients/{gitea_sdk/gitea_sdk.go => gitea/gitea.go} | 2 +- internal/clients/{gitea_sdk => gitea}/status.go | 2 +- internal/webhooks/gitea/webhook.go | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) rename internal/clients/{gitea_sdk/gitea_sdk.go => gitea/gitea.go} (98%) rename internal/clients/{gitea_sdk => gitea}/status.go (93%) diff --git a/internal/api/gitea.go b/internal/api/gitea.go index ea905bd..80cc13f 100644 --- a/internal/api/gitea.go +++ b/internal/api/gitea.go @@ -7,7 +7,7 @@ import ( "log" "net/http" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" webhook "gitea-sonarqube-pr-bot/internal/webhooks/gitea" ) diff --git a/internal/api/main.go b/internal/api/main.go index 49c689c..c860a4d 100644 --- a/internal/api/main.go +++ b/internal/api/main.go @@ -4,7 +4,7 @@ import ( "fmt" "net/http" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "github.com/fvbock/endless" diff --git a/internal/api/sonarqube.go b/internal/api/sonarqube.go index 06e785f..aab2450 100644 --- a/internal/api/sonarqube.go +++ b/internal/api/sonarqube.go @@ -8,7 +8,7 @@ import ( "net/http" "strings" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index 0e0c941..aabab91 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "testing" - giteaSDK "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" sqSDK "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" @@ -35,7 +35,7 @@ func (h *GiteaSdkMock) DetermineHEAD(_ settings.GiteaRepository, _ int64) (strin return "", nil } -func (h *GiteaSdkMock) UpdateStatus(_ settings.GiteaRepository, _ string, _ giteaSDK.StatusDetails) error { +func (h *GiteaSdkMock) UpdateStatus(_ settings.GiteaRepository, _ string, _ giteaSdk.StatusDetails) error { return nil } diff --git a/internal/clients/gitea_sdk/gitea_sdk.go b/internal/clients/gitea/gitea.go similarity index 98% rename from internal/clients/gitea_sdk/gitea_sdk.go rename to internal/clients/gitea/gitea.go index c0f1d77..e8d345e 100644 --- a/internal/clients/gitea_sdk/gitea_sdk.go +++ b/internal/clients/gitea/gitea.go @@ -1,4 +1,4 @@ -package gitea_sdk +package gitea import ( "fmt" diff --git a/internal/clients/gitea_sdk/status.go b/internal/clients/gitea/status.go similarity index 93% rename from internal/clients/gitea_sdk/status.go rename to internal/clients/gitea/status.go index c1bdb07..68dff31 100644 --- a/internal/clients/gitea_sdk/status.go +++ b/internal/clients/gitea/status.go @@ -1,4 +1,4 @@ -package gitea_sdk +package gitea import "code.gitea.io/sdk/gitea" diff --git a/internal/webhooks/gitea/webhook.go b/internal/webhooks/gitea/webhook.go index 3e7212c..450e2bc 100644 --- a/internal/webhooks/gitea/webhook.go +++ b/internal/webhooks/gitea/webhook.go @@ -6,7 +6,7 @@ import ( "log" "strings" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "gitea-sonarqube-pr-bot/internal/settings" ) From c99925abb3a25f1bfd8708817e24cf32913a8a37 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 16:29:29 +0200 Subject: [PATCH 051/128] Fix URL for status Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/clients/gitea/gitea.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/clients/gitea/gitea.go b/internal/clients/gitea/gitea.go index e8d345e..29dca72 100644 --- a/internal/clients/gitea/gitea.go +++ b/internal/clients/gitea/gitea.go @@ -36,8 +36,6 @@ func (sdk *GiteaSdk) UpdateStatus(repo settings.GiteaRepository, ref string, det State: gitea.StatusState(details.State), } - opt.TargetURL = "gitea-sonarqube-pr-bot" - _, _, err := sdk.client.CreateStatus(repo.Owner, repo.Name, ref, opt) if err != nil { log.Printf("Error updating status: %s", err.Error()) From bf453c6c43307f17459efa00aec49eb9cb6ef224 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 16:39:26 +0200 Subject: [PATCH 052/128] Centralize bot actions Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/actions/actions.go | 8 ++++++++ internal/api/sonarqube.go | 3 ++- internal/webhooks/gitea/webhook.go | 9 ++------- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 internal/actions/actions.go diff --git a/internal/actions/actions.go b/internal/actions/actions.go new file mode 100644 index 0000000..28ec2f5 --- /dev/null +++ b/internal/actions/actions.go @@ -0,0 +1,8 @@ +package actions + +type BotAction string + +const ( + ActionReview BotAction = "/sq-bot review" + ActionPrefix string = "/sq-bot" +) diff --git a/internal/api/sonarqube.go b/internal/api/sonarqube.go index aab2450..7278213 100644 --- a/internal/api/sonarqube.go +++ b/internal/api/sonarqube.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" + "gitea-sonarqube-pr-bot/internal/actions" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "gitea-sonarqube-pr-bot/internal/settings" @@ -31,7 +32,7 @@ func (h *SonarQubeWebhookHandler) composeGiteaComment(w *webhook.Webhook) (strin message[1] = m.GetRenderedMarkdownTable() message[2] = fmt.Sprintf("See [SonarQube](%s) for details.", w.Branch.Url) message[3] = "---" - message[4] = "- If you want the bot to check again, post `/sq-bot review`" + message[4] = fmt.Sprintf("- If you want the bot to check again, post `%s`", actions.ActionReview) return strings.Join(message, "\n\n"), nil } diff --git a/internal/webhooks/gitea/webhook.go b/internal/webhooks/gitea/webhook.go index 450e2bc..657810b 100644 --- a/internal/webhooks/gitea/webhook.go +++ b/internal/webhooks/gitea/webhook.go @@ -6,17 +6,12 @@ import ( "log" "strings" + "gitea-sonarqube-pr-bot/internal/actions" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "gitea-sonarqube-pr-bot/internal/settings" ) -type BotAction string - -const ( - ActionReview BotAction = "/pr-bot review" -) - type issue struct { Number int64 `json:"number"` Repository settings.GiteaRepository `json:"repository"` @@ -60,7 +55,7 @@ func (w *Webhook) Validate() error { return fmt.Errorf("ignore hook for action others than created") } - if !strings.HasPrefix(w.Comment.Body, "/pr-bot") { + if !strings.HasPrefix(w.Comment.Body, actions.ActionPrefix) { return fmt.Errorf("ignore hook for non-bot action comment") } From de575605f94dfd7188badc7bccdedb34c6867b3c Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 16:43:37 +0200 Subject: [PATCH 053/128] Move sonarqube sdk files Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/api/gitea.go | 2 +- internal/api/main.go | 2 +- internal/api/sonarqube.go | 2 +- internal/api/sonarqube_test.go | 6 +++--- internal/clients/{sonarqube_sdk => sonarqube}/measures.go | 2 +- .../sonarqube_sdk.go => sonarqube/sonarqube.go} | 2 +- internal/webhooks/gitea/webhook.go | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) rename internal/clients/{sonarqube_sdk => sonarqube}/measures.go (98%) rename internal/clients/{sonarqube_sdk/sonarqube_sdk.go => sonarqube/sonarqube.go} (98%) diff --git a/internal/api/gitea.go b/internal/api/gitea.go index 80cc13f..26be426 100644 --- a/internal/api/gitea.go +++ b/internal/api/gitea.go @@ -8,7 +8,7 @@ import ( "net/http" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" webhook "gitea-sonarqube-pr-bot/internal/webhooks/gitea" ) diff --git a/internal/api/main.go b/internal/api/main.go index c860a4d..dc894bd 100644 --- a/internal/api/main.go +++ b/internal/api/main.go @@ -5,7 +5,7 @@ import ( "net/http" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "github.com/fvbock/endless" "github.com/gin-gonic/gin" diff --git a/internal/api/sonarqube.go b/internal/api/sonarqube.go index 7278213..53e300e 100644 --- a/internal/api/sonarqube.go +++ b/internal/api/sonarqube.go @@ -10,7 +10,7 @@ import ( "gitea-sonarqube-pr-bot/internal/actions" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" ) diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index aabab91..0c8816b 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -7,7 +7,7 @@ import ( "testing" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSDK "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" @@ -43,8 +43,8 @@ type SQSdkMock struct { mock.Mock } -func (h *SQSdkMock) GetMeasures(project string, branch string) (*sqSDK.MeasuresResponse, error) { - return &sqSDK.MeasuresResponse{}, nil +func (h *SQSdkMock) GetMeasures(project string, branch string) (*sqSdk.MeasuresResponse, error) { + return &sqSdk.MeasuresResponse{}, nil } func defaultMockPreparation(h *HandlerPartialMock) { diff --git a/internal/clients/sonarqube_sdk/measures.go b/internal/clients/sonarqube/measures.go similarity index 98% rename from internal/clients/sonarqube_sdk/measures.go rename to internal/clients/sonarqube/measures.go index d0d20ce..08bb24c 100644 --- a/internal/clients/sonarqube_sdk/measures.go +++ b/internal/clients/sonarqube/measures.go @@ -1,4 +1,4 @@ -package sonarqube_sdk +package sonarqube import ( "fmt" diff --git a/internal/clients/sonarqube_sdk/sonarqube_sdk.go b/internal/clients/sonarqube/sonarqube.go similarity index 98% rename from internal/clients/sonarqube_sdk/sonarqube_sdk.go rename to internal/clients/sonarqube/sonarqube.go index e165255..3c0dfb4 100644 --- a/internal/clients/sonarqube_sdk/sonarqube_sdk.go +++ b/internal/clients/sonarqube/sonarqube.go @@ -1,4 +1,4 @@ -package sonarqube_sdk +package sonarqube import ( "encoding/base64" diff --git a/internal/webhooks/gitea/webhook.go b/internal/webhooks/gitea/webhook.go index 657810b..e920242 100644 --- a/internal/webhooks/gitea/webhook.go +++ b/internal/webhooks/gitea/webhook.go @@ -8,7 +8,7 @@ import ( "gitea-sonarqube-pr-bot/internal/actions" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "gitea-sonarqube-pr-bot/internal/settings" ) From 952e094e8f79d830f7a7b82a89e7498ffb4b02cc Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 17:03:09 +0200 Subject: [PATCH 054/128] Restructure todos and possible improvements Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- README.md | 55 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 8d4b7cd..e4793bb 100644 --- a/README.md +++ b/README.md @@ -7,26 +7,14 @@ _Gitea SonarQube PR Bot_ aims to fill the gap between working on pull requests a Luckily, both endpoints have a proper REST API to communicate with each others. - [Gitea SonarQube PR Bot](#gitea-sonarqube-pr-bot) - - [TODOs](#todos) - [Workflow](#workflow) - [Setup](#setup) - [Bot configuration](#bot-configuration) - [Contributing](#contributing) + - [TODOs](#todos) + - [Possible improvements](#possible-improvements) - [License](#license) -## TODOs - -- [ ] Validate configuration on startup -- [ ] Verify webhook secrets -- [ ] Only post status-check (Opt-in/out) -- [ ] Maybe drop `PRBOT_CONFIG_PATH` environment variable in favor of `--config path/to/config.yaml` cli attribute -- [ ] Configure SonarQube PR branch naming pattern for more flexibility (currently focused on Jenkins with [Gitea Plugin](https://github.com/jenkinsci/gitea-plugin)) -- [ ] Configuration live reloading -- [ ] _Caching_ of outgoing requests in case the target is not available -- [ ] Parsable logging for monitoring -- [ ] Official image for containerized hosting -- [ ] Helm chart for Kubernetes - ## Workflow ![Workflow](docs/workflow.png) @@ -35,16 +23,13 @@ Luckily, both endpoints have a proper REST API to communicate with each others. - Bot activities - extract data from SonarQube - - Read payload from hook post to receive project,branch/pr,quality-gate - - Reads "api/project_pull_requests" to get current issue counts and current state - - Load "api/issues/search" to get detailed information for unresolved issues - - Load "api/measures/component" - - comment PR in Gitea (/repos/{owner}/{repo}/issues/{index}/comments) - - stores mapping of repo+pr+comment-id in ?redis? - - updates status check (either failing/success) - - listen on "/sq-bot review" comments - -> updates comment (/repos/{owner}/{repo}/issues/comments/{id}) - -> updates status check (either failing/success) + - [x] Read payload from hook post to receive project,branch/pr,quality-gate + - [x] Load "api/measures/component" + - [x] comment PR in Gitea (/repos/{owner}/{repo}/issues/{index}/comments) + - [x] updates status check (either failing/success) + - [ ] listen on "/sq-bot review" comments + - [ ] comment PR in Gitea (/repos/{owner}/{repo}/issues/{index}/comments) + - [ ] updates status check (either failing/success) ## Setup @@ -70,6 +55,28 @@ NOTES: - **Please read and follow the [CONTRIBUTORS GUIDE](CONTRIBUTING.md).** +## TODOs + +- [ ] Validate configuration on startup +- [ ] Verify webhook secrets +- [ ] Only post status-check (Opt-in/out) +- [ ] Maybe drop `PRBOT_CONFIG_PATH` environment variable in favor of `--config path/to/config.yaml` cli attribute +- [ ] Configure SonarQube PR branch naming pattern for more flexibility (currently focused on Jenkins with [Gitea Plugin](https://github.com/jenkinsci/gitea-plugin)) +- [ ] Configuration live reloading +- [ ] _Caching_ of outgoing requests in case the target is not available +- [ ] Parsable logging for monitoring +- [ ] Official image for containerized hosting +- [ ] Helm chart for Kubernetes + +### Possible improvements + +- Reuse existing posted comment for updates via SonarQube webhook or `/sq-bot` comments +Therefore storing or dynamically retrieving the previous comment id and modify content (/repos/{owner}/{repo}/issues/comments/{id}) +- Add more information to posted comment + - Read "api/project_pull_requests" to get current issue counts and current state + - Load "api/issues/search" to get detailed information for unresolved issues +- Maybe directly show issues via review comments + ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for the full license text. From 5b72ee7bc0848b825f0c7875aa4dda927411d971 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 17:17:29 +0200 Subject: [PATCH 055/128] Differ between several gitea events Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/api/main.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/internal/api/main.go b/internal/api/main.go index dc894bd..d956378 100644 --- a/internal/api/main.go +++ b/internal/api/main.go @@ -38,10 +38,30 @@ func addSonarQubeEndpoint(r *gin.Engine) { }) } +type validGiteaEndpointHeader struct { + GiteaEvent string `header:"X-Gitea-Event" binding:"required"` +} + func addGiteaEndpoint(r *gin.Engine) { webhookHandler := NewGiteaWebhookHandler(giteaSdk.New(), sqSdk.New()) r.POST("/hooks/gitea", func(c *gin.Context) { - webhookHandler.Handle(c.Writer, c.Request) + h := validGiteaEndpointHeader{} + + if err := c.ShouldBindHeader(&h); err != nil { + c.Status(http.StatusNotFound) + return + } + + switch h.GiteaEvent { + case "pull_request": + fmt.Println("Pull Request activity") + case "issue_comment": + webhookHandler.Handle(c.Writer, c.Request) + default: + c.JSON(http.StatusOK, gin.H{ + "message": "ignore unknown event", + }) + } }) } From 56f7a1081bcc1f2c03dcfd348f4f2a2006218f23 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 17:32:25 +0200 Subject: [PATCH 056/128] Implement skeleton for different webhook handler Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/api/gitea.go | 27 ++++++++++++++++++++++++--- internal/api/main.go | 4 ++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/internal/api/gitea.go b/internal/api/gitea.go index 26be426..1b7d1ab 100644 --- a/internal/api/gitea.go +++ b/internal/api/gitea.go @@ -17,9 +17,7 @@ type GiteaWebhookHandler struct { sqSdk sqSdk.SonarQubeSdkInterface } -func (h *GiteaWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request) { - rw.Header().Set("Content-Type", "application/json") - +func (h *GiteaWebhookHandler) parseBody(rw http.ResponseWriter, r *http.Request) ([]byte, error) { if r.Body != nil { defer r.Body.Close() } @@ -30,6 +28,29 @@ func (h *GiteaWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request) { log.Printf("Error reading request body %s", err.Error()) rw.WriteHeader(http.StatusInternalServerError) io.WriteString(rw, fmt.Sprintf(`{"message": "%s"}`, err.Error())) + return nil, err + } + + return raw, nil +} + +func (h *GiteaWebhookHandler) HandleSynchronize(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + + _, err := h.parseBody(rw, r) + if err != nil { + return + } + + rw.WriteHeader(http.StatusOK) + io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`) +} + +func (h *GiteaWebhookHandler) HandleComment(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + + raw, err := h.parseBody(rw, r) + if err != nil { return } diff --git a/internal/api/main.go b/internal/api/main.go index d956378..8a4581a 100644 --- a/internal/api/main.go +++ b/internal/api/main.go @@ -54,9 +54,9 @@ func addGiteaEndpoint(r *gin.Engine) { switch h.GiteaEvent { case "pull_request": - fmt.Println("Pull Request activity") + webhookHandler.HandleSynchronize(c.Writer, c.Request) case "issue_comment": - webhookHandler.Handle(c.Writer, c.Request) + webhookHandler.HandleComment(c.Writer, c.Request) default: c.JSON(http.StatusOK, gin.H{ "message": "ignore unknown event", From 895dfe92e0d440c6f8aa3675c88e17282a5df333 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 17:59:28 +0200 Subject: [PATCH 057/128] Add pending status on PR synchronize event Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/api/gitea.go | 19 +++- internal/clients/gitea/status.go | 1 + .../webhooks/gitea/{webhook.go => comment.go} | 12 +-- internal/webhooks/gitea/pull.go | 87 +++++++++++++++++++ 4 files changed, 111 insertions(+), 8 deletions(-) rename internal/webhooks/gitea/{webhook.go => comment.go} (84%) create mode 100644 internal/webhooks/gitea/pull.go diff --git a/internal/api/gitea.go b/internal/api/gitea.go index 1b7d1ab..c96ffbc 100644 --- a/internal/api/gitea.go +++ b/internal/api/gitea.go @@ -37,13 +37,28 @@ func (h *GiteaWebhookHandler) parseBody(rw http.ResponseWriter, r *http.Request) func (h *GiteaWebhookHandler) HandleSynchronize(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") - _, err := h.parseBody(rw, r) + raw, err := h.parseBody(rw, r) if err != nil { return } + w, ok := webhook.NewPullWebhook(raw) + if !ok { + rw.WriteHeader(http.StatusUnprocessableEntity) + io.WriteString(rw, `{"message": "Error parsing POST body."}`) + return + } + + if err := w.Validate(); err != nil { + rw.WriteHeader(http.StatusOK) + io.WriteString(rw, fmt.Sprintf(`{"message": "%s"}`, err.Error())) + return + } + rw.WriteHeader(http.StatusOK) io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`) + + w.ProcessData(h.giteaSdk, h.sqSdk) } func (h *GiteaWebhookHandler) HandleComment(rw http.ResponseWriter, r *http.Request) { @@ -54,7 +69,7 @@ func (h *GiteaWebhookHandler) HandleComment(rw http.ResponseWriter, r *http.Requ return } - w, ok := webhook.New(raw) + w, ok := webhook.NewCommentWebhook(raw) if !ok { rw.WriteHeader(http.StatusUnprocessableEntity) io.WriteString(rw, `{"message": "Error parsing POST body."}`) diff --git a/internal/clients/gitea/status.go b/internal/clients/gitea/status.go index 68dff31..5c5b59f 100644 --- a/internal/clients/gitea/status.go +++ b/internal/clients/gitea/status.go @@ -6,6 +6,7 @@ type State gitea.StatusState const ( StatusOK State = State(gitea.StatusSuccess) + StatusPending State = State(gitea.StatusPending) StatusFailure State = State(gitea.StatusFailure) ) diff --git a/internal/webhooks/gitea/webhook.go b/internal/webhooks/gitea/comment.go similarity index 84% rename from internal/webhooks/gitea/webhook.go rename to internal/webhooks/gitea/comment.go index e920242..c7a5436 100644 --- a/internal/webhooks/gitea/webhook.go +++ b/internal/webhooks/gitea/comment.go @@ -21,7 +21,7 @@ type comment struct { Body string `json:"body"` } -type Webhook struct { +type CommentWebhook struct { Action string `json:"action"` IsPR bool `json:"is_pull"` Issue issue `json:"issue"` @@ -29,7 +29,7 @@ type Webhook struct { ConfiguredProject settings.Project } -func (w *Webhook) inProjectsMapping(p []settings.Project) (bool, int) { +func (w *CommentWebhook) inProjectsMapping(p []settings.Project) (bool, int) { owner := w.Issue.Repository.Owner name := w.Issue.Repository.Name for idx, proj := range p { @@ -41,7 +41,7 @@ func (w *Webhook) inProjectsMapping(p []settings.Project) (bool, int) { return false, 0 } -func (w *Webhook) Validate() error { +func (w *CommentWebhook) Validate() error { if !w.IsPR { return fmt.Errorf("ignore non-PR hook") } @@ -64,7 +64,7 @@ func (w *Webhook) Validate() error { return nil } -func (w *Webhook) ProcessData(gSDK giteaSdk.GiteaSdkInterface, sqSDK sqSdk.SonarQubeSdkInterface) { +func (w *CommentWebhook) ProcessData(gSDK giteaSdk.GiteaSdkInterface, sqSDK sqSdk.SonarQubeSdkInterface) { headRef, err := gSDK.DetermineHEAD(w.ConfiguredProject.Gitea, w.Issue.Number) if err != nil { log.Printf("Error retrieving HEAD ref: %s", err.Error()) @@ -79,8 +79,8 @@ func (w *Webhook) ProcessData(gSDK giteaSdk.GiteaSdkInterface, sqSDK sqSdk.Sonar }) } -func New(raw []byte) (*Webhook, bool) { - w := &Webhook{} +func NewCommentWebhook(raw []byte) (*CommentWebhook, bool) { + w := &CommentWebhook{} err := json.Unmarshal(raw, &w) if err != nil { log.Printf("Error parsing Gitea webhook: %s", err.Error()) diff --git a/internal/webhooks/gitea/pull.go b/internal/webhooks/gitea/pull.go new file mode 100644 index 0000000..8d7ff6a --- /dev/null +++ b/internal/webhooks/gitea/pull.go @@ -0,0 +1,87 @@ +package gitea + +import ( + "encoding/json" + "fmt" + "log" + + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" + "gitea-sonarqube-pr-bot/internal/settings" +) + +type pullRequest struct { + Number int64 `json:"number"` + Head struct { + Sha string `json:"sha"` + } `json:"head"` +} + +type repoOwner struct { + Login string `json:"login"` +} + +type rawRepository struct { + Name string `json:"name"` + Owner repoOwner `json:"owner"` +} + +type PullWebhook struct { + Action string `json:"action"` + PullRequest pullRequest `json:"pull_request"` + RawRepository rawRepository `json:"repository"` + Repository settings.GiteaRepository + ConfiguredProject settings.Project +} + +func (w *PullWebhook) inProjectsMapping(p []settings.Project) (bool, int) { + owner := w.RawRepository.Owner.Login + name := w.RawRepository.Name + for idx, proj := range p { + if proj.Gitea.Owner == owner && proj.Gitea.Name == name { + return true, idx + } + } + + return false, 0 +} + +func (w *PullWebhook) Validate() error { + found, pIdx := w.inProjectsMapping(settings.Projects) + owner := w.RawRepository.Owner.Login + name := w.RawRepository.Name + if !found { + return fmt.Errorf("ignore hook for non-configured project '%s/%s'", owner, name) + } + + if w.Action != "synchronized" { + return fmt.Errorf("ignore hook for action others than synchronized") + } + + w.Repository = settings.GiteaRepository{ + Owner: owner, + Name: name, + } + w.ConfiguredProject = settings.Projects[pIdx] + + return nil +} + +func (w *PullWebhook) ProcessData(gSDK giteaSdk.GiteaSdkInterface, sqSDK sqSdk.SonarQubeSdkInterface) { + _ = gSDK.UpdateStatus(w.ConfiguredProject.Gitea, w.PullRequest.Head.Sha, giteaSdk.StatusDetails{ + Url: "", + Message: "Analysis pending...", + State: giteaSdk.StatusPending, + }) +} + +func NewPullWebhook(raw []byte) (*PullWebhook, bool) { + w := &PullWebhook{} + err := json.Unmarshal(raw, &w) + if err != nil { + log.Printf("Error parsing Gitea webhook: %s", err.Error()) + return w, false + } + + return w, true +} From 369edfcfaef044fe9f977a6fdf77b4c3bc276370 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 19:01:48 +0200 Subject: [PATCH 058/128] Fetch actual quality gate status on bot comment Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/api/sonarqube_test.go | 8 ++++ internal/clients/sonarqube/pulls.go | 21 +++++++++ internal/clients/sonarqube/sonarqube.go | 62 +++++++++++++++++++++++++ internal/webhooks/gitea/comment.go | 17 +++++-- internal/webhooks/sonarqube/webhook.go | 16 ++----- 5 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 internal/clients/sonarqube/pulls.go diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index 0c8816b..30f0a93 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -47,6 +47,14 @@ func (h *SQSdkMock) GetMeasures(project string, branch string) (*sqSdk.MeasuresR return &sqSdk.MeasuresResponse{}, nil } +func (h *SQSdkMock) GetPullRequestUrl(project string, index int64) string { + return "" +} + +func (h *SQSdkMock) GetPullRequest(project string, index int64) (*sqSdk.PullRequest, error) { + return nil, nil +} + func defaultMockPreparation(h *HandlerPartialMock) { h.On("fetchDetails", mock.Anything).Return(nil) } diff --git a/internal/clients/sonarqube/pulls.go b/internal/clients/sonarqube/pulls.go new file mode 100644 index 0000000..567ef8d --- /dev/null +++ b/internal/clients/sonarqube/pulls.go @@ -0,0 +1,21 @@ +package sonarqube + +type PullRequest struct { + Key string `json:"key"` + Status struct { + QualityGateStatus string `json:"qualityGateStatus"` + } `json:"status"` +} + +type PullsResponse struct { + PullRequests []PullRequest `json:"pullRequests"` +} + +func (r *PullsResponse) GetPullRequest(name string) *PullRequest { + for _, pr := range r.PullRequests { + if pr.Key == name { + return &pr + } + } + return nil +} diff --git a/internal/clients/sonarqube/sonarqube.go b/internal/clients/sonarqube/sonarqube.go index 3c0dfb4..628be0a 100644 --- a/internal/clients/sonarqube/sonarqube.go +++ b/internal/clients/sonarqube/sonarqube.go @@ -5,13 +5,32 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" + "regexp" + "strconv" "gitea-sonarqube-pr-bot/internal/settings" ) +func ParsePRIndex(name string) (int, error) { + re := regexp.MustCompile(`^PR-(\d+)$`) + res := re.FindSubmatch([]byte(name)) + if len(res) != 2 { + return 0, fmt.Errorf("branch name '%s' does not match regex '%s'", name, re.String()) + } + + return strconv.Atoi(string(res[1])) +} + +func PRNameFromIndex(index int64) string { + return fmt.Sprintf("PR-%d", index) +} + type SonarQubeSdkInterface interface { GetMeasures(string, string) (*MeasuresResponse, error) + GetPullRequestUrl(string, int64) string + GetPullRequest(string, int64) (*PullRequest, error) } type SonarQubeSdk struct { @@ -20,6 +39,49 @@ type SonarQubeSdk struct { token string } +func (sdk *SonarQubeSdk) GetPullRequestUrl(project string, index int64) string { + return fmt.Sprintf("%s/dashboard?id=%s&pullRequest=%s", sdk.baseUrl, project, PRNameFromIndex(index)) +} + +func (sdk *SonarQubeSdk) fetchPullRequests(project string) *PullsResponse { + url := fmt.Sprintf("%s/api/project_pull_requests/list?project=%s", sdk.baseUrl, project) + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + log.Printf("Cannot initialize Request: %s", err.Error()) + return nil + } + req.Header.Add("Authorization", sdk.basicAuth()) + rawResp, _ := sdk.client.Do(req) + if rawResp.Body != nil { + defer rawResp.Body.Close() + } + + body, _ := io.ReadAll(rawResp.Body) + response := &PullsResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + log.Printf("cannot parse response from SonarQube: %s", err.Error()) + return nil + } + + return response +} + +func (sdk *SonarQubeSdk) GetPullRequest(project string, index int64) (*PullRequest, error) { + response := sdk.fetchPullRequests(project) + if response == nil { + return nil, fmt.Errorf("unable to retrieve pull requests from SonarQube") + } + + name := PRNameFromIndex(index) + pr := response.GetPullRequest(name) + if pr == nil { + return nil, fmt.Errorf("no pull request found with name '%s'", name) + } + + return pr, nil +} + func (sdk *SonarQubeSdk) GetMeasures(project string, branch string) (*MeasuresResponse, error) { url := fmt.Sprintf("%s/api/measures/component?additionalFields=metrics&metricKeys=bugs,vulnerabilities,new_security_hotspots,code_smells&component=%s&pullRequest=%s", sdk.baseUrl, project, branch) req, err := http.NewRequest(http.MethodGet, url, nil) diff --git a/internal/webhooks/gitea/comment.go b/internal/webhooks/gitea/comment.go index c7a5436..4e4eff6 100644 --- a/internal/webhooks/gitea/comment.go +++ b/internal/webhooks/gitea/comment.go @@ -72,10 +72,21 @@ func (w *CommentWebhook) ProcessData(gSDK giteaSdk.GiteaSdkInterface, sqSDK sqSd } log.Printf("Fetching SonarQube data...") + pr, err := sqSDK.GetPullRequest(w.ConfiguredProject.SonarQube.Key, w.Issue.Number) + if err != nil { + log.Printf("Error loading PR data from SonarQube: %s", err.Error()) + return + } + + status := giteaSdk.StatusOK + if pr.Status.QualityGateStatus != "OK" { + status = giteaSdk.StatusFailure + } + _ = gSDK.UpdateStatus(w.ConfiguredProject.Gitea, headRef, giteaSdk.StatusDetails{ - Url: "", - Message: "OK", - State: giteaSdk.StatusOK, + Url: sqSDK.GetPullRequestUrl(w.ConfiguredProject.SonarQube.Key, w.Issue.Number), + Message: pr.Status.QualityGateStatus, + State: status, }) } diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index d05117d..5bcdd17 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -4,8 +4,8 @@ import ( "bytes" "fmt" "log" - "regexp" - "strconv" + + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "github.com/spf13/viper" ) @@ -55,7 +55,7 @@ func New(raw []byte) (*Webhook, bool) { return w, false } - idx, err1 := parsePRIndex(w) + idx, err1 := sqSdk.ParsePRIndex(w.Branch.Name) if err1 != nil { log.Printf("Error parsing PR index: %s", err1.Error()) return w, false @@ -65,13 +65,3 @@ func New(raw []byte) (*Webhook, bool) { return w, true } - -func parsePRIndex(w *Webhook) (int, error) { - re := regexp.MustCompile(`^PR-(\d+)$`) - res := re.FindSubmatch([]byte(w.Branch.Name)) - if len(res) != 2 { - return 0, fmt.Errorf("branch name '%s' does not match regex '%s'", w.Branch.Name, re.String()) - } - - return strconv.Atoi(string(res[1])) -} From e28e52445621392bb0aa8f778161b868b0dd0a54 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 19:39:33 +0200 Subject: [PATCH 059/128] Post analysis details on bot action comment Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/api/sonarqube.go | 25 +++++------------ internal/api/sonarqube_test.go | 4 +++ internal/clients/sonarqube/sonarqube.go | 36 +++++++++++++++++++++++++ internal/webhooks/gitea/comment.go | 15 ++++++++++- internal/webhooks/sonarqube/webhook.go | 10 ------- 5 files changed, 60 insertions(+), 30 deletions(-) diff --git a/internal/api/sonarqube.go b/internal/api/sonarqube.go index 53e300e..209640b 100644 --- a/internal/api/sonarqube.go +++ b/internal/api/sonarqube.go @@ -8,7 +8,6 @@ import ( "net/http" "strings" - "gitea-sonarqube-pr-bot/internal/actions" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "gitea-sonarqube-pr-bot/internal/settings" @@ -21,22 +20,6 @@ type SonarQubeWebhookHandler struct { sqSdk sqSdk.SonarQubeSdkInterface } -func (h *SonarQubeWebhookHandler) composeGiteaComment(w *webhook.Webhook) (string, error) { - m, err := h.sqSdk.GetMeasures(w.Project.Key, w.Branch.Name) - if err != nil { - return "", err - } - - message := make([]string, 5) - message[0] = w.GetRenderedQualityGate() - message[1] = m.GetRenderedMarkdownTable() - message[2] = fmt.Sprintf("See [SonarQube](%s) for details.", w.Branch.Url) - message[3] = "---" - message[4] = fmt.Sprintf("- If you want the bot to check again, post `%s`", actions.ActionReview) - - return strings.Join(message, "\n\n"), nil -} - func (*SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) (bool, int) { for idx, proj := range p { if proj.SonarQube.Key == n { @@ -65,9 +48,13 @@ func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings. State: status, }) - comment, err := h.composeGiteaComment(w) + comment, err := h.sqSdk.ComposeGiteaComment(&sqSdk.CommentComposeData{ + Key: w.Project.Key, + PRName: w.Branch.Name, + Url: w.Branch.Url, + QualityGate: w.QualityGate.Status, + }) if err != nil { - log.Printf("Error composing Gitea comment: %s", err.Error()) return } h.giteaSdk.PostComment(repo, w.PRIndex, comment) diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index 30f0a93..5d4a3fc 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -55,6 +55,10 @@ func (h *SQSdkMock) GetPullRequest(project string, index int64) (*sqSdk.PullRequ return nil, nil } +func (h *SQSdkMock) ComposeGiteaComment(data *sqSdk.CommentComposeData) (string, error) { + return "", nil +} + func defaultMockPreparation(h *HandlerPartialMock) { h.On("fetchDetails", mock.Anything).Return(nil) } diff --git a/internal/clients/sonarqube/sonarqube.go b/internal/clients/sonarqube/sonarqube.go index 628be0a..5565948 100644 --- a/internal/clients/sonarqube/sonarqube.go +++ b/internal/clients/sonarqube/sonarqube.go @@ -9,7 +9,9 @@ import ( "net/http" "regexp" "strconv" + "strings" + "gitea-sonarqube-pr-bot/internal/actions" "gitea-sonarqube-pr-bot/internal/settings" ) @@ -27,10 +29,27 @@ func PRNameFromIndex(index int64) string { return fmt.Sprintf("PR-%d", index) } +func GetRenderedQualityGate(qg string) string { + status := ":white_check_mark:" + if qg != "OK" { + status = ":x:" + } + + return fmt.Sprintf("**Quality Gate**: %s", status) +} + type SonarQubeSdkInterface interface { GetMeasures(string, string) (*MeasuresResponse, error) GetPullRequestUrl(string, int64) string GetPullRequest(string, int64) (*PullRequest, error) + ComposeGiteaComment(*CommentComposeData) (string, error) +} + +type CommentComposeData struct { + Key string + PRName string + Url string + QualityGate string } type SonarQubeSdk struct { @@ -104,6 +123,23 @@ func (sdk *SonarQubeSdk) GetMeasures(project string, branch string) (*MeasuresRe return response, nil } +func (sdk *SonarQubeSdk) ComposeGiteaComment(data *CommentComposeData) (string, error) { + m, err := sdk.GetMeasures(data.Key, data.PRName) + if err != nil { + log.Printf("Error composing Gitea comment: %s", err.Error()) + return "", err + } + + message := make([]string, 5) + message[0] = GetRenderedQualityGate(data.QualityGate) + message[1] = m.GetRenderedMarkdownTable() + message[2] = fmt.Sprintf("See [SonarQube](%s) for details.", data.Url) + message[3] = "---" + message[4] = fmt.Sprintf("- If you want the bot to check again, post `%s`", actions.ActionReview) + + return strings.Join(message, "\n\n"), nil +} + func (sdk *SonarQubeSdk) basicAuth() string { auth := []byte(fmt.Sprintf("%s:", sdk.token)) return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(auth)) diff --git a/internal/webhooks/gitea/comment.go b/internal/webhooks/gitea/comment.go index 4e4eff6..51bf054 100644 --- a/internal/webhooks/gitea/comment.go +++ b/internal/webhooks/gitea/comment.go @@ -83,11 +83,24 @@ func (w *CommentWebhook) ProcessData(gSDK giteaSdk.GiteaSdkInterface, sqSDK sqSd status = giteaSdk.StatusFailure } + url := sqSDK.GetPullRequestUrl(w.ConfiguredProject.SonarQube.Key, w.Issue.Number) + _ = gSDK.UpdateStatus(w.ConfiguredProject.Gitea, headRef, giteaSdk.StatusDetails{ - Url: sqSDK.GetPullRequestUrl(w.ConfiguredProject.SonarQube.Key, w.Issue.Number), + Url: url, Message: pr.Status.QualityGateStatus, State: status, }) + + comment, err := sqSDK.ComposeGiteaComment(&sqSdk.CommentComposeData{ + Key: w.ConfiguredProject.SonarQube.Key, + PRName: sqSdk.PRNameFromIndex(w.Issue.Number), + Url: url, + QualityGate: pr.Status.QualityGateStatus, + }) + if err != nil { + return + } + gSDK.PostComment(w.ConfiguredProject.Gitea, int(w.Issue.Number), comment) } func NewCommentWebhook(raw []byte) (*CommentWebhook, bool) { diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index 5bcdd17..2836043 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -2,7 +2,6 @@ package sonarqube import ( "bytes" - "fmt" "log" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" @@ -33,15 +32,6 @@ type Webhook struct { PRIndex int } -func (w *Webhook) GetRenderedQualityGate() string { - status := ":white_check_mark:" - if w.QualityGate.Status != "OK" { - status = ":x:" - } - - return fmt.Sprintf("**Quality Gate**: %s", status) -} - func New(raw []byte) (*Webhook, bool) { v := viper.New() v.SetConfigType("json") From ae55eaf97c12cba5a4a1cafb7c6928624876988d Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 19:45:57 +0200 Subject: [PATCH 060/128] Prevent bot from interpreting unknown actions Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/webhooks/gitea/comment.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/webhooks/gitea/comment.go b/internal/webhooks/gitea/comment.go index 51bf054..5cbe899 100644 --- a/internal/webhooks/gitea/comment.go +++ b/internal/webhooks/gitea/comment.go @@ -59,6 +59,10 @@ func (w *CommentWebhook) Validate() error { return fmt.Errorf("ignore hook for non-bot action comment") } + if w.Comment.Body != string(actions.ActionReview) { + return fmt.Errorf("ignore hook for unknown bot action") + } + w.ConfiguredProject = settings.Projects[pIdx] return nil From 8678f1391166f43aa7093e0c6eb2da1e0d7c0a6c Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sun, 10 Oct 2021 19:47:54 +0200 Subject: [PATCH 061/128] Update bot insights Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e4793bb..d95fd7e 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ Luckily, both endpoints have a proper REST API to communicate with each others. - [x] Load "api/measures/component" - [x] comment PR in Gitea (/repos/{owner}/{repo}/issues/{index}/comments) - [x] updates status check (either failing/success) - - [ ] listen on "/sq-bot review" comments - - [ ] comment PR in Gitea (/repos/{owner}/{repo}/issues/{index}/comments) - - [ ] updates status check (either failing/success) + - [x] listen on "/sq-bot review" comments + - [x] comment PR in Gitea (/repos/{owner}/{repo}/issues/{index}/comments) + - [x] updates status check (either failing/success) ## Setup From a51520382e40e59b91c63237bed91a5c819a5e40 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 11 Oct 2021 08:42:11 +0200 Subject: [PATCH 062/128] Add production Dockerfile Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- .dockerignore | 7 ++++ Dockerfile | 47 +++++++++++++++++++++++ docker/usr/local/bin/docker-entrypoint.sh | 7 ++++ 3 files changed, 61 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker/usr/local/bin/docker-entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4c1504f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +** +!/cmd +!/internal +!/vendor +!/docker +!go.mod +!go.sum diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..68f5c1f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +################################### +# Build stages +################################### +FROM golang:1.17-alpine3.14 AS build-go + +ARG GOPROXY +ENV GOPROXY ${GOPROXY:-direct} + +RUN apk update \ + && apk --no-cache add build-base git bash + +COPY . ${GOPATH}/src/bot +WORKDIR ${GOPATH}/src/bot + +RUN go build ./cmd/gitea-sonarqube-bot + +################################### +# Production image +################################### +FROM alpine:3.14 +LABEL maintainer="justusbunsi " + +RUN apk update \ + && apk --no-cache add ca-certificates bash \ + && rm -rf /var/cache/apk/* + +RUN addgroup -S -g 1000 bot \ + && adduser -S -D -H -h /home/bot -s /bin/bash -u 1000 -G bot bot + +RUN mkdir -p /home/bot/config/ +RUN chown bot:bot /home/bot/config/ + +COPY --chown=bot:bot docker / +COPY --from=build-go --chown=bot:bot /go/src/bot/gitea-sonarqube-bot /usr/local/bin/gitea-sonarqube-bot + +#bot:bot +USER 1000:1000 +WORKDIR /home/bot +ENV HOME=/home/bot + +EXPOSE 3000 +ENV GIN_MODE "release" + +VOLUME ["/home/bot/config/"] + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] +CMD [] diff --git a/docker/usr/local/bin/docker-entrypoint.sh b/docker/usr/local/bin/docker-entrypoint.sh new file mode 100644 index 0000000..37a0ba7 --- /dev/null +++ b/docker/usr/local/bin/docker-entrypoint.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +if [ $# -gt 0 ]; then + exec "$@" +else + exec /usr/local/bin/gitea-sonarqube-bot +fi From 48cb8a0edef3fd5cf918a0057466644e9ffefedc Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 11 Oct 2021 08:50:50 +0200 Subject: [PATCH 063/128] Handle favicon requests Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/api/main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/api/main.go b/internal/api/main.go index 8a4581a..d6fb44c 100644 --- a/internal/api/main.go +++ b/internal/api/main.go @@ -74,5 +74,9 @@ func Serve(c *cli.Context) error { addSonarQubeEndpoint(r) addGiteaEndpoint(r) + r.GET("/favicon.ico", func(c *gin.Context) { + c.Status(http.StatusNoContent) + }) + return endless.ListenAndServe(":3000", r) } From 105ba59b4db74e9fa5a68778fd4c4bd3759a2538 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 11 Oct 2021 08:52:35 +0200 Subject: [PATCH 064/128] Add raw release instructions Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- CONTRIBUTING.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c7ff03..143cad9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,7 @@ - [Table of Contents](#table-of-contents) - [Setup development environment](#setup-development-environment) - [Testing](#testing) + - [Release](#release) - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) ## Setup development environment @@ -34,6 +35,14 @@ go test ./... go test -coverprofile cover.out ./... ``` +## Release + +For local purposes + +```bash +docker build -t gitea-sonarqube-pr-bot/prod . +``` + ## Developer Certificate of Origin (DCO) I consider the act of contributing to the code by submitting a Pull Request as the "Sign off" or agreement to the From bb156f95bfef114dc2adb16f5ca4d077ac369b02 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 11 Oct 2021 11:28:32 +0200 Subject: [PATCH 065/128] Log response code for non-working status update Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/clients/gitea/gitea.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/clients/gitea/gitea.go b/internal/clients/gitea/gitea.go index 29dca72..d48e66c 100644 --- a/internal/clients/gitea/gitea.go +++ b/internal/clients/gitea/gitea.go @@ -36,9 +36,9 @@ func (sdk *GiteaSdk) UpdateStatus(repo settings.GiteaRepository, ref string, det State: gitea.StatusState(details.State), } - _, _, err := sdk.client.CreateStatus(repo.Owner, repo.Name, ref, opt) + _, r, err := sdk.client.CreateStatus(repo.Owner, repo.Name, ref, opt) if err != nil { - log.Printf("Error updating status: %s", err.Error()) + log.Printf("Error updating status: response code: %d | error: '%s'", r.StatusCode, err.Error()) } return err From 0bd65d8a1d924e4110929c1a4d3fccd66c3299e2 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 11 Oct 2021 14:21:17 +0200 Subject: [PATCH 066/128] Allow override the provided revision Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- README.md | 7 ++++ internal/api/sonarqube.go | 2 +- internal/webhooks/sonarqube/webhook.go | 47 ++++++++++++++------------ 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index d95fd7e..2fe28ee 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,13 @@ Luckily, both endpoints have a proper REST API to communicate with each others. - Create a token for this user that will be used by the bot. - Create a project/organization/system webhook pointing to `https:///gitea`. Consider securing it with a secret. +**CI system** +- Some CI systems may emulate a merge and therefore produce another, not yet existing commit hash that is promoted to SonarQube. + This would cause the bot to fail to set the commit status in Gitea because the webhook sent by SonarQube contains that commit hash. + To mitigate that situation, the bot will look inside the `properties` object for the key `sonar.analysis.sqbot`. If available, this + key can contain the actual commit hash to use for updating the status in Gitea. + See [SonarQube docs](https://docs.sonarqube.org/latest/project-administration/webhooks) for details. + ## Bot configuration See [config.example.yaml](config/config.example.yaml) for a full configuration specification and description. diff --git a/internal/api/sonarqube.go b/internal/api/sonarqube.go index 209640b..77fbf30 100644 --- a/internal/api/sonarqube.go +++ b/internal/api/sonarqube.go @@ -42,7 +42,7 @@ func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings. if w.QualityGate.Status != "OK" { status = giteaSdk.StatusFailure } - _ = h.giteaSdk.UpdateStatus(repo, w.Revision, giteaSdk.StatusDetails{ + _ = h.giteaSdk.UpdateStatus(repo, w.GetRevision(), giteaSdk.StatusDetails{ Url: w.Branch.Url, Message: w.QualityGate.Status, State: status, diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index 2836043..c5bfcce 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -1,45 +1,50 @@ package sonarqube import ( - "bytes" + "encoding/json" "log" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" - - "github.com/spf13/viper" ) type Webhook struct { - ServerUrl string `mapstructure:"serverUrl"` - Revision string + ServerUrl string `json:"serverUrl"` + Revision string `json:"revision"` Project struct { - Key string - Name string - Url string - } + Key string `json:"key"` + Name string `json:"name"` + Url string `json:"url"` + } `json:"project"` Branch struct { - Name string - Type string - Url string - } + Name string `json:"name"` + Type string `json:"type"` + Url string `json:"url"` + } `json:"branch"` QualityGate struct { - Status string + Status string `json:"status"` Conditions []struct { Metric string Status string - } - } `mapstructure:"qualityGate"` + } `json:"conditions"` + } `json:"qualityGate"` + Properties *struct { + OriginalCommit string `json:"sonar.analysis.sqbot,omitempty"` + } `json:"properties,omitempty"` PRIndex int } -func New(raw []byte) (*Webhook, bool) { - v := viper.New() - v.SetConfigType("json") - v.ReadConfig(bytes.NewBuffer(raw)) +func (w *Webhook) GetRevision() string { + if w.Properties != nil && w.Properties.OriginalCommit != "" { + return w.Properties.OriginalCommit + } + return w.Revision +} + +func New(raw []byte) (*Webhook, bool) { w := &Webhook{} - err := v.Unmarshal(&w) + err := json.Unmarshal(raw, w) if err != nil { log.Printf("Error parsing SonarQube webhook: %s", err.Error()) return w, false From ec781a5a29d6696e86c80dcec28aef41bce0dc21 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 11 Oct 2021 16:09:13 +0200 Subject: [PATCH 067/128] Add helm chart Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- README.md | 4 +- helm/.helmignore | 23 ++++ helm/Chart.yaml | 6 + helm/templates/NOTES.txt | 22 ++++ helm/templates/_helpers.tpl | 62 +++++++++ helm/templates/config.yaml | 7 + helm/templates/deployment.yaml | 73 +++++++++++ helm/templates/ingress.yaml | 61 +++++++++ helm/templates/service.yaml | 15 +++ helm/templates/serviceaccount.yaml | 13 ++ helm/templates/tests/test-connection.yaml | 15 +++ helm/values.yaml | 150 ++++++++++++++++++++++ 12 files changed, 449 insertions(+), 2 deletions(-) create mode 100644 helm/.helmignore create mode 100644 helm/Chart.yaml create mode 100644 helm/templates/NOTES.txt create mode 100644 helm/templates/_helpers.tpl create mode 100644 helm/templates/config.yaml create mode 100644 helm/templates/deployment.yaml create mode 100644 helm/templates/ingress.yaml create mode 100644 helm/templates/service.yaml create mode 100644 helm/templates/serviceaccount.yaml create mode 100644 helm/templates/tests/test-connection.yaml create mode 100644 helm/values.yaml diff --git a/README.md b/README.md index 2fe28ee..8db4fc6 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,12 @@ Luckily, both endpoints have a proper REST API to communicate with each others. **SonarQube** - Create a user and grant permissions to "Browse on project" for the desired project - Create a token for this user that will be used by the bot. -- Create a webhook pointing to `https:///sonarqube`. Consider securing it with a secret. +- Create a webhook pointing to `https:///hooks/sonarqube`. Consider securing it with a secret. **Gitea** - Create a user and grant permissions to "Read project" for the desired projects including access to "Pull Requests" - Create a token for this user that will be used by the bot. -- Create a project/organization/system webhook pointing to `https:///gitea`. Consider securing it with a secret. +- Create a project/organization/system webhook pointing to `https:///hooks/gitea`. Consider securing it with a secret. **CI system** - Some CI systems may emulate a merge and therefore produce another, not yet existing commit hash that is promoted to SonarQube. diff --git a/helm/.helmignore b/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..42e03eb --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: gitea-sonarqube-bot +description: A Helm Chart for running a bot to communicate between both Gitea and SonarQube +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt new file mode 100644 index 0000000..892e8dc --- /dev/null +++ b/helm/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "helm.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "helm.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "helm.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "helm.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl new file mode 100644 index 0000000..ba04c30 --- /dev/null +++ b/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "helm.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "helm.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "helm.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "helm.labels" -}} +helm.sh/chart: {{ include "helm.chart" . }} +{{ include "helm.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "helm.selectorLabels" -}} +app.kubernetes.io/name: {{ include "helm.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "helm.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "helm.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/templates/config.yaml b/helm/templates/config.yaml new file mode 100644 index 0000000..e0ffa01 --- /dev/null +++ b/helm/templates/config.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "helm.fullname" . }} +stringData: + config.yaml: |- + {{- toYaml .Values.app.configuration | nindent 4 }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 0000000..2a04847 --- /dev/null +++ b/helm/templates/deployment.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "helm.fullname" . }} + labels: + {{- include "helm.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "helm.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "helm.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "helm.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 3000 + protocol: TCP + livenessProbe: + httpGet: + path: /ping + port: http + readinessProbe: + httpGet: + path: /ping + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: sq-bot-config + mountPath: /home/bot/config + readOnly: true + {{- if .Values.volumeMounts }} + {{- toYaml .Values.volumeMounts | nindent 12 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: sq-bot-config + secret: + secretName: {{ include "helm.fullname" . }} + {{- if .Values.volumes }} + {{- toYaml .Values.volumes | nindent 8 }} + {{- end }} diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml new file mode 100644 index 0000000..014f7c6 --- /dev/null +++ b/helm/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "helm.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "helm.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 0000000..de450fc --- /dev/null +++ b/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "helm.fullname" . }} + labels: + {{- include "helm.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "helm.selectorLabels" . | nindent 4 }} diff --git a/helm/templates/serviceaccount.yaml b/helm/templates/serviceaccount.yaml new file mode 100644 index 0000000..5c51ef1 --- /dev/null +++ b/helm/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "helm.serviceAccountName" . }} + labels: + {{- include "helm.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} +automountServiceAccountToken: false diff --git a/helm/templates/tests/test-connection.yaml b/helm/templates/tests/test-connection.yaml new file mode 100644 index 0000000..bf1c65f --- /dev/null +++ b/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "helm.fullname" . }}-test-connection" + labels: + {{- include "helm.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "helm.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 0000000..d9b3aa9 --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,150 @@ +# Default values for helm. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: justusbunsi/gitea-sonarqube-bot + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +app: + # This object represents the config.yaml provided to the application + configuration: + # Gitea related configuration. Necessary for adding/updating comments on repository pull requests + gitea: + # Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. + url: "" + + # Created access token for the user that shall be used as bot account. + # User needs "Read project" permissions with access to "Pull Requests" + token: + value: "" + # # or path to file containing the plain text secret + # file: /bot/secrets/gitea/user-token + + # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the + # request will be ignored. + # The bot looks for `X-Gitea-Signature` header containing the sha256 hmac hash of the plain text secret. If the header + # exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be validated. + webhook: + secret: "" + # # or path to file containing the plain text secret + # secretFile: /bot/secrets/gitea/webhook-secret + + # SonarQube related configuration. Necessary for requesting data from the API and processing the webhook. + sonarqube: + # Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. + url: "" + + # Created access token for the user that shall be used as bot account. + # User needs "Browse on project" permissions + token: + value: "" + # # or path to file containing the plain text secret + # file: /bot/secrets/sonarqube/user-token + + # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the + # request will be ignored. + # The bot looks for `X-Sonar-Webhook-HMAC-SHA256` header containing the sha256 hmac hash of the plain text secret. + # If the header exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be + # validated. + webhook: + secret: "" + # # or path to file containing the plain text secret + # secretFile: /bot/secrets/sonarqube/webhook-secret + + # List of project mappings to take care of. Webhooks for other projects will be ignored. + # At least one must be configured. Otherwise all webhooks (no matter which source) because the bot cannot map on its own. + projects: + - sonarqube: + key: "" + # A repository specification contains the owner name and the repository name itself. The owner can be the name of a + # real account or an organization in which the repository is located. + gitea: + owner: "" + name: "" + +# If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly +volumes: [] +# - name: gitea-connection +# secret: +# secretName: gitea-secret-with-token-and-maybe-webhook-secret +# - name: sonarqube-connection +# secret: +# secretName: sonarqube-secret-with-token-and-maybe-webhook-secret + +# If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly +volumeMounts: [] +# - name: gitea-connection +# readOnly: true +# mountPath: "/bot/secrets/gitea/" +# - name: sonarqube-connection +# readOnly: true +# mountPath: "/bot/secrets/sonarqube/" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: + fsGroup: 1000 + +securityContext: + # capabilities: + # drop: + # - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: sqbot.example.com + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - sqbot.example.com + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} From b8c86aaab8d2e98a0d3e8db7daf72aef30896fe6 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Mon, 11 Oct 2021 17:39:21 +0200 Subject: [PATCH 068/128] Update docs Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8db4fc6..e803fd7 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Luckily, both endpoints have a proper REST API to communicate with each others. - [Gitea SonarQube PR Bot](#gitea-sonarqube-pr-bot) - [Workflow](#workflow) + - [Requirements](#requirements) - [Setup](#setup) - [Bot configuration](#bot-configuration) - [Contributing](#contributing) @@ -31,6 +32,10 @@ Luckily, both endpoints have a proper REST API to communicate with each others. - [x] comment PR in Gitea (/repos/{owner}/{repo}/issues/{index}/comments) - [x] updates status check (either failing/success) +## Requirements + +This bot is designed to interact with [SonarQube _Developer_ edition](https://www.sonarsource.com/plans-and-pricing/) and above due to its pull request features. It will most likely work with public SonarCloud because it includes that feature for open source projects. + ## Setup **SonarQube** @@ -72,8 +77,8 @@ NOTES: - [ ] Configuration live reloading - [ ] _Caching_ of outgoing requests in case the target is not available - [ ] Parsable logging for monitoring -- [ ] Official image for containerized hosting -- [ ] Helm chart for Kubernetes +- [x] Official image for containerized hosting +- [x] Helm chart for Kubernetes ### Possible improvements From 48b522d348114ae3acea2625868507cb9225fa0c Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Mon, 11 Oct 2021 19:09:29 +0200 Subject: [PATCH 069/128] Fix bot name Signed-off-by: Steven Kriegler --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e803fd7..3561eba 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# Gitea SonarQube PR Bot +# Gitea SonarQube Bot -_Gitea SonarQube PR Bot_ is a bot that receives messages from both SonarQube and Gitea to help developers +_Gitea SonarQube Bot_ is a bot that receives messages from both SonarQube and Gitea to help developers being productive. The idea behind this project is the missing ALM integration of Gitea in SonarQube. Unfortunately, this [won't be added in near future](https://github.com/SonarSource/sonarqube/pull/3248#issuecomment-701334327). -_Gitea SonarQube PR Bot_ aims to fill the gap between working on pull requests and being notified on quality changes. +_Gitea SonarQube Bot_ aims to fill the gap between working on pull requests and being notified on quality changes. Luckily, both endpoints have a proper REST API to communicate with each others. -- [Gitea SonarQube PR Bot](#gitea-sonarqube-pr-bot) +- [Gitea SonarQube Bot](#gitea-sonarqube-bot) - [Workflow](#workflow) - [Requirements](#requirements) - [Setup](#setup) From 208a866a159b08db18710a88f3c1ac660ab5f60e Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Mon, 11 Oct 2021 19:48:01 +0200 Subject: [PATCH 070/128] Restructure README Signed-off-by: Steven Kriegler --- README.md | 59 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 3561eba..25c7e62 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,14 @@ Luckily, both endpoints have a proper REST API to communicate with each others. - [Gitea SonarQube Bot](#gitea-sonarqube-bot) - [Workflow](#workflow) - [Requirements](#requirements) - - [Setup](#setup) - [Bot configuration](#bot-configuration) - - [Contributing](#contributing) + - [Setup](#setup) + - [SonarQube](#sonarqube) + - [Gitea](#gitea) + - [CI system](#ci-system) - [TODOs](#todos) - [Possible improvements](#possible-improvements) + - [Contributing](#contributing) - [License](#license) ## Workflow @@ -36,36 +39,33 @@ Luckily, both endpoints have a proper REST API to communicate with each others. This bot is designed to interact with [SonarQube _Developer_ edition](https://www.sonarsource.com/plans-and-pricing/) and above due to its pull request features. It will most likely work with public SonarCloud because it includes that feature for open source projects. -## Setup - -**SonarQube** -- Create a user and grant permissions to "Browse on project" for the desired project -- Create a token for this user that will be used by the bot. -- Create a webhook pointing to `https:///hooks/sonarqube`. Consider securing it with a secret. - -**Gitea** -- Create a user and grant permissions to "Read project" for the desired projects including access to "Pull Requests" -- Create a token for this user that will be used by the bot. -- Create a project/organization/system webhook pointing to `https:///hooks/gitea`. Consider securing it with a secret. - -**CI system** -- Some CI systems may emulate a merge and therefore produce another, not yet existing commit hash that is promoted to SonarQube. - This would cause the bot to fail to set the commit status in Gitea because the webhook sent by SonarQube contains that commit hash. - To mitigate that situation, the bot will look inside the `properties` object for the key `sonar.analysis.sqbot`. If available, this - key can contain the actual commit hash to use for updating the status in Gitea. - See [SonarQube docs](https://docs.sonarqube.org/latest/project-administration/webhooks) for details. - ## Bot configuration See [config.example.yaml](config/config.example.yaml) for a full configuration specification and description. -## Contributing +## Setup -Expected workflow is: Fork -> Patch -> Push -> Pull Request +### SonarQube -NOTES: +- Create a user and grant permissions to "Browse on project" for the desired project +- Create a token for this user that will be used by the bot +- Create a webhook pointing to `https:///hooks/sonarqube` +- Consider securing it with a secret -- **Please read and follow the [CONTRIBUTORS GUIDE](CONTRIBUTING.md).** +### Gitea + +- Create a user and grant permissions to "Read project" for the desired projects including access to "Pull Requests" +- Create a token for this user that will be used by the bot +- Create a project/organization/system webhook pointing to `https:///hooks/gitea` +- Consider securing the webhook with a secret + +### CI system + +Some CI systems may emulate a merge and therefore produce another, not yet existing commit hash that is promoted to SonarQube. +This would cause the bot to fail to set the commit status in Gitea because the webhook sent by SonarQube contains that commit hash. +To mitigate that situation, the bot will look inside the `properties` object for the key `sonar.analysis.sqbot`. If available, this +key can contain the actual commit hash to use for updating the status in Gitea. +See [SonarQube docs](https://docs.sonarqube.org/latest/project-administration/webhooks) for details. ## TODOs @@ -79,6 +79,7 @@ NOTES: - [ ] Parsable logging for monitoring - [x] Official image for containerized hosting - [x] Helm chart for Kubernetes +- [ ] Publish Helm chart + docker image ### Possible improvements @@ -89,6 +90,14 @@ Therefore storing or dynamically retrieving the previous comment id and modify c - Load "api/issues/search" to get detailed information for unresolved issues - Maybe directly show issues via review comments +## Contributing + +Expected workflow is: Fork -> Patch -> Push -> Pull Request + +NOTES: + +- **Please read and follow the [CONTRIBUTORS GUIDE](CONTRIBUTING.md).** + ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for the full license text. From a2d68ccc1232bf4cfd471af91eaaa5e7c5abceae Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Mon, 11 Oct 2021 20:02:12 +0200 Subject: [PATCH 071/128] Add screenshots Signed-off-by: Steven Kriegler --- README.md | 10 ++++++++++ docs/slideshow/comment.png | Bin 0 -> 12975 bytes docs/slideshow/status.png | Bin 0 -> 4566 bytes 3 files changed, 10 insertions(+) create mode 100644 docs/slideshow/comment.png create mode 100644 docs/slideshow/status.png diff --git a/README.md b/README.md index 25c7e62..19f55fb 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Luckily, both endpoints have a proper REST API to communicate with each others. - [Possible improvements](#possible-improvements) - [Contributing](#contributing) - [License](#license) + - [Screenshots](#screenshots) ## Workflow @@ -101,3 +102,12 @@ NOTES: ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for the full license text. + +--- + +## Screenshots + +> Bot name and avatar depend on user configuration. + +![Comment](./docs/slideshow/comment.png) +![Status](./docs/slideshow/status.png) diff --git a/docs/slideshow/comment.png b/docs/slideshow/comment.png new file mode 100644 index 0000000000000000000000000000000000000000..d9e7c6b649c99996dbb3a1bf0745bff487d67a6c GIT binary patch literal 12975 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 literal 0 HcmV?d00001 diff --git a/docs/slideshow/status.png b/docs/slideshow/status.png new file mode 100644 index 0000000000000000000000000000000000000000..5fa3ca4104307f9f5e5ef31b41bc4be782f73eae GIT binary patch literal 4566 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 LaXdq^zH;Y3U) Date: Tue, 12 Oct 2021 13:10:35 +0200 Subject: [PATCH 072/128] Allow customizing metrics fetch from SonarQube Signed-off-by: Steven Kriegler --- .gitignore | 1 + config/config.example.yaml | 22 ++++++++----- helm/values.yaml | 6 ++++ internal/clients/sonarqube/sonarqube.go | 2 +- internal/settings/settings.go | 12 +++++--- internal/settings/settings_test.go | 41 +++++++++++++++++++++++++ internal/settings/sonarqube.go | 21 +++++++++++-- 7 files changed, 88 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 73c14b5..9cf8818 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /coverage.html /*.log /cover.out +/cover.html diff --git a/config/config.example.yaml b/config/config.example.yaml index 951ac4d..cd48617 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -7,8 +7,8 @@ gitea: # User needs "Read project" permissions with access to "Pull Requests" token: value: "" - # # or path to file containing the plain text secret - # file: /path/to/gitea/token + # # or path to file containing the plain text secret + # file: /path/to/gitea/token # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the # request will be ignored. @@ -16,8 +16,8 @@ gitea: # exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be validated. webhook: secret: "" - # # or path to file containing the plain text secret - # secretFile: /path/to/gitea/webhook/secret + # # or path to file containing the plain text secret + # secretFile: /path/to/gitea/webhook/secret # SonarQube related configuration. Necessary for requesting data from the API and processing the webhook. sonarqube: @@ -28,8 +28,8 @@ sonarqube: # User needs "Browse on project" permissions token: value: "" - # # or path to file containing the plain text secret - # file: /path/to/sonarqube/token + # # or path to file containing the plain text secret + # file: /path/to/sonarqube/token # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the # request will be ignored. @@ -38,8 +38,14 @@ sonarqube: # validated. webhook: secret: "" - # # or path to file containing the plain text secret - # secretFile: /path/to/sonarqube/webhook/secret + # # or path to file containing the plain text secret + # secretFile: /path/to/sonarqube/webhook/secret + + # Some useful metrics depend on the edition in use. There are various ones like code_smells, vulnerabilities, bugs, etc. + # By default the bot will extract "bugs,vulnerabilities,code_smells" + # Setting this option you can extend that default list by your own metrics. + additionalMetrics: [] + # - "new_security_hotspots" # List of project mappings to take care of. Webhooks for other projects will be ignored. # At least one must be configured. Otherwise all webhooks (no matter which source) because the bot cannot map on its own. diff --git a/helm/values.yaml b/helm/values.yaml index d9b3aa9..aec6e51 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -55,6 +55,12 @@ app: secret: "" # # or path to file containing the plain text secret # secretFile: /bot/secrets/sonarqube/webhook-secret + + # Some useful metrics depend on the edition in use. There are various ones like code_smells, vulnerabilities, bugs, etc. + # By default the bot will extract "bugs,vulnerabilities,code_smells" + # Setting this option you can extend that default list by your own metrics. + additionalMetrics: [] + # - "new_security_hotspots" # List of project mappings to take care of. Webhooks for other projects will be ignored. # At least one must be configured. Otherwise all webhooks (no matter which source) because the bot cannot map on its own. diff --git a/internal/clients/sonarqube/sonarqube.go b/internal/clients/sonarqube/sonarqube.go index 5565948..be657b0 100644 --- a/internal/clients/sonarqube/sonarqube.go +++ b/internal/clients/sonarqube/sonarqube.go @@ -102,7 +102,7 @@ func (sdk *SonarQubeSdk) GetPullRequest(project string, index int64) (*PullReque } func (sdk *SonarQubeSdk) GetMeasures(project string, branch string) (*MeasuresResponse, error) { - url := fmt.Sprintf("%s/api/measures/component?additionalFields=metrics&metricKeys=bugs,vulnerabilities,new_security_hotspots,code_smells&component=%s&pullRequest=%s", sdk.baseUrl, project, branch) + url := fmt.Sprintf("%s/api/measures/component?additionalFields=metrics&metricKeys=%s&component=%s&pullRequest=%s", sdk.baseUrl, settings.SonarQube.GetMetricsList(), project, branch) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("cannot initialize Request: %w", err) diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 44f9850..918cd72 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -32,6 +32,7 @@ func newConfigReader() *viper.Viper { v.SetDefault("sonarqube.token.file", "") v.SetDefault("sonarqube.webhook.secret", "") v.SetDefault("sonarqube.webhook.secretFile", "") + v.SetDefault("sonarqube.additionalMetrics", []string{}) v.SetDefault("projects", []Project{}) return v @@ -43,14 +44,14 @@ func Load(configPath string) { err := r.ReadInConfig() if err != nil { - panic(fmt.Errorf("Fatal error while reading config file: %w \n", err)) + panic(fmt.Errorf("fatal error while reading config file: %w", err)) } var projects []Project err = r.UnmarshalKey("projects", &projects) if err != nil { - panic(fmt.Errorf("Unable to load project mapping: %s", err.Error())) + panic(fmt.Errorf("unable to load project mapping: %s", err.Error())) } if len(projects) == 0 { @@ -67,8 +68,9 @@ func Load(configPath string) { Webhook: NewWebhook(r, "gitea", errCallback), } SonarQube = sonarQubeConfig{ - Url: r.GetString("sonarqube.url"), - Token: NewToken(r, "sonarqube", errCallback), - Webhook: NewWebhook(r, "sonarqube", errCallback), + Url: r.GetString("sonarqube.url"), + Token: NewToken(r, "sonarqube", errCallback), + Webhook: NewWebhook(r, "sonarqube", errCallback), + AdditionalMetrics: r.GetStringSlice("sonarqube.additionalMetrics"), } } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 82cd6cd..051c27f 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -22,6 +22,7 @@ sonarqube: value: a09eb5785b25bb2cbacf48808a677a0709f02d8e webhook: secret: haxxor-sonarqube-secret + additionalMetrics: [] projects: - sonarqube: key: gitea-sonarqube-pr-bot @@ -107,6 +108,45 @@ func TestLoadSonarQubeStructure(t *testing.T) { } assert.EqualValues(t, expected, SonarQube) + assert.EqualValues(t, expected.GetMetricsList(), "bugs,vulnerabilities,code_smells") +} + +func TestLoadSonarQubeStructureWithAdditionalMetrics(t *testing.T) { + WriteConfigFile(t, []byte( + `gitea: + url: https://example.com/gitea + token: + value: fake-gitea-token +sonarqube: + url: https://example.com/sonarqube + token: + value: fake-sonarqube-token + additionalMetrics: "new_security_hotspots" +projects: + - sonarqube: + key: gitea-sonarqube-pr-bot + gitea: + owner: example-organization + name: pr-bot +`)) + Load(os.TempDir()) + + expected := sonarQubeConfig{ + Url: "https://example.com/sonarqube", + Token: &token{ + Value: "fake-sonarqube-token", + }, + Webhook: &webhook{ + Secret: "", + }, + AdditionalMetrics: []string{ + "new_security_hotspots", + }, + } + + assert.EqualValues(t, expected, SonarQube) + assert.EqualValues(t, expected.AdditionalMetrics, []string{"new_security_hotspots"}) + assert.EqualValues(t, "bugs,vulnerabilities,code_smells,new_security_hotspots", SonarQube.GetMetricsList()) } func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { @@ -189,6 +229,7 @@ projects: Secret: "sonarqube-totally-secret", secretFile: sonarqubeWebhookSecretFile, }, + AdditionalMetrics: []string{}, } Load(os.TempDir()) diff --git a/internal/settings/sonarqube.go b/internal/settings/sonarqube.go index b7ef864..1c7fde2 100644 --- a/internal/settings/sonarqube.go +++ b/internal/settings/sonarqube.go @@ -1,7 +1,22 @@ package settings +import "strings" + type sonarQubeConfig struct { - Url string - Token *token - Webhook *webhook + Url string + Token *token + Webhook *webhook + AdditionalMetrics []string +} + +func (c *sonarQubeConfig) GetMetricsList() string { + metrics := []string{ + "bugs", + "vulnerabilities", + "code_smells", + } + if len(c.AdditionalMetrics) != 0 { + metrics = append(metrics, c.AdditionalMetrics...) + } + return strings.Join(metrics, ",") } From dfffd172233a4f553a45714a7994a9db45b936c7 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Tue, 12 Oct 2021 14:09:24 +0200 Subject: [PATCH 073/128] Open comment link in new tab Signed-off-by: Steven Kriegler --- internal/clients/sonarqube/sonarqube.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/clients/sonarqube/sonarqube.go b/internal/clients/sonarqube/sonarqube.go index be657b0..c834345 100644 --- a/internal/clients/sonarqube/sonarqube.go +++ b/internal/clients/sonarqube/sonarqube.go @@ -133,7 +133,7 @@ func (sdk *SonarQubeSdk) ComposeGiteaComment(data *CommentComposeData) (string, message := make([]string, 5) message[0] = GetRenderedQualityGate(data.QualityGate) message[1] = m.GetRenderedMarkdownTable() - message[2] = fmt.Sprintf("See [SonarQube](%s) for details.", data.Url) + message[2] = fmt.Sprintf(`See SonarQube for details.`, data.Url) message[3] = "---" message[4] = fmt.Sprintf("- If you want the bot to check again, post `%s`", actions.ActionReview) From 90581744ffa1ca869c128afafff9e6bbd020c1e1 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Wed, 13 Oct 2021 08:54:21 +0200 Subject: [PATCH 074/128] Set analysis status on PR creation Signed-off-by: Steven Kriegler --- README.md | 1 + internal/webhooks/gitea/pull.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 19f55fb..64b7885 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ See [SonarQube docs](https://docs.sonarqube.org/latest/project-administration/we - [x] Official image for containerized hosting - [x] Helm chart for Kubernetes - [ ] Publish Helm chart + docker image +- [x] Respect `"action": "opened"` PR event for updating status check ### Possible improvements diff --git a/internal/webhooks/gitea/pull.go b/internal/webhooks/gitea/pull.go index 8d7ff6a..5a71ad1 100644 --- a/internal/webhooks/gitea/pull.go +++ b/internal/webhooks/gitea/pull.go @@ -54,8 +54,8 @@ func (w *PullWebhook) Validate() error { return fmt.Errorf("ignore hook for non-configured project '%s/%s'", owner, name) } - if w.Action != "synchronized" { - return fmt.Errorf("ignore hook for action others than synchronized") + if w.Action != "synchronized" && w.Action != "opened" { + return fmt.Errorf("ignore hook for action others than 'opened' or 'synchronized'") } w.Repository = settings.GiteaRepository{ From d2424a38881e85623d026bc69b2a6994d8ecc679 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 17 Oct 2021 09:17:19 +0200 Subject: [PATCH 075/128] Migrate TODOs to Repo issues Signed-off-by: Steven Kriegler --- README.md | 42 ++++++++---------------------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 64b7885..bcc0f6b 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,6 @@ Luckily, both endpoints have a proper REST API to communicate with each others. - [SonarQube](#sonarqube) - [Gitea](#gitea) - [CI system](#ci-system) - - [TODOs](#todos) - - [Possible improvements](#possible-improvements) - [Contributing](#contributing) - [License](#license) - [Screenshots](#screenshots) @@ -27,14 +25,14 @@ Luckily, both endpoints have a proper REST API to communicate with each others. **Insights** - Bot activities - - extract data from SonarQube - - [x] Read payload from hook post to receive project,branch/pr,quality-gate - - [x] Load "api/measures/component" - - [x] comment PR in Gitea (/repos/{owner}/{repo}/issues/{index}/comments) - - [x] updates status check (either failing/success) - - [x] listen on "/sq-bot review" comments - - [x] comment PR in Gitea (/repos/{owner}/{repo}/issues/{index}/comments) - - [x] updates status check (either failing/success) + - Extract data from SonarQube + - Read payload from hook post to receive project,branch/pr,quality-gate + - Load "api/measures/component" + - Comment PR in Gitea (/repos/{owner}/{repo}/issues/{index}/comments) + - Updates status check (either failing/success) + - Listen on "/sq-bot review" comments + - Comment PR in Gitea (/repos/{owner}/{repo}/issues/{index}/comments) + - Updates status check (either failing/success) ## Requirements @@ -68,30 +66,6 @@ To mitigate that situation, the bot will look inside the `properties` object for key can contain the actual commit hash to use for updating the status in Gitea. See [SonarQube docs](https://docs.sonarqube.org/latest/project-administration/webhooks) for details. -## TODOs - -- [ ] Validate configuration on startup -- [ ] Verify webhook secrets -- [ ] Only post status-check (Opt-in/out) -- [ ] Maybe drop `PRBOT_CONFIG_PATH` environment variable in favor of `--config path/to/config.yaml` cli attribute -- [ ] Configure SonarQube PR branch naming pattern for more flexibility (currently focused on Jenkins with [Gitea Plugin](https://github.com/jenkinsci/gitea-plugin)) -- [ ] Configuration live reloading -- [ ] _Caching_ of outgoing requests in case the target is not available -- [ ] Parsable logging for monitoring -- [x] Official image for containerized hosting -- [x] Helm chart for Kubernetes -- [ ] Publish Helm chart + docker image -- [x] Respect `"action": "opened"` PR event for updating status check - -### Possible improvements - -- Reuse existing posted comment for updates via SonarQube webhook or `/sq-bot` comments -Therefore storing or dynamically retrieving the previous comment id and modify content (/repos/{owner}/{repo}/issues/comments/{id}) -- Add more information to posted comment - - Read "api/project_pull_requests" to get current issue counts and current state - - Load "api/issues/search" to get detailed information for unresolved issues -- Maybe directly show issues via review comments - ## Contributing Expected workflow is: Fork -> Patch -> Push -> Pull Request From 49087433fb2bc52df5e6d577d2ca0296d3c6a08c Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 17 Oct 2021 10:07:39 +0200 Subject: [PATCH 076/128] Introduce Makefile Signed-off-by: Steven Kriegler --- .gitignore | 1 + CONTRIBUTING.md | 21 +++++++++++++-------- Makefile | 44 ++++++++++++++++++++++++++++++++++++++++++++ go.sum | 17 +---------------- 4 files changed, 59 insertions(+), 24 deletions(-) create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index 9cf8818..aaafdbc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /.git/ /.idea/ +/.vscode/ /config/ /vendor/ /gitea-sonarqube-bot diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 143cad9..c064626 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,7 @@ - [Contribution Guidelines](#contribution-guidelines) - [Table of Contents](#table-of-contents) - [Setup development environment](#setup-development-environment) + - [Build and Run](#build-and-run) - [Testing](#testing) - [Release](#release) - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) @@ -17,22 +18,26 @@ docker build -t gitea-sonarqube-pr-bot/dev -f contrib/Dockerfile contrib # Start the environment docker run --rm -it -p 49182:3000 -v "$(pwd):/projects" gitea-sonarqube-pr-bot/dev +``` +## Build and Run + +```bash # Build the binary -go build ./cmd/gitea-sonarqube-bot - +make build # Start the server -./gitea-sonarqube-bot +make run + +# or all in once +make build run ``` ## Testing ```bash -# generic test execution -go test ./... - -# or with coverage report -go test -coverprofile cover.out ./... +make test +# or +make coverage ``` ## Release diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2e26473 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +BINARY_NAME=gitea-sonarqube-bot + +export GO111MODULE=on + +help: + @echo "Make Routines:" + @echo " - build Build the bot" + @echo " - run Start the bot" + @echo " - clean Delete generated files" + @echo " - test Run test suite" + @echo " - coverage Run test suite and generates coverage report as HTML file" + @echo " - dep Dependency maintenance (tidy, vendor, verify)" + @echo " - vet Examine Go source code and reports suspicious parts" + @echo " - fmt Format the Go code" + @echo " - help Print this help" + +build: + GOARCH=amd64 GOOS=linux go build --mod=vendor -o ${BINARY_NAME} ./cmd/gitea-sonarqube-bot/ + +run: + ./${BINARY_NAME} + +clean: + go clean + rm -f ${BINARY_NAME} + rm -f cover.out cover.html + +test: + go test -v ./... + +coverage: + go test -v -coverprofile=cover.out ./... + go tool cover -html=cover.out -o cover.html + +dep: + go mod tidy + go mod vendor + go mod verify + +vet: + go vet ./... + +fmt: + go fmt ./... diff --git a/go.sum b/go.sum index d896752..517c37b 100644 --- a/go.sum +++ b/go.sum @@ -97,15 +97,12 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A= github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= @@ -211,7 +208,6 @@ github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKEN github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -221,17 +217,14 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= @@ -244,7 +237,6 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -257,12 +249,10 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= @@ -280,6 +270,7 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -311,11 +302,9 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= @@ -349,7 +338,6 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -506,8 +494,6 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c h1:EyJTLQbOxvk8V6oDdD8ILR1BOs3nEJXThD6aqsiPNkM= -golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -712,7 +698,6 @@ google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+Rur google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 826204b667ba3830199b6e957848121952e63db6 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 17 Oct 2021 10:53:54 +0200 Subject: [PATCH 077/128] Allow for running dedicated tests Signed-off-by: Steven Kriegler --- Makefile | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 2e26473..db21fe7 100644 --- a/Makefile +++ b/Makefile @@ -7,15 +7,17 @@ help: @echo " - build Build the bot" @echo " - run Start the bot" @echo " - clean Delete generated files" - @echo " - test Run test suite" - @echo " - coverage Run test suite and generates coverage report as HTML file" + @echo " - test Run full test suite" + @echo " - test p=./path/to/package Run test suite for specific package" + @echo " - test\#SpecificTestName Run a specific" + @echo " - coverage Run full test suite and generates coverage report as HTML file" @echo " - dep Dependency maintenance (tidy, vendor, verify)" @echo " - vet Examine Go source code and reports suspicious parts" @echo " - fmt Format the Go code" @echo " - help Print this help" build: - GOARCH=amd64 GOOS=linux go build --mod=vendor -o ${BINARY_NAME} ./cmd/gitea-sonarqube-bot/ + GOARCH=amd64 GOOS=linux go build -mod=vendor -o ${BINARY_NAME} ./cmd/gitea-sonarqube-bot/ run: ./${BINARY_NAME} @@ -26,10 +28,17 @@ clean: rm -f cover.out cover.html test: - go test -v ./... +ifdef p + go test -v -mod=vendor $(p) +else + go test -v -mod=vendor ./... +endif + +test\#%: + go test -mod=vendor -run $(subst .,/,$*) ./... coverage: - go test -v -coverprofile=cover.out ./... + go test -coverprofile=cover.out ./... go tool cover -html=cover.out -o cover.html dep: From 8a7e9f83fafa450580bf0c66e2c4b680fa771113 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 17 Oct 2021 11:12:50 +0200 Subject: [PATCH 078/128] Refactor action validation Signed-off-by: Steven Kriegler --- internal/actions/actions.go | 14 ++++++++++++++ internal/actions/actions_test.go | 17 +++++++++++++++++ internal/webhooks/gitea/comment.go | 9 ++------- 3 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 internal/actions/actions_test.go diff --git a/internal/actions/actions.go b/internal/actions/actions.go index 28ec2f5..166098d 100644 --- a/internal/actions/actions.go +++ b/internal/actions/actions.go @@ -1,8 +1,22 @@ package actions +import "strings" + type BotAction string const ( ActionReview BotAction = "/sq-bot review" ActionPrefix string = "/sq-bot" ) + +func IsValidBotComment(c string) bool { + if !strings.HasPrefix(c, ActionPrefix) { + return false + } + + if c != string(ActionReview) { + return false + } + + return true +} diff --git a/internal/actions/actions_test.go b/internal/actions/actions_test.go new file mode 100644 index 0000000..14e13c7 --- /dev/null +++ b/internal/actions/actions_test.go @@ -0,0 +1,17 @@ +package actions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsValidBotCommentForInvalidComment(t *testing.T) { + assert.False(t, IsValidBotComment(""), "Undetected missing action prefix") + assert.False(t, IsValidBotComment("/sq-bot invalid-command"), "Undetected invalid bot command") + assert.False(t, IsValidBotComment("Some context with /sq-bot review within"), "Incorrect bot prefix detected inside random comment") +} + +func TestIsValidBotCommentForValidComment(t *testing.T) { + assert.True(t, IsValidBotComment("/sq-bot review"), "Correct bot comment not recognized") +} diff --git a/internal/webhooks/gitea/comment.go b/internal/webhooks/gitea/comment.go index 5cbe899..4daf6c9 100644 --- a/internal/webhooks/gitea/comment.go +++ b/internal/webhooks/gitea/comment.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "log" - "strings" "gitea-sonarqube-pr-bot/internal/actions" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" @@ -55,12 +54,8 @@ func (w *CommentWebhook) Validate() error { return fmt.Errorf("ignore hook for action others than created") } - if !strings.HasPrefix(w.Comment.Body, actions.ActionPrefix) { - return fmt.Errorf("ignore hook for non-bot action comment") - } - - if w.Comment.Body != string(actions.ActionReview) { - return fmt.Errorf("ignore hook for unknown bot action") + if !actions.IsValidBotComment(w.Comment.Body) { + return fmt.Errorf("ignore hook for non-bot action comment or unknown action") } w.ConfiguredProject = settings.Projects[pIdx] From 24e4249411b8441b1c1c82e73f9ccf4c0480914a Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 17 Oct 2021 12:16:42 +0200 Subject: [PATCH 079/128] Add SonarQube analysis Signed-off-by: Steven Kriegler --- .gitignore | 2 ++ Makefile | 5 ++++- README.md | 2 ++ sonar-project.properties | 15 +++++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 sonar-project.properties diff --git a/.gitignore b/.gitignore index aaafdbc..c62d6fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.git/ /.idea/ /.vscode/ +/.scannerwork/ /config/ /vendor/ /gitea-sonarqube-bot @@ -8,3 +9,4 @@ /*.log /cover.out /cover.html +/test-report.out diff --git a/Makefile b/Makefile index db21fe7..edf1d01 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ run: clean: go clean rm -f ${BINARY_NAME} - rm -f cover.out cover.html + rm -f cover.out cover.html test-report.out test: ifdef p @@ -37,6 +37,9 @@ endif test\#%: go test -mod=vendor -run $(subst .,/,$*) ./... +test-ci: + go test -mod=vendor -coverprofile=cover.out -json ./... > test-report.out + coverage: go test -coverprofile=cover.out ./... go tool cover -html=cover.out -o cover.html diff --git a/README.md b/README.md index bcc0f6b..a9dd18e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Gitea SonarQube Bot +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=gitea-sonarqube-bot&metric=alert_status)](https://sonarcloud.io/dashboard?id=gitea-sonarqube-bot) + _Gitea SonarQube Bot_ is a bot that receives messages from both SonarQube and Gitea to help developers being productive. The idea behind this project is the missing ALM integration of Gitea in SonarQube. Unfortunately, this [won't be added in near future](https://github.com/SonarSource/sonarqube/pull/3248#issuecomment-701334327). diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..d12c673 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,15 @@ +sonar.sourceEncoding=UTF-8 + +sonar.host.url=https://sonarcloud.io +sonar.organization=justusbunsi +sonar.projectKey=gitea-sonarqube-bot +sonar.projectName=Gitea SonarQube Bot + +sonar.sources=. +sonar.exclusions=**/*_test.go,contrib/**,docker/**,docs/**,helm/** + +sonar.tests=. +sonar.test.inclusions=**/*_test.go + +sonar.go.tests.reportPaths=test-report.out +sonar.go.coverage.reportPaths=cover.out From 021d01b5fb184c723a47744a27816ac2dfda392c Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 17 Oct 2021 15:43:25 +0200 Subject: [PATCH 080/128] Remove useless types Signed-off-by: Steven Kriegler --- internal/api/sonarqube_test.go | 10 +++++----- internal/settings/settings_test.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index 5d4a3fc..cc6794b 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -84,7 +84,7 @@ func withValidRequestData(t *testing.T, mockPreparation func(*HandlerPartialMock func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { settings.Projects = []settings.Project{ - settings.Project{ + { SonarQube: struct{ Key string }{ Key: "pr-bot", }, @@ -99,7 +99,7 @@ func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { settings.Projects = []settings.Project{ - settings.Project{ + { SonarQube: struct{ Key string }{ Key: "another-project", }, @@ -114,7 +114,7 @@ func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { func TestHandleSonarQubeWebhookInvalidJSONBody(t *testing.T) { settings.Projects = []settings.Project{ - settings.Project{ + { SonarQube: struct{ Key string }{ Key: "pr-bot", }, @@ -130,7 +130,7 @@ func TestHandleSonarQubeWebhookInvalidJSONBody(t *testing.T) { func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { settings.Projects = []settings.Project{ - settings.Project{ + { SonarQube: struct{ Key string }{ Key: "pr-bot", }, @@ -147,7 +147,7 @@ func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { func TestHandleSonarQubeWebhookForBranch(t *testing.T) { settings.Projects = []settings.Project{ - settings.Project{ + { SonarQube: struct{ Key string }{ Key: "pr-bot", }, diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 051c27f..8c5522a 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -253,7 +253,7 @@ func TestLoadProjectsStructure(t *testing.T) { Load(os.TempDir()) expectedProjects := []Project{ - Project{ + { SonarQube: struct{ Key string }{ Key: "gitea-sonarqube-pr-bot", }, From e01096a7fe6167a39daadfa251526bbcde222a19 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 17 Oct 2021 15:57:45 +0200 Subject: [PATCH 081/128] Eliminate viper references from token and webhook Signed-off-by: Steven Kriegler --- internal/settings/settings.go | 8 ++++---- internal/settings/token.go | 8 +++----- internal/settings/webhook.go | 8 +++----- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 918cd72..d0238a9 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -64,13 +64,13 @@ func Load(configPath string) { Gitea = giteaConfig{ Url: r.GetString("gitea.url"), - Token: NewToken(r, "gitea", errCallback), - Webhook: NewWebhook(r, "gitea", errCallback), + Token: NewToken(r.GetString, "gitea", errCallback), + Webhook: NewWebhook(r.GetString, "gitea", errCallback), } SonarQube = sonarQubeConfig{ Url: r.GetString("sonarqube.url"), - Token: NewToken(r, "sonarqube", errCallback), - Webhook: NewWebhook(r, "sonarqube", errCallback), + Token: NewToken(r.GetString, "sonarqube", errCallback), + Webhook: NewWebhook(r.GetString, "sonarqube", errCallback), AdditionalMetrics: r.GetStringSlice("sonarqube.additionalMetrics"), } } diff --git a/internal/settings/token.go b/internal/settings/token.go index be7ce93..6905656 100644 --- a/internal/settings/token.go +++ b/internal/settings/token.go @@ -3,8 +3,6 @@ package settings import ( "fmt" "io/ioutil" - - "github.com/spf13/viper" ) type token struct { @@ -26,10 +24,10 @@ func (t *token) lookupSecret(errCallback func(string)) { t.Value = string(content) } -func NewToken(v *viper.Viper, confContainer string, errCallback func(string)) *token { +func NewToken(extractor func(string) string, confContainer string, errCallback func(string)) *token { t := &token{ - Value: v.GetString(fmt.Sprintf("%s.token.value", confContainer)), - file: v.GetString(fmt.Sprintf("%s.token.file", confContainer)), + Value: extractor(fmt.Sprintf("%s.token.value", confContainer)), + file: extractor(fmt.Sprintf("%s.token.file", confContainer)), } t.lookupSecret(errCallback) diff --git a/internal/settings/webhook.go b/internal/settings/webhook.go index 64333f4..0b8374d 100644 --- a/internal/settings/webhook.go +++ b/internal/settings/webhook.go @@ -3,8 +3,6 @@ package settings import ( "fmt" "io/ioutil" - - "github.com/spf13/viper" ) type webhook struct { @@ -26,10 +24,10 @@ func (w *webhook) lookupSecret(errCallback func(string)) { w.Secret = string(content) } -func NewWebhook(v *viper.Viper, confContainer string, errCallback func(string)) *webhook { +func NewWebhook(extractor func(string) string, confContainer string, errCallback func(string)) *webhook { w := &webhook{ - Secret: v.GetString(fmt.Sprintf("%s.webhook.secret", confContainer)), - secretFile: v.GetString(fmt.Sprintf("%s.webhook.secretFile", confContainer)), + Secret: extractor(fmt.Sprintf("%s.webhook.secret", confContainer)), + secretFile: extractor(fmt.Sprintf("%s.webhook.secretFile", confContainer)), } w.lookupSecret(errCallback) From 21837f9b25feb54302333e5fc7688ba823c611cc Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 17 Oct 2021 16:01:15 +0200 Subject: [PATCH 082/128] Export webhook and token struct Signed-off-by: Steven Kriegler --- internal/settings/gitea.go | 4 ++-- internal/settings/settings_test.go | 28 ++++++++++++++-------------- internal/settings/sonarqube.go | 4 ++-- internal/settings/token.go | 8 ++++---- internal/settings/webhook.go | 8 ++++---- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/internal/settings/gitea.go b/internal/settings/gitea.go index 375efcc..b095e42 100644 --- a/internal/settings/gitea.go +++ b/internal/settings/gitea.go @@ -7,6 +7,6 @@ type GiteaRepository struct { type giteaConfig struct { Url string - Token *token - Webhook *webhook + Token *Token + Webhook *Webhook } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 8c5522a..3df25d6 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -58,10 +58,10 @@ func TestLoadGiteaStructure(t *testing.T) { expected := giteaConfig{ Url: "https://example.com/gitea", - Token: &token{ + Token: &Token{ Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", }, - Webhook: &webhook{ + Webhook: &Webhook{ Secret: "haxxor-gitea-secret", }, } @@ -77,10 +77,10 @@ func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { expected := giteaConfig{ Url: "https://example.com/gitea", - Token: &token{ + Token: &Token{ Value: "injected-token", }, - Webhook: &webhook{ + Webhook: &Webhook{ Secret: "injected-webhook-secret", }, } @@ -99,10 +99,10 @@ func TestLoadSonarQubeStructure(t *testing.T) { expected := sonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: &token{ + Token: &Token{ Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", }, - Webhook: &webhook{ + Webhook: &Webhook{ Secret: "haxxor-sonarqube-secret", }, } @@ -133,10 +133,10 @@ projects: expected := sonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: &token{ + Token: &Token{ Value: "fake-sonarqube-token", }, - Webhook: &webhook{ + Webhook: &Webhook{ Secret: "", }, AdditionalMetrics: []string{ @@ -157,10 +157,10 @@ func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { expected := sonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: &token{ + Token: &Token{ Value: "injected-token", }, - Webhook: &webhook{ + Webhook: &Webhook{ Secret: "injected-webhook-secret", }, } @@ -209,11 +209,11 @@ projects: expectedGitea := giteaConfig{ Url: "https://example.com/gitea", - Token: &token{ + Token: &Token{ Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", file: giteaTokenFile, }, - Webhook: &webhook{ + Webhook: &Webhook{ Secret: "gitea-totally-secret", secretFile: giteaWebhookSecretFile, }, @@ -221,11 +221,11 @@ projects: expectedSonarQube := sonarQubeConfig{ Url: "https://example.com/sonarqube", - Token: &token{ + Token: &Token{ Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", file: sonarqubeTokenFile, }, - Webhook: &webhook{ + Webhook: &Webhook{ Secret: "sonarqube-totally-secret", secretFile: sonarqubeWebhookSecretFile, }, diff --git a/internal/settings/sonarqube.go b/internal/settings/sonarqube.go index 1c7fde2..645f9d2 100644 --- a/internal/settings/sonarqube.go +++ b/internal/settings/sonarqube.go @@ -4,8 +4,8 @@ import "strings" type sonarQubeConfig struct { Url string - Token *token - Webhook *webhook + Token *Token + Webhook *Webhook AdditionalMetrics []string } diff --git a/internal/settings/token.go b/internal/settings/token.go index 6905656..0c47dfd 100644 --- a/internal/settings/token.go +++ b/internal/settings/token.go @@ -5,12 +5,12 @@ import ( "io/ioutil" ) -type token struct { +type Token struct { Value string file string } -func (t *token) lookupSecret(errCallback func(string)) { +func (t *Token) lookupSecret(errCallback func(string)) { if t.file == "" { return } @@ -24,8 +24,8 @@ func (t *token) lookupSecret(errCallback func(string)) { t.Value = string(content) } -func NewToken(extractor func(string) string, confContainer string, errCallback func(string)) *token { - t := &token{ +func NewToken(extractor func(string) string, confContainer string, errCallback func(string)) *Token { + t := &Token{ Value: extractor(fmt.Sprintf("%s.token.value", confContainer)), file: extractor(fmt.Sprintf("%s.token.file", confContainer)), } diff --git a/internal/settings/webhook.go b/internal/settings/webhook.go index 0b8374d..53f50e8 100644 --- a/internal/settings/webhook.go +++ b/internal/settings/webhook.go @@ -5,12 +5,12 @@ import ( "io/ioutil" ) -type webhook struct { +type Webhook struct { Secret string secretFile string } -func (w *webhook) lookupSecret(errCallback func(string)) { +func (w *Webhook) lookupSecret(errCallback func(string)) { if w.secretFile == "" { return } @@ -24,8 +24,8 @@ func (w *webhook) lookupSecret(errCallback func(string)) { w.Secret = string(content) } -func NewWebhook(extractor func(string) string, confContainer string, errCallback func(string)) *webhook { - w := &webhook{ +func NewWebhook(extractor func(string) string, confContainer string, errCallback func(string)) *Webhook { + w := &Webhook{ Secret: extractor(fmt.Sprintf("%s.webhook.secret", confContainer)), secretFile: extractor(fmt.Sprintf("%s.webhook.secretFile", confContainer)), } From afd523c9cdb38f12d60652e5d52ae78abcb34ede Mon Sep 17 00:00:00 2001 From: tuongvi9911 Date: Sun, 24 Oct 2021 18:14:55 +0200 Subject: [PATCH 083/128] Fix permission denied (#14) error handling at runtime starting container process caused: exec: "/usr/local/bin/docker-entrypoint.sh": permission denied: unknown. Reviewed-on: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/pulls/14 Co-authored-by: tuongvi9911 Co-committed-by: tuongvi9911 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 68f5c1f..162b0bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,6 +42,6 @@ EXPOSE 3000 ENV GIN_MODE "release" VOLUME ["/home/bot/config/"] - +RUN ["chmod", "+x", "/usr/local/bin/docker-entrypoint.sh"] ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] CMD [] From f85bfb4bf52afa04d96120b251c0a3ddde75bc9e Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Fri, 12 Nov 2021 08:30:37 +0100 Subject: [PATCH 084/128] Fix helm chart service account template Signed-off-by: Steven Kriegler --- helm/templates/serviceaccount.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/templates/serviceaccount.yaml b/helm/templates/serviceaccount.yaml index 5c51ef1..785b1a5 100644 --- a/helm/templates/serviceaccount.yaml +++ b/helm/templates/serviceaccount.yaml @@ -9,5 +9,5 @@ metadata: annotations: {{- toYaml . | nindent 4 }} {{- end }} -{{- end }} automountServiceAccountToken: false +{{- end }} From 7f68b52076b86dcf523a3edc79663a6885f1af48 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 18 Dec 2021 23:03:47 +0100 Subject: [PATCH 085/128] Extend requirements for Community Edition (#16) Fixes #15 Signed-off-by: Steven Kriegler Reviewed-on: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/pulls/16 Co-authored-by: justusbunsi Co-committed-by: justusbunsi --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a9dd18e..7ba6320 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Luckily, both endpoints have a proper REST API to communicate with each others. ## Requirements -This bot is designed to interact with [SonarQube _Developer_ edition](https://www.sonarsource.com/plans-and-pricing/) and above due to its pull request features. It will most likely work with public SonarCloud because it includes that feature for open source projects. +This bot is designed to perform SonarQube/SonarCloud API requests specific for pull requests. This feature is available in the _Community_ edition via [Sonarqube Community Branch Plugin](https://github.com/mc1arke/sonarqube-community-branch-plugin) or natively in [SonarQube _Developer_ edition](https://www.sonarsource.com/plans-and-pricing/) and above. ## Bot configuration From e4ff25a193bdef98e4f7a12e94d163fac02327db Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Fri, 13 May 2022 20:48:47 +0200 Subject: [PATCH 086/128] Add Docker image instructions Signed-off-by: Steven Kriegler --- CONTRIBUTING.md | 7 +++++++ README.md | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c064626..585a9f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,6 +48,13 @@ For local purposes docker build -t gitea-sonarqube-pr-bot/prod . ``` +For actual release builds + +```bash +docker build -t justusbunsi/gitea-sonarqube-bot:$TAG . +docker push justusbunsi/gitea-sonarqube-bot:$TAG +``` + ## Developer Certificate of Origin (DCO) I consider the act of contributing to the code by submitting a Pull Request as the "Sign off" or agreement to the diff --git a/README.md b/README.md index 7ba6320..e58deb7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Gitea SonarQube Bot [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=gitea-sonarqube-bot&metric=alert_status)](https://sonarcloud.io/dashboard?id=gitea-sonarqube-bot) +![Docker Image Version (latest semver)](https://img.shields.io/docker/v/justusbunsi/gitea-sonarqube-bot?logo=docker) _Gitea SonarQube Bot_ is a bot that receives messages from both SonarQube and Gitea to help developers being productive. The idea behind this project is the missing ALM integration of Gitea in SonarQube. Unfortunately, @@ -12,6 +13,8 @@ Luckily, both endpoints have a proper REST API to communicate with each others. - [Workflow](#workflow) - [Requirements](#requirements) - [Bot configuration](#bot-configuration) + - [Installation](#installation) + - [Docker](#docker) - [Setup](#setup) - [SonarQube](#sonarqube) - [Gitea](#gitea) @@ -44,6 +47,17 @@ This bot is designed to perform SonarQube/SonarCloud API requests specific for p See [config.example.yaml](config/config.example.yaml) for a full configuration specification and description. +## Installation + +### Docker + +Create a directory `config` and place your [config.yaml](config/config.example.yaml) inside it. Open a terminal next to this directory +and execute the following (replace `$TAG` first): + +```bash +docker run --rm -it -p 9000:3000 -v "$(pwd)/config/:/home/bot/config/" justusbunsi/gitea-sonarqube-bot:$TAG +``` + ## Setup ### SonarQube From 3c76c8d904a136ac5bc56f3ce8176380df61a713 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Fri, 13 May 2022 20:51:51 +0200 Subject: [PATCH 087/128] Fix Docker Hub link Signed-off-by: Steven Kriegler --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e58deb7..d5b4ff9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Gitea SonarQube Bot [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=gitea-sonarqube-bot&metric=alert_status)](https://sonarcloud.io/dashboard?id=gitea-sonarqube-bot) -![Docker Image Version (latest semver)](https://img.shields.io/docker/v/justusbunsi/gitea-sonarqube-bot?logo=docker) +[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/justusbunsi/gitea-sonarqube-bot?logo=docker)](https://hub.docker.com/r/justusbunsi/gitea-sonarqube-bot) _Gitea SonarQube Bot_ is a bot that receives messages from both SonarQube and Gitea to help developers being productive. The idea behind this project is the missing ALM integration of Gitea in SonarQube. Unfortunately, From 9175bacefa1cef32bbd9f386f2fe6107ba4bb4d8 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Fri, 13 May 2022 22:43:38 +0200 Subject: [PATCH 088/128] Improve Chart release specification Signed-off-by: Steven Kriegler --- helm/Chart.yaml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 42e03eb..97d8d56 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -3,4 +3,17 @@ name: gitea-sonarqube-bot description: A Helm Chart for running a bot to communicate between both Gitea and SonarQube type: application version: 0.1.0 -appVersion: "0.1.0" +appVersion: "v0.1.0" +keywords: + - code review + - git + - gitea + - pull request + - sonarqube + - sonarcloud +sources: + - https://codeberg.org/justusbunsi/gitea-sonarqube-bot/ + - https://hub.docker.com/r/justusbunsi/gitea-sonarqube-bot/ +maintainers: + - name: Steven Kriegler + email: sk.bunsenbrenner@gmail.com From b57cc4b23ebbd68d44a91d4a9909c8630f94307e Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 14 May 2022 01:00:14 +0200 Subject: [PATCH 089/128] Publish 0.1.0 Signed-off-by: Steven Kriegler --- gitea-sonarqube-bot-0.1.0.tgz | Bin 0 -> 6721 bytes index.yaml | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 gitea-sonarqube-bot-0.1.0.tgz create mode 100644 index.yaml diff --git a/gitea-sonarqube-bot-0.1.0.tgz b/gitea-sonarqube-bot-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..2a8620cba3a4782edc55ccc2daab9f6a0a5d70df GIT binary patch literal 6721 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 Date: Sat, 14 May 2022 01:10:07 +0200 Subject: [PATCH 090/128] Prepare Helm Chart release - Auto-generate parameters documentation - Provide required `make` commands - Update contribution environment to match new requirements Signed-off-by: Steven Kriegler --- .gitignore | 2 + CONTRIBUTING.md | 20 +- Makefile | 12 ++ README.md | 5 + contrib/Dockerfile | 7 +- helm/.helmignore | 1 + helm/README.md | 79 ++++++++ helm/readme-generator-config.json | 19 ++ helm/values.yaml | 130 +++++++++---- package-lock.json | 307 ++++++++++++++++++++++++++++++ package.json | 23 +++ 11 files changed, 564 insertions(+), 41 deletions(-) create mode 100644 helm/README.md create mode 100644 helm/readme-generator-config.json create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index c62d6fe..a3538d3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ /.scannerwork/ /config/ /vendor/ +/node_modules/ +/helm-releases/ /gitea-sonarqube-bot /coverage.html /*.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 585a9f1..c14df07 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ - [Setup development environment](#setup-development-environment) - [Build and Run](#build-and-run) - [Testing](#testing) + - [Helm Chart](#helm-chart) - [Release](#release) - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) @@ -40,6 +41,15 @@ make test make coverage ``` +## Helm Chart + +The [Parameters section](helm/README.md#parameters) is auto-generated using [readme-generator-for-helm](https://github.com/bitnami-labs/readme-generator-for-helm). +When modifying anything in the `helm` directory, remember to update the documentation by running + +```bash +make helm-params +``` + ## Release For local purposes @@ -48,13 +58,21 @@ For local purposes docker build -t gitea-sonarqube-pr-bot/prod . ``` -For actual release builds +**Docker image** ```bash docker build -t justusbunsi/gitea-sonarqube-bot:$TAG . docker push justusbunsi/gitea-sonarqube-bot:$TAG ``` +**Helm Chart** + +```bash +make helm-pack +``` + +Use the two files in `helm-releases` and push them to the `charts` branch. + ## Developer Certificate of Origin (DCO) I consider the act of contributing to the code by submitting a Pull Request as the "Sign off" or agreement to the diff --git a/Makefile b/Makefile index edf1d01..9277ba6 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ help: @echo " - test p=./path/to/package Run test suite for specific package" @echo " - test\#SpecificTestName Run a specific" @echo " - coverage Run full test suite and generates coverage report as HTML file" + @echo " - helm-params Auto-generates 'Parameters' section of 'helm/README.md' based on comments in values.yaml" + @echo " - helm-pack Prepares Helm Chart release artifacts for pushing to 'charts' branch" @echo " - dep Dependency maintenance (tidy, vendor, verify)" @echo " - vet Examine Go source code and reports suspicious parts" @echo " - fmt Format the Go code" @@ -44,6 +46,16 @@ coverage: go test -coverprofile=cover.out ./... go tool cover -html=cover.out -o cover.html +helm-params: + npm install + npm run helm-params + +helm-pack: + rm -rf ./helm-releases/ + helm package ./helm/ -d ./helm-releases/ + curl -L -o ./helm-releases/index.yaml https://codeberg.org/justusbunsi/gitea-sonarqube-bot/raw/branch/charts/index.yaml + helm repo index ./helm-releases/ --url https://codeberg.org/justusbunsi/gitea-sonarqube-bot/raw/branch/charts/ --merge ./helm-releases/index.yaml + dep: go mod tidy go mod vendor diff --git a/README.md b/README.md index d5b4ff9..4aa37ed 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Luckily, both endpoints have a proper REST API to communicate with each others. - [Bot configuration](#bot-configuration) - [Installation](#installation) - [Docker](#docker) + - [Helm Chart](#helm-chart) - [Setup](#setup) - [SonarQube](#sonarqube) - [Gitea](#gitea) @@ -58,6 +59,10 @@ and execute the following (replace `$TAG` first): docker run --rm -it -p 9000:3000 -v "$(pwd)/config/:/home/bot/config/" justusbunsi/gitea-sonarqube-bot:$TAG ``` +### Helm Chart + +See [Helm Chart README](helm/README.md) for detailed instructions. + ## Setup ### SonarQube diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 8d06f64..0894d1d 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -1,6 +1,11 @@ FROM golang:1.17-alpine3.14 -RUN apk --no-cache add build-base git bash +RUN apk --no-cache add build-base git bash curl openssl npm + +RUN curl -fsSL -o ./get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 \ + && chmod 700 ./get_helm.sh \ + && ./get_helm.sh \ + && rm ./get_helm.sh WORKDIR /projects diff --git a/helm/.helmignore b/helm/.helmignore index 0e8a0eb..242d656 100644 --- a/helm/.helmignore +++ b/helm/.helmignore @@ -21,3 +21,4 @@ .idea/ *.tmproj .vscode/ +readme-generator-config.json diff --git a/helm/README.md b/helm/README.md new file mode 100644 index 0000000..ab8c709 --- /dev/null +++ b/helm/README.md @@ -0,0 +1,79 @@ +# Gitea SonarQube Bot + +## Installation + +```bash +helm repo add gitea-sonarqube-bot https://codeberg.org/justusbunsi/gitea-sonarqube-bot/raw/branch/charts/ +helm repo update +helm install gitea-sonarqube-bot gitea-sonarqube-bot/gitea-sonarqube-bot +``` + +You have to modify the `app.configuration` values. Otherwise, the bot won't start as it tries to establish a connection +to your Gitea instance. See [config.example.yaml](https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/config/config.example.yaml) +for full configuration options. + +## Parameters + +### Common parameters + +| Name | Description | Value | +| -------------------- | -------------------------------------------------------------------------------------------- | --------------------------------- | +| `replicaCount` | Number of replicas for the bot | `1` | +| `image.repository` | Image repository | `justusbunsi/gitea-sonarqube-bot` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `image.tag` | Image tag (Overrides the image tag whose default is the chart `appVersion`) | `""` | +| `imagePullSecrets` | Specify docker-registry secret names as an array | `[]` | +| `nameOverride` | String to partially override common.names.fullname template (will maintain the release name) | `""` | +| `fullnameOverride` | String to fully override common.names.fullname template | `""` | +| `resources.limits` | The resources limits for the container | `{}` | +| `resources.requests` | The requested resources for the container | `{}` | +| `nodeSelector` | Node labels for pod assignment. Evaluated as a template. | `{}` | +| `tolerations` | Tolerations for pod assignment. Evaluated as a template. | `[]` | +| `affinity` | Affinity for pod assignment. Evaluated as a template. | `{}` | +| `podAnnotations` | Pod annotations. | `{}` | + + +### App parameters + +| Name | Description | Value | +| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` | +| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` | +| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` | +| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` | +| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` | +| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` | +| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` | +| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` | +| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | +| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | + + +### Security parameters + +| Name | Description | Value | +| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------ | +| `serviceAccount.create` | Specifies whether a service account should be created | `true` | +| `serviceAccount.annotations` | Annotations to add to the service account | `{}` | +| `serviceAccount.name` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | `""` | +| `podSecurityContext.fsGroup` | Group ID for the container | `1000` | +| `securityContext.readOnlyRootFilesystem` | Mounts the container's root filesystem as read-only | `true` | +| `securityContext.runAsNonRoot` | Avoid running as root user | `true` | +| `securityContext.runAsUser` | User ID for the container | `1000` | + + +### Traffic exposure parameters + +| Name | Description | Value | +| ------------------------------------ | ------------------------------------------------------------------------------------- | ------------------------ | +| `service.type` | Service type | `ClusterIP` | +| `service.port` | Service port | `80` | +| `ingress.enabled` | Enable ingress controller resource | `false` | +| `ingress.className` | IngressClass that will be be used to implement the Ingress (Kubernetes 1.18+) | `""` | +| `ingress.annotations` | Additional annotations for the Ingress resource. | `{}` | +| `ingress.hosts[0].host` | Host for the ingress resource | `sqbot.example.com` | +| `ingress.hosts[0].paths[0].path` | The path to the bot endpoint | `/` | +| `ingress.hosts[0].paths[0].pathType` | Ingress path type | `ImplementationSpecific` | +| `ingress.tls` | The tls configuration for additional hostnames to be covered with configured ingress. | `[]` | + + diff --git a/helm/readme-generator-config.json b/helm/readme-generator-config.json new file mode 100644 index 0000000..a0ade9c --- /dev/null +++ b/helm/readme-generator-config.json @@ -0,0 +1,19 @@ +{ + "comments": { + "format": "#" + }, + "tags": { + "param": "@param", + "section": "@section", + "skip": "@skip", + "extra": "@extra" + }, + "modifiers": { + "array": "array", + "object": "object", + "string": "string" + }, + "regexp": { + "paramsSectionTitle": "Parameters" + } +} diff --git a/helm/values.yaml b/helm/values.yaml index aec6e51..6ac32a8 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -1,25 +1,68 @@ -# Default values for helm. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. +# @section Common parameters +# @param replicaCount Number of replicas for the bot replicaCount: 1 +# ref: https://hub.docker.com/r/justusbunsi/gitea-sonarqube-bot/tags/ +# @param image.repository Image repository +# @param image.pullPolicy Image pull policy +# @param image.tag Image tag (Overrides the image tag whose default is the chart `appVersion`) image: repository: justusbunsi/gitea-sonarqube-bot pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. tag: "" +# @param imagePullSecrets Specify docker-registry secret names as an array +imagePullSecrets: [] + +# @param nameOverride String to partially override common.names.fullname template (will maintain the release name) +nameOverride: "" + +# @param fullnameOverride String to fully override common.names.fullname template +fullnameOverride: "" + +# We usually recommend not to specify default resources and to leave this as a conscious +# choice for the user. This also increases chances charts run on environments with little +# resources, such as Minikube. If you do want to specify resources, uncomment the following +# lines, adjust them as necessary, and remove the curly braces after 'resources:'. +# @param resources.limits The resources limits for the container +# @param resources.requests The requested resources for the container +resources: + limits: {} + # cpu: 100m + # memory: 128Mi + requests: {} + # cpu: 100m + # memory: 128Mi + +# @param nodeSelector Node labels for pod assignment. Evaluated as a template. +# ref: https://kubernetes.io/docs/user-guide/node-selection/ +nodeSelector: {} + +# @param tolerations Tolerations for pod assignment. Evaluated as a template. +# ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +tolerations: [] + +# @param affinity Affinity for pod assignment. Evaluated as a template. +# ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity +affinity: {} + +# @param podAnnotations Pod annotations. +podAnnotations: {} + +# @section App parameters + app: - # This object represents the config.yaml provided to the application + # This object represents the [config.yaml](https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/config/config.example.yaml) provided to the application. configuration: # Gitea related configuration. Necessary for adding/updating comments on repository pull requests gitea: - # Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. + # @param app.configuration.gitea.url Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. url: "" # Created access token for the user that shall be used as bot account. # User needs "Read project" permissions with access to "Pull Requests" + # @param app.configuration.gitea.token.value Gitea token as plain text. Can be replaced with `file` key containing path to file. token: value: "" # # or path to file containing the plain text secret @@ -29,6 +72,7 @@ app: # request will be ignored. # The bot looks for `X-Gitea-Signature` header containing the sha256 hmac hash of the plain text secret. If the header # exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be validated. + # @skip app.configuration.gitea.webhook webhook: secret: "" # # or path to file containing the plain text secret @@ -36,11 +80,12 @@ app: # SonarQube related configuration. Necessary for requesting data from the API and processing the webhook. sonarqube: - # Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. + # @param app.configuration.sonarqube.url Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. url: "" # Created access token for the user that shall be used as bot account. # User needs "Browse on project" permissions + # @param app.configuration.sonarqube.token.value SonarQube token as plain text. Can be replaced with `file` key containing path to file. token: value: "" # # or path to file containing the plain text secret @@ -51,6 +96,7 @@ app: # The bot looks for `X-Sonar-Webhook-HMAC-SHA256` header containing the sha256 hmac hash of the plain text secret. # If the header exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be # validated. + # @skip app.configuration.sonarqube.webhook webhook: secret: "" # # or path to file containing the plain text secret @@ -58,12 +104,15 @@ app: # Some useful metrics depend on the edition in use. There are various ones like code_smells, vulnerabilities, bugs, etc. # By default the bot will extract "bugs,vulnerabilities,code_smells" - # Setting this option you can extend that default list by your own metrics. + # @param app.configuration.sonarqube.additionalMetrics Setting this option you can extend that default list by your own metrics. additionalMetrics: [] # - "new_security_hotspots" # List of project mappings to take care of. Webhooks for other projects will be ignored. # At least one must be configured. Otherwise all webhooks (no matter which source) because the bot cannot map on its own. + # @param app.configuration.projects[0].sonarqube.key Project key inside SonarQube + # @param app.configuration.projects[0].gitea.owner Repository owner inside Gitea + # @param app.configuration.projects[0].gitea.name Repository name inside Gitea projects: - sonarqube: key: "" @@ -73,7 +122,7 @@ app: owner: "" name: "" -# If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly +# @param volumes If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly volumes: [] # - name: gitea-connection # secret: @@ -82,7 +131,7 @@ volumes: [] # secret: # secretName: sonarqube-secret-with-token-and-maybe-webhook-secret -# If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly +# @param volumeMounts If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly volumeMounts: [] # - name: gitea-connection # readOnly: true @@ -91,24 +140,25 @@ volumeMounts: [] # readOnly: true # mountPath: "/bot/secrets/sonarqube/" -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" +# @section Security parameters serviceAccount: - # Specifies whether a service account should be created + # @param serviceAccount.create Specifies whether a service account should be created create: true - # Annotations to add to the service account + # @param serviceAccount.annotations Annotations to add to the service account annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template + # @param serviceAccount.name The name of the service account to use. If not set and create is true, a name is generated using the fullname template name: "" -podAnnotations: {} - +# ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod +# @param podSecurityContext.fsGroup Group ID for the container podSecurityContext: fsGroup: 1000 +# ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container +# @param securityContext.readOnlyRootFilesystem Mounts the container's root filesystem as read-only +# @param securityContext.runAsNonRoot Avoid running as root user +# @param securityContext.runAsUser User ID for the container securityContext: # capabilities: # drop: @@ -117,40 +167,42 @@ securityContext: runAsNonRoot: true runAsUser: 1000 +# @section Traffic exposure parameters + +# @param service.type Service type +# @param service.port Service port service: type: ClusterIP port: 80 +# ref: https://kubernetes.io/docs/user-guide/ingress/ ingress: + + # @param ingress.enabled Enable ingress controller resource enabled: false + + # @param ingress.className IngressClass that will be be used to implement the Ingress (Kubernetes 1.18+) + # This is supported in Kubernetes 1.18+ and required if you have more than one IngressClass marked as the default for your cluster. + # ref: https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/ className: "" + + # @param ingress.annotations Additional annotations for the Ingress resource. annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" + + # @param ingress.hosts[0].host Host for the ingress resource + # @param ingress.hosts[0].paths[0].path The path to the bot endpoint + # @param ingress.hosts[0].paths[0].pathType Ingress path type hosts: - host: sqbot.example.com paths: - path: / pathType: ImplementationSpecific + + # @param ingress.tls The tls configuration for additional hostnames to be covered with configured ingress. + # see: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls tls: [] - # - secretName: chart-example-tls - # hosts: + # - hosts: # - sqbot.example.com - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -nodeSelector: {} - -tolerations: [] - -affinity: {} + # secretName: chart-example-tls diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..06f1dff --- /dev/null +++ b/package-lock.json @@ -0,0 +1,307 @@ +{ + "name": "gitea-sonarqube-pr-bot", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "gitea-sonarqube-pr-bot", + "license": "MIT", + "dependencies": { + "readme-generator-for-helm": "^1.3.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/dot-object": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/dot-object/-/dot-object-2.1.4.tgz", + "integrity": "sha512-7FXnyyCLFawNYJ+NhkqyP9Wd2yzuo+7n9pGiYpkmXCTYa8Ci2U0eUNDVg5OuO5Pm6aFXI2SWN8/N/w7SJWu1WA==", + "dependencies": { + "commander": "^4.0.0", + "glob": "^7.1.5" + }, + "bin": { + "dot-object": "bin/dot-object" + } + }, + "node_modules/dot-object/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/glob": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.2.tgz", + "integrity": "sha512-NzDgHDiJwKYByLrL5lONmQFpK/2G78SMMfo+E9CuGlX4IkvfKDsiQSNPwAYxEy+e6p7ZQ3uslSLlwlJcqezBmQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "dependencies": { + "repeat-string": "^1.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readme-generator-for-helm": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/readme-generator-for-helm/-/readme-generator-for-helm-1.3.1.tgz", + "integrity": "sha512-3esincvKfR32K+xdxbYXcDG2bhiPSJdmie4dDFJxEu2Y93Dm8xPoDn7ieppyEpSiTBKiIYBBfE6BwHW2HE/j+A==", + "dependencies": { + "commander": "^7.1.0", + "dot-object": "^2.1.4", + "lodash": "^4.17.21", + "markdown-table": "^2.0.0", + "yaml": "^2.0.0-3" + }, + "bin": { + "readme-generator": "bin/index.js" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/yaml": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.1.tgz", + "integrity": "sha512-1NpAYQ3wjzIlMs0mgdBmYzLkFgWBIWrzYVDYfrixhoFNNgJ444/jT2kUT2sicRbJES3oQYRZugjB6Ro8SjKeFg==", + "engines": { + "node": ">= 14" + } + } + }, + "dependencies": { + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "dot-object": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/dot-object/-/dot-object-2.1.4.tgz", + "integrity": "sha512-7FXnyyCLFawNYJ+NhkqyP9Wd2yzuo+7n9pGiYpkmXCTYa8Ci2U0eUNDVg5OuO5Pm6aFXI2SWN8/N/w7SJWu1WA==", + "requires": { + "commander": "^4.0.0", + "glob": "^7.1.5" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + } + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.2.tgz", + "integrity": "sha512-NzDgHDiJwKYByLrL5lONmQFpK/2G78SMMfo+E9CuGlX4IkvfKDsiQSNPwAYxEy+e6p7ZQ3uslSLlwlJcqezBmQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "requires": { + "repeat-string": "^1.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "readme-generator-for-helm": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/readme-generator-for-helm/-/readme-generator-for-helm-1.3.1.tgz", + "integrity": "sha512-3esincvKfR32K+xdxbYXcDG2bhiPSJdmie4dDFJxEu2Y93Dm8xPoDn7ieppyEpSiTBKiIYBBfE6BwHW2HE/j+A==", + "requires": { + "commander": "^7.1.0", + "dot-object": "^2.1.4", + "lodash": "^4.17.21", + "markdown-table": "^2.0.0", + "yaml": "^2.0.0-3" + } + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "yaml": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.1.tgz", + "integrity": "sha512-1NpAYQ3wjzIlMs0mgdBmYzLkFgWBIWrzYVDYfrixhoFNNgJ444/jT2kUT2sicRbJES3oQYRZugjB6Ro8SjKeFg==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ec6ef0a --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "gitea-sonarqube-pr-bot", + "description": "Integrate SonarQube analysis into Gitea Pull Requests", + "author": "Steven Kriegler ", + "license": "MIT", + "private": true, + "repository": { + "type": "git", + "url": "https://codeberg.org/justusbunsi/gitea-sonarqube-bot.git" + }, + "keywords": [ + "gitea", + "sonarqube", + "pull request", + "bot" + ], + "scripts": { + "helm-params": "cd node_modules/readme-generator-for-helm && node bin/index.js --config ./../../helm/readme-generator-config.json --readme ./../../helm/README.md --values ./../../helm/values.yaml" + }, + "dependencies": { + "readme-generator-for-helm": "^1.3.1" + } +} From ffc2086b38feaec5b2f47b996f9909c0a21c82d1 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 14 May 2022 01:42:14 +0200 Subject: [PATCH 091/128] Add ArtifactHub badge Signed-off-by: Steven Kriegler --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4aa37ed..7507ed6 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=gitea-sonarqube-bot&metric=alert_status)](https://sonarcloud.io/dashboard?id=gitea-sonarqube-bot) [![Docker Image Version (latest semver)](https://img.shields.io/docker/v/justusbunsi/gitea-sonarqube-bot?logo=docker)](https://hub.docker.com/r/justusbunsi/gitea-sonarqube-bot) +[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/gitea-sonarqube-bot)](https://artifacthub.io/packages/helm/gitea-sonarqube-bot/gitea-sonarqube-bot) + _Gitea SonarQube Bot_ is a bot that receives messages from both SonarQube and Gitea to help developers being productive. The idea behind this project is the missing ALM integration of Gitea in SonarQube. Unfortunately, From 1f6fe0d4bc8f8af4ea68ed3d824959a9889eba32 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 14 May 2022 01:42:42 +0200 Subject: [PATCH 092/128] Bump golang to 1.18 Signed-off-by: Steven Kriegler --- Dockerfile | 4 ++-- contrib/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 162b0bd..c91dd76 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ################################### # Build stages ################################### -FROM golang:1.17-alpine3.14 AS build-go +FROM golang:1.18-alpine3.15 AS build-go ARG GOPROXY ENV GOPROXY ${GOPROXY:-direct} @@ -17,7 +17,7 @@ RUN go build ./cmd/gitea-sonarqube-bot ################################### # Production image ################################### -FROM alpine:3.14 +FROM alpine:3.15 LABEL maintainer="justusbunsi " RUN apk update \ diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 0894d1d..c51970b 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.17-alpine3.14 +FROM golang:1.18-alpine3.15 RUN apk --no-cache add build-base git bash curl openssl npm From d2353dede3ecf97634fd0cf389b6e6b07b6188db Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 14 May 2022 02:22:00 +0200 Subject: [PATCH 093/128] Bump dependencies Signed-off-by: Steven Kriegler --- Makefile | 6 ++ go.mod | 43 ++++----- go.sum | 270 +++++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 240 insertions(+), 79 deletions(-) diff --git a/Makefile b/Makefile index 9277ba6..59be2ff 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ help: @echo " - helm-params Auto-generates 'Parameters' section of 'helm/README.md' based on comments in values.yaml" @echo " - helm-pack Prepares Helm Chart release artifacts for pushing to 'charts' branch" @echo " - dep Dependency maintenance (tidy, vendor, verify)" + @echo " - dep-up Dependency upgrade (including auto-sync + auto-test)" @echo " - vet Examine Go source code and reports suspicious parts" @echo " - fmt Format the Go code" @echo " - help Print this help" @@ -61,6 +62,11 @@ dep: go mod vendor go mod verify +dep-bump: + go get -t -u ./... + +dep-up: dep-bump dep test + vet: go vet ./... diff --git a/go.mod b/go.mod index d909abc..c3a0701 100644 --- a/go.mod +++ b/go.mod @@ -3,47 +3,48 @@ module gitea-sonarqube-pr-bot go 1.17 require ( - code.gitea.io/sdk/gitea v0.15.0 + code.gitea.io/sdk/gitea v0.15.1 github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 - github.com/gin-gonic/gin v1.7.4 - github.com/spf13/viper v1.9.0 - github.com/stretchr/testify v1.7.0 - github.com/urfave/cli/v2 v2.3.0 + github.com/gin-gonic/gin v1.7.7 + github.com/spf13/viper v1.11.0 + github.com/stretchr/testify v1.7.1 + github.com/urfave/cli/v2 v2.6.0 ) require ( - github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect - github.com/go-playground/validator/v10 v10.9.0 // indirect + github.com/go-playground/validator/v10 v10.11.0 // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/hashicorp/go-version v1.3.0 // indirect + github.com/hashicorp/go-version v1.4.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect - github.com/magiconair/properties v1.8.5 // indirect + github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mitchellh/mapstructure v1.4.2 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml v1.9.4 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/spf13/afero v1.6.0 // indirect - github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.3.0 // indirect + github.com/stretchr/objx v0.4.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect - github.com/ugorji/go/codec v1.2.6 // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect + github.com/ugorji/go/codec v1.2.7 // indirect + golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 // indirect + golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect golang.org/x/text v0.3.7 // indirect - google.golang.org/protobuf v1.27.1 // indirect - gopkg.in/ini.v1 v1.63.2 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 // indirect ) diff --git a/go.sum b/go.sum index 517c37b..47defd0 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,7 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -15,6 +16,7 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -23,15 +25,22 @@ cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSU cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -41,34 +50,52 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= -code.gitea.io/sdk/gitea v0.15.0 h1:tsNhxDM/2N1Ohv1Xq5UWrht/esg0WmtRj4wsHVHriTg= -code.gitea.io/sdk/gitea v0.15.0/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA= +code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M= +code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -80,21 +107,30 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc= github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= -github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -104,14 +140,17 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A= -github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= +github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -153,8 +192,9 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -169,6 +209,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= @@ -179,43 +220,58 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= -github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4= +github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -228,11 +284,13 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= @@ -240,15 +298,17 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -256,68 +316,91 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= +github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk= -github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= +github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= +github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= -github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= -github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= -github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/urfave/cli/v2 v2.6.0 h1:yj2Drkflh8X/zUrkWlWlUjZYHyWN7WMmpVxyxXIUyv8= +github.com/urfave/cli/v2 v2.6.0/go.mod h1:oDzoM7pVwz6wHn5ogWgFUU1s4VJayeQS+aEZDqXIEJs= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.2/go.mod h1:2D7ZejHVMIfog1221iLSYlQRzrtECw3kz4I4VAQm3qI= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -329,18 +412,19 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c= +golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -378,7 +462,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -386,6 +470,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -408,11 +493,18 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -428,6 +520,11 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -441,11 +538,13 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -480,12 +579,15 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -494,9 +596,21 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY= +golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -559,6 +673,7 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -568,8 +683,9 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -596,7 +712,16 @@ google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtuk google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -639,7 +764,9 @@ google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -656,6 +783,24 @@ google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKr google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -681,6 +826,9 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -694,24 +842,30 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c= -gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= +gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 h1:dbuHpmKjkDzSOMKAWl10QNlgaZUd3V1q99xc81tt2Kc= +gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 7e32d5c5a06151d7b4ef0254fad8ccc3e22aa11d Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 14 May 2022 02:27:54 +0200 Subject: [PATCH 094/128] Bump chart versions Signed-off-by: Steven Kriegler --- helm/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 97d8d56..6f322cf 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: gitea-sonarqube-bot description: A Helm Chart for running a bot to communicate between both Gitea and SonarQube type: application -version: 0.1.0 -appVersion: "v0.1.0" +version: 0.1.1 +appVersion: "v0.1.1" keywords: - code review - git From 7374bc9daf700068ff29eba6b07c9cd69b7c5501 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 14 May 2022 02:31:00 +0200 Subject: [PATCH 095/128] Publish 0.1.1 Signed-off-by: Steven Kriegler --- gitea-sonarqube-bot-0.1.1.tgz | Bin 0 -> 6722 bytes index.yaml | 26 +++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 gitea-sonarqube-bot-0.1.1.tgz diff --git a/gitea-sonarqube-bot-0.1.1.tgz b/gitea-sonarqube-bot-0.1.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..3e8d326045c1352606813fb56e9e3683d5a69dc1 GIT binary patch literal 6722 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 Yf8AgA*O$BgHvj Date: Sun, 15 May 2022 15:31:05 +0200 Subject: [PATCH 096/128] Improve Chart definition Signed-off-by: Steven Kriegler --- helm/Chart.yaml | 29 ++++++++++++++++++----------- helm/LICENSE | 19 +++++++++++++++++++ helm/README.md | 3 +++ 3 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 helm/LICENSE diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 6f322cf..23adaee 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -2,18 +2,25 @@ apiVersion: v2 name: gitea-sonarqube-bot description: A Helm Chart for running a bot to communicate between both Gitea and SonarQube type: application -version: 0.1.1 +version: 0.1.2 appVersion: "v0.1.1" -keywords: - - code review - - git - - gitea - - pull request - - sonarqube - - sonarcloud -sources: - - https://codeberg.org/justusbunsi/gitea-sonarqube-bot/ - - https://hub.docker.com/r/justusbunsi/gitea-sonarqube-bot/ +home: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/ maintainers: - name: Steven Kriegler email: sk.bunsenbrenner@gmail.com +keywords: + - code-quality + - code-review + - git + - gitea + - pull-request + - sonarqube + - sonarcloud +sources: + - https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/helm +annotations: + artifacthub.io/links: | + - name: support + url: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/issues + - name: Container image + url: https://hub.docker.com/r/justusbunsi/gitea-sonarqube-bot/ diff --git a/helm/LICENSE b/helm/LICENSE new file mode 100644 index 0000000..da08240 --- /dev/null +++ b/helm/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 Steven Kriegler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/helm/README.md b/helm/README.md index ab8c709..4e929a1 100644 --- a/helm/README.md +++ b/helm/README.md @@ -77,3 +77,6 @@ for full configuration options. | `ingress.tls` | The tls configuration for additional hostnames to be covered with configured ingress. | `[]` | +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for the full license text. From 34e2783cb14e9b0db21729ad83fac2df6180f552 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 15 May 2022 16:03:19 +0200 Subject: [PATCH 097/128] Skip logging non-api routes Signed-off-by: Steven Kriegler --- internal/api/main.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/api/main.go b/internal/api/main.go index d6fb44c..aa92dfe 100644 --- a/internal/api/main.go +++ b/internal/api/main.go @@ -68,7 +68,12 @@ func addGiteaEndpoint(r *gin.Engine) { func Serve(c *cli.Context) error { fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") - r := gin.Default() + r := gin.New() + + r.Use(gin.Recovery()) + r.Use(gin.LoggerWithConfig(gin.LoggerConfig{ + SkipPaths: []string{"/ping", "/favicon.ico"}, + })) addPingApi(r) addSonarQubeEndpoint(r) From 4d28133b12586bdaf72ccf87da0fa4645f84ff57 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 21 May 2022 12:23:57 +0200 Subject: [PATCH 098/128] Extract test api test helpers Signed-off-by: Steven Kriegler --- internal/api/main_test.go | 56 ++++++++++++++++++++++++++++++++++ internal/api/sonarqube_test.go | 52 ------------------------------- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/internal/api/main_test.go b/internal/api/main_test.go index c950486..8d181ef 100644 --- a/internal/api/main_test.go +++ b/internal/api/main_test.go @@ -5,8 +5,64 @@ import ( "log" "os" "testing" + + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" + "gitea-sonarqube-pr-bot/internal/settings" + webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" + + "github.com/stretchr/testify/mock" ) +// Default SDK mocking +type HandlerPartialMock struct { + mock.Mock +} + +func (h *HandlerPartialMock) fetchDetails(w *webhook.Webhook) { + h.Called(w) +} + +type GiteaSdkMock struct { + mock.Mock +} + +func (h *GiteaSdkMock) PostComment(_ settings.GiteaRepository, _ int, _ string) error { + return nil +} + +func (h *GiteaSdkMock) DetermineHEAD(_ settings.GiteaRepository, _ int64) (string, error) { + return "", nil +} + +func (h *GiteaSdkMock) UpdateStatus(_ settings.GiteaRepository, _ string, _ giteaSdk.StatusDetails) error { + return nil +} + +type SQSdkMock struct { + mock.Mock +} + +func (h *SQSdkMock) GetMeasures(project string, branch string) (*sqSdk.MeasuresResponse, error) { + return &sqSdk.MeasuresResponse{}, nil +} + +func (h *SQSdkMock) GetPullRequestUrl(project string, index int64) string { + return "" +} + +func (h *SQSdkMock) GetPullRequest(project string, index int64) (*sqSdk.PullRequest, error) { + return nil, nil +} + +func (h *SQSdkMock) ComposeGiteaComment(data *sqSdk.CommentComposeData) (string, error) { + return "", nil +} + +func defaultMockPreparation(h *HandlerPartialMock) { + h.On("fetchDetails", mock.Anything).Return(nil) +} + // SETUP: mute logs func TestMain(m *testing.M) { log.SetOutput(ioutil.Discard) diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index cc6794b..a349731 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -6,63 +6,11 @@ import ( "net/http/httptest" "testing" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "gitea-sonarqube-pr-bot/internal/settings" - webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" ) -type HandlerPartialMock struct { - mock.Mock -} - -func (h *HandlerPartialMock) fetchDetails(w *webhook.Webhook) { - h.Called(w) -} - -type GiteaSdkMock struct { - mock.Mock -} - -func (h *GiteaSdkMock) PostComment(_ settings.GiteaRepository, _ int, _ string) error { - return nil -} - -func (h *GiteaSdkMock) DetermineHEAD(_ settings.GiteaRepository, _ int64) (string, error) { - return "", nil -} - -func (h *GiteaSdkMock) UpdateStatus(_ settings.GiteaRepository, _ string, _ giteaSdk.StatusDetails) error { - return nil -} - -type SQSdkMock struct { - mock.Mock -} - -func (h *SQSdkMock) GetMeasures(project string, branch string) (*sqSdk.MeasuresResponse, error) { - return &sqSdk.MeasuresResponse{}, nil -} - -func (h *SQSdkMock) GetPullRequestUrl(project string, index int64) string { - return "" -} - -func (h *SQSdkMock) GetPullRequest(project string, index int64) (*sqSdk.PullRequest, error) { - return nil, nil -} - -func (h *SQSdkMock) ComposeGiteaComment(data *sqSdk.CommentComposeData) (string, error) { - return "", nil -} - -func defaultMockPreparation(h *HandlerPartialMock) { - h.On("fetchDetails", mock.Anything).Return(nil) -} - func withValidRequestData(t *testing.T, mockPreparation func(*HandlerPartialMock), jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc, *HandlerPartialMock) { partialMock := new(HandlerPartialMock) mockPreparation(partialMock) From 7f5c3390c4bc6a31a784e7762c6adf8896cd013c Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 21 May 2022 13:50:58 +0200 Subject: [PATCH 099/128] Add tests for Gitea API Signed-off-by: Steven Kriegler --- internal/api/gitea_test.go | 140 +++++++++++++++++++++++++++++++++ internal/api/main_test.go | 8 +- internal/api/sonarqube_test.go | 12 +-- 3 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 internal/api/gitea_test.go diff --git a/internal/api/gitea_test.go b/internal/api/gitea_test.go new file mode 100644 index 0000000..23af493 --- /dev/null +++ b/internal/api/gitea_test.go @@ -0,0 +1,140 @@ +package api + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "gitea-sonarqube-pr-bot/internal/settings" + + "github.com/stretchr/testify/assert" +) + +func withValidGiteaCommentRequestData(t *testing.T, jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) { + webhookHandler := NewGiteaWebhookHandler(new(GiteaSdkMock), new(SQSdkMock)) + + req, err := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer(jsonBody)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(webhookHandler.HandleComment) + + return req, rr, handler +} + +func withValidGiteaSynchronizeRequestData(t *testing.T, jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) { + webhookHandler := NewGiteaWebhookHandler(new(GiteaSdkMock), new(SQSdkMock)) + + req, err := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer(jsonBody)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(webhookHandler.HandleSynchronize) + + return req, rr, handler +} + +func TestHandleGiteaCommentWebhookSuccess(t *testing.T) { + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + Gitea: settings.GiteaRepository{ + Owner: "test-user", + Name: "gitea-sonarqube-bot", + }, + }, + } + req, rr, handler := withValidGiteaCommentRequestData(t, []byte(`{"action":"created","issue":{"id":1,"url":"http://localhost:3000/api/v1/repos/test-user/gitea-sonarqube-bot/issues/1","html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"title":"„README.md“ ändern","body":"","ref":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:57:29Z","closed_at":null,"due_date":null,"pull_request":{"merged":false,"merged_at":null},"repository":{"id":1,"name":"gitea-sonarqube-bot","owner":"test-user","full_name":"test-user/gitea-sonarqube-bot"}},"comment":{"id":2,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1#issuecomment-2","pull_request_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","issue_url":"","user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"body":"/sq-bot review","created_at":"2022-05-15T18:57:29Z","updated_at":"2022-05-15T18:57:29Z"},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":1,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"is_pull":true}`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) +} + +func TestHandleGiteaCommentWebhookInvalidJSONBody(t *testing.T) { + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + }, + } + + req, rr, handler := withValidGiteaCommentRequestData(t, []byte(`{ "action": ["non-string-value-for-action"] }`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) + assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) +} + +func TestHandleGiteaCommentWebhookIgnoredProject(t *testing.T) { + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + }, + } + req, rr, handler := withValidGiteaCommentRequestData(t, []byte(`{"action":"created","issue":{"id":1,"url":"http://localhost:3000/api/v1/repos/test-user/gitea-sonarqube-bot/issues/1","html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"title":"„README.md“ ändern","body":"","ref":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:57:29Z","closed_at":null,"due_date":null,"pull_request":{"merged":false,"merged_at":null},"repository":{"id":1,"name":"gitea-sonarqube-bot","owner":"test-user","full_name":"test-user/gitea-sonarqube-bot"}},"comment":{"id":2,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1#issuecomment-2","pull_request_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","issue_url":"","user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"body":"/sq-bot review","created_at":"2022-05-15T18:57:29Z","updated_at":"2022-05-15T18:57:29Z"},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":1,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"is_pull":true}`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "ignore hook for non-configured project 'test-user/gitea-sonarqube-bot'"}`, rr.Body.String()) +} + +func TestHandleGiteaSynchronizeWebhookSuccess(t *testing.T) { + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + Gitea: settings.GiteaRepository{ + Owner: "test-user", + Name: "gitea-sonarqube-bot", + }, + }, + } + req, rr, handler := withValidGiteaSynchronizeRequestData(t, []byte(`{"action":"opened","number":1,"pull_request":{"id":1,"url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"title":"„README.md“ ändern","body":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","diff_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.diff","patch_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"base":{"label":"main","ref":"main","sha":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"head":{"label":"test-user-patch-1","ref":"test-user-patch-1","sha":"4d3f126f7f6b76c01187a06ec704a8a3055591de","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"merge_base":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","due_date":null,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:46:19Z","closed_at":null},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"review":null}`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) +} + +func TestHandleGiteaSynchronizeWebhookInvalidJSONBody(t *testing.T) { + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + }, + } + + req, rr, handler := withValidGiteaSynchronizeRequestData(t, []byte(`{ "action": ["non-string-value-for-action"] }`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) + assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) +} + +func TestHandleGiteaSynchronizeWebhookIgnoredProject(t *testing.T) { + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + }, + } + req, rr, handler := withValidGiteaSynchronizeRequestData(t, []byte(`{"action":"opened","number":1,"pull_request":{"id":1,"url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"title":"„README.md“ ändern","body":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","diff_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.diff","patch_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"base":{"label":"main","ref":"main","sha":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"head":{"label":"test-user-patch-1","ref":"test-user-patch-1","sha":"4d3f126f7f6b76c01187a06ec704a8a3055591de","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"merge_base":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","due_date":null,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:46:19Z","closed_at":null},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"review":null}`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "ignore hook for non-configured project 'test-user/gitea-sonarqube-bot'"}`, rr.Body.String()) +} diff --git a/internal/api/main_test.go b/internal/api/main_test.go index 8d181ef..8b27e47 100644 --- a/internal/api/main_test.go +++ b/internal/api/main_test.go @@ -52,7 +52,13 @@ func (h *SQSdkMock) GetPullRequestUrl(project string, index int64) string { } func (h *SQSdkMock) GetPullRequest(project string, index int64) (*sqSdk.PullRequest, error) { - return nil, nil + return &sqSdk.PullRequest{ + Status: struct { + QualityGateStatus string "json:\"qualityGateStatus\"" + }{ + QualityGateStatus: "OK", + }, + }, nil } func (h *SQSdkMock) ComposeGiteaComment(data *sqSdk.CommentComposeData) (string, error) { diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index a349731..ce5011e 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" ) -func withValidRequestData(t *testing.T, mockPreparation func(*HandlerPartialMock), jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc, *HandlerPartialMock) { +func withValidSonarQubeRequestData(t *testing.T, mockPreparation func(*HandlerPartialMock), jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc, *HandlerPartialMock) { partialMock := new(HandlerPartialMock) mockPreparation(partialMock) @@ -38,7 +38,7 @@ func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { }, }, } - req, rr, handler, _ := withValidRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + req, rr, handler, _ := withValidSonarQubeRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) @@ -53,7 +53,7 @@ func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { }, }, } - req, rr, handler, _ := withValidRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + req, rr, handler, _ := withValidSonarQubeRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) @@ -69,7 +69,7 @@ func TestHandleSonarQubeWebhookInvalidJSONBody(t *testing.T) { }, } - req, rr, handler, _ := withValidRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": ["invalid-server-url-content"] }`)) + req, rr, handler, _ := withValidSonarQubeRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": ["invalid-server-url-content"] }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) @@ -85,7 +85,7 @@ func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { }, } - req, rr, handler, partialMock := withValidRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + req, rr, handler, partialMock := withValidSonarQubeRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) @@ -102,7 +102,7 @@ func TestHandleSonarQubeWebhookForBranch(t *testing.T) { }, } - req, rr, handler, partialMock := withValidRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "BRANCH", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + req, rr, handler, partialMock := withValidSonarQubeRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "BRANCH", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) From e203034228ff0bd947cf0b8f029c091137556e59 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 21 May 2022 18:21:05 +0200 Subject: [PATCH 100/128] Rewrite API entrypoint to be testable (#22) The current code base regarding API entrypoint is not testable as it directly connects to Gitea when creating the API endpoints. This prevented my from writing tests in the past for that part. As the SonarQube quality gate broke due to changes in the API entrypoint logic, tests are now required to satisfy the quality gate. Therefore, the instantiation of the API handlers is now decoupled from building the bot API endpoints and follows the same interface wrapper strategy as used for the Gitea API client. This makes it testable. Now, tests are written for the most parts of the API entrypoint. I've also noticed that there was much overhead within the tests for a non-implemented function `fetchDetails`. So I dropped that function for now. Signed-off-by: Steven Kriegler Co-authored-by: justusbunsi Reviewed-on: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/pulls/22 --- cmd/gitea-sonarqube-bot/main.go | 16 +++- internal/api/gitea.go | 7 +- internal/api/main.go | 82 +++++++++----------- internal/api/main_test.go | 130 +++++++++++++++++++++++++++++--- internal/api/sonarqube.go | 34 ++++----- internal/api/sonarqube_test.go | 22 ++---- sonar-project.properties | 3 + 7 files changed, 204 insertions(+), 90 deletions(-) diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index 8c08d20..5c9eb4d 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -1,13 +1,17 @@ package main import ( + "fmt" "log" "os" "path" "gitea-sonarqube-pr-bot/internal/api" + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" + sonarQubeSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "gitea-sonarqube-pr-bot/internal/settings" + "github.com/fvbock/endless" "github.com/urfave/cli/v2" ) @@ -27,7 +31,7 @@ func main() { Name: "gitea-sonarqube-pr-bot", Usage: "Improve your experience with SonarQube and Gitea", Description: `By default, gitea-sonarqube-pr-bot will start running the webserver if no arguments are passed.`, - Action: api.Serve, + Action: serveApi, } err := app.Run(os.Args) @@ -35,3 +39,13 @@ func main() { log.Fatal(err) } } + +func serveApi(c *cli.Context) error { + fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") + + giteaHandler := api.NewGiteaWebhookHandler(giteaSdk.New(), sonarQubeSdk.New()) + sqHandler := api.NewSonarQubeWebhookHandler(giteaSdk.New(), sonarQubeSdk.New()) + server := api.New(giteaHandler, sqHandler) + + return endless.ListenAndServe(":3000", server.Engine) +} diff --git a/internal/api/gitea.go b/internal/api/gitea.go index c96ffbc..13660b1 100644 --- a/internal/api/gitea.go +++ b/internal/api/gitea.go @@ -12,6 +12,11 @@ import ( webhook "gitea-sonarqube-pr-bot/internal/webhooks/gitea" ) +type GiteaWebhookHandlerInferface interface { + HandleSynchronize(rw http.ResponseWriter, r *http.Request) + HandleComment(rw http.ResponseWriter, r *http.Request) +} + type GiteaWebhookHandler struct { giteaSdk giteaSdk.GiteaSdkInterface sqSdk sqSdk.SonarQubeSdkInterface @@ -88,7 +93,7 @@ func (h *GiteaWebhookHandler) HandleComment(rw http.ResponseWriter, r *http.Requ w.ProcessData(h.giteaSdk, h.sqSdk) } -func NewGiteaWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) *GiteaWebhookHandler { +func NewGiteaWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) GiteaWebhookHandlerInferface { return &GiteaWebhookHandler{ giteaSdk: g, sqSdk: sq, diff --git a/internal/api/main.go b/internal/api/main.go index aa92dfe..7ae869f 100644 --- a/internal/api/main.go +++ b/internal/api/main.go @@ -1,32 +1,38 @@ package api import ( - "fmt" "net/http" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" - - "github.com/fvbock/endless" "github.com/gin-gonic/gin" - "github.com/urfave/cli/v2" ) -func addPingApi(r *gin.Engine) { - r.GET("/ping", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "message": "pong", - }) - }) -} - type validSonarQubeEndpointHeader struct { SonarQubeProject string `header:"X-SonarQube-Project" binding:"required"` } -func addSonarQubeEndpoint(r *gin.Engine) { - webhookHandler := NewSonarQubeWebhookHandler(giteaSdk.New(), sqSdk.New()) - r.POST("/hooks/sonarqube", func(c *gin.Context) { +type validGiteaEndpointHeader struct { + GiteaEvent string `header:"X-Gitea-Event" binding:"required"` +} + +type ApiServer struct { + Engine *gin.Engine + sonarQubeWebhookHandler SonarQubeWebhookHandlerInferface + giteaWebhookHandler GiteaWebhookHandlerInferface +} + +func (s *ApiServer) setup() { + s.Engine.Use(gin.Recovery()) + s.Engine.Use(gin.LoggerWithConfig(gin.LoggerConfig{ + SkipPaths: []string{"/ping", "/favicon.ico"}, + })) + + s.Engine.GET("/favicon.ico", func(c *gin.Context) { + c.Status(http.StatusNoContent) + }).GET("/ping", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) + }).POST("/hooks/sonarqube", func(c *gin.Context) { h := validSonarQubeEndpointHeader{} if err := c.ShouldBindHeader(&h); err != nil { @@ -34,17 +40,8 @@ func addSonarQubeEndpoint(r *gin.Engine) { return } - webhookHandler.Handle(c.Writer, c.Request) - }) -} - -type validGiteaEndpointHeader struct { - GiteaEvent string `header:"X-Gitea-Event" binding:"required"` -} - -func addGiteaEndpoint(r *gin.Engine) { - webhookHandler := NewGiteaWebhookHandler(giteaSdk.New(), sqSdk.New()) - r.POST("/hooks/gitea", func(c *gin.Context) { + s.sonarQubeWebhookHandler.Handle(c.Writer, c.Request) + }).POST("/hooks/gitea", func(c *gin.Context) { h := validGiteaEndpointHeader{} if err := c.ShouldBindHeader(&h); err != nil { @@ -54,9 +51,9 @@ func addGiteaEndpoint(r *gin.Engine) { switch h.GiteaEvent { case "pull_request": - webhookHandler.HandleSynchronize(c.Writer, c.Request) + s.giteaWebhookHandler.HandleSynchronize(c.Writer, c.Request) case "issue_comment": - webhookHandler.HandleComment(c.Writer, c.Request) + s.giteaWebhookHandler.HandleComment(c.Writer, c.Request) default: c.JSON(http.StatusOK, gin.H{ "message": "ignore unknown event", @@ -65,23 +62,14 @@ func addGiteaEndpoint(r *gin.Engine) { }) } -func Serve(c *cli.Context) error { - fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") +func New(giteaHandler GiteaWebhookHandlerInferface, sonarQubeHandler SonarQubeWebhookHandlerInferface) *ApiServer { + s := &ApiServer{ + Engine: gin.New(), + giteaWebhookHandler: giteaHandler, + sonarQubeWebhookHandler: sonarQubeHandler, + } - r := gin.New() + s.setup() - r.Use(gin.Recovery()) - r.Use(gin.LoggerWithConfig(gin.LoggerConfig{ - SkipPaths: []string{"/ping", "/favicon.ico"}, - })) - - addPingApi(r) - addSonarQubeEndpoint(r) - addGiteaEndpoint(r) - - r.GET("/favicon.ico", func(c *gin.Context) { - c.Status(http.StatusNoContent) - }) - - return endless.ListenAndServe(":3000", r) + return s } diff --git a/internal/api/main_test.go b/internal/api/main_test.go index 8b27e47..046972a 100644 --- a/internal/api/main_test.go +++ b/internal/api/main_test.go @@ -1,26 +1,41 @@ package api import ( + "bytes" "io/ioutil" "log" + "net/http" + "net/http/httptest" "os" "testing" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "gitea-sonarqube-pr-bot/internal/settings" - webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) -// Default SDK mocking -type HandlerPartialMock struct { +type SonarQubeHandlerMock struct { mock.Mock } -func (h *HandlerPartialMock) fetchDetails(w *webhook.Webhook) { - h.Called(w) +func (h *SonarQubeHandlerMock) Handle(rw http.ResponseWriter, r *http.Request) { + h.Called(rw, r) +} + +type GiteaHandlerMock struct { + mock.Mock +} + +func (h *GiteaHandlerMock) HandleSynchronize(rw http.ResponseWriter, r *http.Request) { + h.Called(rw, r) +} + +func (h *GiteaHandlerMock) HandleComment(rw http.ResponseWriter, r *http.Request) { + h.Called(rw, r) } type GiteaSdkMock struct { @@ -65,12 +80,109 @@ func (h *SQSdkMock) ComposeGiteaComment(data *sqSdk.CommentComposeData) (string, return "", nil } -func defaultMockPreparation(h *HandlerPartialMock) { - h.On("fetchDetails", mock.Anything).Return(nil) -} - // SETUP: mute logs func TestMain(m *testing.M) { + gin.SetMode(gin.TestMode) log.SetOutput(ioutil.Discard) os.Exit(m.Run()) } + +func TestNonAPIRoutes(t *testing.T) { + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/favicon.ico", nil) + router.Engine.ServeHTTP(w, req) + assert.Equal(t, http.StatusNoContent, w.Code) + + w = httptest.NewRecorder() + req, _ = http.NewRequest("GET", "/ping", nil) + router.Engine.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestSonarQubeAPIRouteMissingProjectHeader(t *testing.T) { + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer([]byte(`{}`))) + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestSonarQubeAPIRouteProcessing(t *testing.T) { + sonarQubeHandlerMock := new(SonarQubeHandlerMock) + sonarQubeHandlerMock.On("Handle", mock.Anything, mock.Anything).Return(nil) + + router := New(new(GiteaHandlerMock), sonarQubeHandlerMock) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer([]byte(`{}`))) + req.Header.Add("X-SonarQube-Project", "gitea-sonarqube-bot") + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + sonarQubeHandlerMock.AssertNumberOfCalls(t, "Handle", 1) +} + +func TestGiteaAPIRouteMissingEventHeader(t *testing.T) { + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestGiteaAPIRouteSynchronizeProcessing(t *testing.T) { + giteaHandlerMock := new(GiteaHandlerMock) + giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) + giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) + + router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) + req.Header.Add("X-Gitea-Event", "pull_request") + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 1) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 0) +} + +func TestGiteaAPIRouteCommentProcessing(t *testing.T) { + giteaHandlerMock := new(GiteaHandlerMock) + giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) + giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) + + router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) + req.Header.Add("X-Gitea-Event", "issue_comment") + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 0) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 1) +} + +func TestGiteaAPIRouteUnknownEvent(t *testing.T) { + giteaHandlerMock := new(GiteaHandlerMock) + giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) + giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) + + router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) + req.Header.Add("X-Gitea-Event", "unknown") + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 0) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 0) +} diff --git a/internal/api/sonarqube.go b/internal/api/sonarqube.go index 77fbf30..6a73d49 100644 --- a/internal/api/sonarqube.go +++ b/internal/api/sonarqube.go @@ -14,10 +14,13 @@ import ( webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" ) +type SonarQubeWebhookHandlerInferface interface { + Handle(rw http.ResponseWriter, r *http.Request) +} + type SonarQubeWebhookHandler struct { - fetchDetails func(w *webhook.Webhook) - giteaSdk giteaSdk.GiteaSdkInterface - sqSdk sqSdk.SonarQubeSdkInterface + giteaSdk giteaSdk.GiteaSdkInterface + sqSdk sqSdk.SonarQubeSdkInterface } func (*SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) (bool, int) { @@ -31,13 +34,6 @@ func (*SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string } func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings.GiteaRepository) { - if strings.ToLower(w.Branch.Type) != "pull_request" { - log.Println("Ignore Hook for non-PR") - return - } - - h.fetchDetails(w) - status := giteaSdk.StatusOK if w.QualityGate.Status != "OK" { status = giteaSdk.StatusFailure @@ -96,19 +92,21 @@ func (h *SonarQubeWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request // Send response to SonarQube at this point to ensure being within 10 seconds limit of webhook response timeout rw.WriteHeader(http.StatusOK) + + if strings.ToLower(w.Branch.Type) != "pull_request" { + io.WriteString(rw, `{"message": "Ignore Hook for non-PR analysis."}`) + log.Println("Ignore Hook for non-PR analysis") + return + } + io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`) h.processData(w, settings.Projects[pIdx].Gitea) } -func fetchDetails(w *webhook.Webhook) { - log.Printf("This method will load additional data from SonarQube based on PR %d", w.PRIndex) -} - -func NewSonarQubeWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) *SonarQubeWebhookHandler { +func NewSonarQubeWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) SonarQubeWebhookHandlerInferface { return &SonarQubeWebhookHandler{ - fetchDetails: fetchDetails, - giteaSdk: g, - sqSdk: sq, + giteaSdk: g, + sqSdk: sq, } } diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index ce5011e..d0643ef 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -11,12 +11,8 @@ import ( "github.com/stretchr/testify/assert" ) -func withValidSonarQubeRequestData(t *testing.T, mockPreparation func(*HandlerPartialMock), jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc, *HandlerPartialMock) { - partialMock := new(HandlerPartialMock) - mockPreparation(partialMock) - +func withValidSonarQubeRequestData(t *testing.T, jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) { webhookHandler := NewSonarQubeWebhookHandler(new(GiteaSdkMock), new(SQSdkMock)) - webhookHandler.fetchDetails = partialMock.fetchDetails req, err := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer(jsonBody)) if err != nil { @@ -27,7 +23,7 @@ func withValidSonarQubeRequestData(t *testing.T, mockPreparation func(*HandlerPa rr := httptest.NewRecorder() handler := http.HandlerFunc(webhookHandler.Handle) - return req, rr, handler, partialMock + return req, rr, handler } func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { @@ -38,7 +34,7 @@ func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { }, }, } - req, rr, handler, _ := withValidSonarQubeRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) @@ -53,7 +49,7 @@ func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { }, }, } - req, rr, handler, _ := withValidSonarQubeRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) @@ -69,7 +65,7 @@ func TestHandleSonarQubeWebhookInvalidJSONBody(t *testing.T) { }, } - req, rr, handler, _ := withValidSonarQubeRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": ["invalid-server-url-content"] }`)) + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": ["invalid-server-url-content"] }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) @@ -85,12 +81,11 @@ func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { }, } - req, rr, handler, partialMock := withValidSonarQubeRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) - partialMock.AssertNumberOfCalls(t, "fetchDetails", 1) } func TestHandleSonarQubeWebhookForBranch(t *testing.T) { @@ -102,10 +97,9 @@ func TestHandleSonarQubeWebhookForBranch(t *testing.T) { }, } - req, rr, handler, partialMock := withValidSonarQubeRequestData(t, defaultMockPreparation, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "BRANCH", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "BRANCH", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) - partialMock.AssertNumberOfCalls(t, "fetchDetails", 0) + assert.Equal(t, `{"message": "Ignore Hook for non-PR analysis."}`, rr.Body.String()) } diff --git a/sonar-project.properties b/sonar-project.properties index d12c673..a1d7469 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -11,5 +11,8 @@ sonar.exclusions=**/*_test.go,contrib/**,docker/**,docs/**,helm/** sonar.tests=. sonar.test.inclusions=**/*_test.go +# Entrypoint of the application and not properly testable +sonar.coverage.exclusions=cmd/gitea-sonarqube-bot/main.go + sonar.go.tests.reportPaths=test-report.out sonar.go.coverage.reportPaths=cover.out From 5cb3daab6084fd6231ed275f90406b360ca3dadb Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 21 May 2022 19:03:56 +0200 Subject: [PATCH 101/128] Add webhook secret validation Resolves: #4 Signed-off-by: Steven Kriegler --- internal/api/gitea.go | 17 ++++++ internal/api/gitea_test.go | 72 +++++++++++++++++++++++++ internal/api/request_validation.go | 41 ++++++++++++++ internal/api/request_validation_test.go | 46 ++++++++++++++++ internal/api/sonarqube.go | 8 +++ internal/api/sonarqube_test.go | 36 +++++++++++++ internal/settings/gitea.go | 2 +- internal/settings/settings.go | 8 +-- internal/settings/settings_test.go | 14 ++--- internal/settings/sonarqube.go | 4 +- 10 files changed, 234 insertions(+), 14 deletions(-) create mode 100644 internal/api/request_validation.go create mode 100644 internal/api/request_validation_test.go diff --git a/internal/api/gitea.go b/internal/api/gitea.go index 13660b1..bbb65cd 100644 --- a/internal/api/gitea.go +++ b/internal/api/gitea.go @@ -9,6 +9,7 @@ import ( giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" + "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/gitea" ) @@ -47,6 +48,14 @@ func (h *GiteaWebhookHandler) HandleSynchronize(rw http.ResponseWriter, r *http. return } + ok, err := isValidWebhook(raw, settings.Gitea.Webhook.Secret, r.Header.Get("X-Gitea-Signature"), "Gitea") + if !ok { + log.Print(err.Error()) + rw.WriteHeader(http.StatusPreconditionFailed) + io.WriteString(rw, fmt.Sprint(`{"message": "Webhook validation failed. Request rejected."}`)) + return + } + w, ok := webhook.NewPullWebhook(raw) if !ok { rw.WriteHeader(http.StatusUnprocessableEntity) @@ -74,6 +83,14 @@ func (h *GiteaWebhookHandler) HandleComment(rw http.ResponseWriter, r *http.Requ return } + ok, err := isValidWebhook(raw, settings.Gitea.Webhook.Secret, r.Header.Get("X-Gitea-Signature"), "Gitea") + if !ok { + log.Print(err.Error()) + rw.WriteHeader(http.StatusPreconditionFailed) + io.WriteString(rw, `{"message": "Webhook validation failed. Request rejected."}`) + return + } + w, ok := webhook.NewCommentWebhook(raw) if !ok { rw.WriteHeader(http.StatusUnprocessableEntity) diff --git a/internal/api/gitea_test.go b/internal/api/gitea_test.go index 23af493..c0083f8 100644 --- a/internal/api/gitea_test.go +++ b/internal/api/gitea_test.go @@ -40,6 +40,11 @@ func withValidGiteaSynchronizeRequestData(t *testing.T, jsonBody []byte) (*http. } func TestHandleGiteaCommentWebhookSuccess(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } settings.Projects = []settings.Project{ { SonarQube: struct{ Key string }{ @@ -59,6 +64,11 @@ func TestHandleGiteaCommentWebhookSuccess(t *testing.T) { } func TestHandleGiteaCommentWebhookInvalidJSONBody(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } settings.Projects = []settings.Project{ { SonarQube: struct{ Key string }{ @@ -74,7 +84,33 @@ func TestHandleGiteaCommentWebhookInvalidJSONBody(t *testing.T) { assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) } +func TestHandleGiteaCommentInvalidWebhookSignature(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "gitea-comment-test-webhook", + }, + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "pr-bot", + }, + }, + } + req, rr, handler := withValidGiteaCommentRequestData(t, []byte(`{"action":"created","issue":{"id":1,"url":"http://localhost:3000/api/v1/repos/test-user/gitea-sonarqube-bot/issues/1","html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"title":"„README.md“ ändern","body":"","ref":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:57:29Z","closed_at":null,"due_date":null,"pull_request":{"merged":false,"merged_at":null},"repository":{"id":1,"name":"gitea-sonarqube-bot","owner":"test-user","full_name":"test-user/gitea-sonarqube-bot"}},"comment":{"id":2,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1#issuecomment-2","pull_request_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","issue_url":"","user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"body":"/sq-bot review","created_at":"2022-05-15T18:57:29Z","updated_at":"2022-05-15T18:57:29Z"},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":1,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"is_pull":true}`)) + req.Header.Set("X-Gitea-Signature", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467") + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusPreconditionFailed, rr.Code) + assert.Equal(t, `{"message": "Webhook validation failed. Request rejected."}`, rr.Body.String()) +} + func TestHandleGiteaCommentWebhookIgnoredProject(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } settings.Projects = []settings.Project{ { SonarQube: struct{ Key string }{ @@ -90,6 +126,11 @@ func TestHandleGiteaCommentWebhookIgnoredProject(t *testing.T) { } func TestHandleGiteaSynchronizeWebhookSuccess(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } settings.Projects = []settings.Project{ { SonarQube: struct{ Key string }{ @@ -109,6 +150,11 @@ func TestHandleGiteaSynchronizeWebhookSuccess(t *testing.T) { } func TestHandleGiteaSynchronizeWebhookInvalidJSONBody(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } settings.Projects = []settings.Project{ { SonarQube: struct{ Key string }{ @@ -124,7 +170,33 @@ func TestHandleGiteaSynchronizeWebhookInvalidJSONBody(t *testing.T) { assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) } +func TestHandleGiteaSynchronizeInvalidWebhookSignature(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "gitea-synchronize-test-webhook", + }, + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "pr-bot", + }, + }, + } + req, rr, handler := withValidGiteaSynchronizeRequestData(t, []byte(`{"action":"opened","number":1,"pull_request":{"id":1,"url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"title":"„README.md“ ändern","body":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","diff_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.diff","patch_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"base":{"label":"main","ref":"main","sha":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"head":{"label":"test-user-patch-1","ref":"test-user-patch-1","sha":"4d3f126f7f6b76c01187a06ec704a8a3055591de","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"merge_base":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","due_date":null,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:46:19Z","closed_at":null},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"review":null}`)) + req.Header.Set("X-Gitea-Signature", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467") + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusPreconditionFailed, rr.Code) + assert.Equal(t, `{"message": "Webhook validation failed. Request rejected."}`, rr.Body.String()) +} + func TestHandleGiteaSynchronizeWebhookIgnoredProject(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } settings.Projects = []settings.Project{ { SonarQube: struct{ Key string }{ diff --git a/internal/api/request_validation.go b/internal/api/request_validation.go new file mode 100644 index 0000000..86008da --- /dev/null +++ b/internal/api/request_validation.go @@ -0,0 +1,41 @@ +package api + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" + "log" +) + +func isValidWebhook(message []byte, key string, signature string, component string) (bool, error) { + log.Printf("'%s'", signature) + + if key == "" && signature == "" { + // No webhook token configured and no signature header received. Skipping request validation. + return true, nil + } + + if key == "" && signature != "" { + return false, fmt.Errorf("Signature header received but no %s webhook secret configured. Request rejected due to possible configuration mismatch.", component) + } + + if key != "" && signature == "" { + return false, fmt.Errorf("%s webhook secret configured but no signature header received. Request rejected due to possible configuration mismatch.", component) + } + + decodedSignature, err := hex.DecodeString(signature) + if err != nil { + return false, fmt.Errorf("Error decoding signature for %s webhook.", component) + } + + mac := hmac.New(sha256.New, []byte(key)) + mac.Write(message) + sum := mac.Sum(nil) + + if !hmac.Equal(decodedSignature, sum) { + return false, fmt.Errorf("Signature header does not match the received %s webhook content. Request rejected.", component) + } + + return true, nil +} diff --git a/internal/api/request_validation_test.go b/internal/api/request_validation_test.go new file mode 100644 index 0000000..c5e241d --- /dev/null +++ b/internal/api/request_validation_test.go @@ -0,0 +1,46 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func getRequestData() []byte { + return []byte(`{"serverUrl":"https://example.com","status":"SUCCESS","analysedAt":"2022-05-15T16:45:31+0000","revision":"378080777919s07657a07f7a3e2d05dc75f64edd","changedAt":"2022-05-15T16:41:39+0000","project":{"key":"gitea-sonarqube-bot","name":"Gitea SonarQube Bot","url":"https://example.com/dashboard?id=gitea-sonarqube-bot"},"branch":{"name":"PR-1822","type":"PULL_REQUEST","isMain":false,"url":"https://example.com/dashboard?id=gitea-sonarqube-bot&pullRequest=PR-1822"},"qualityGate":{"name":"GiteaSonarQubeBot","status":"OK","conditions":[{"metric":"new_reliability_rating","operator":"GREATER_THAN","value":"1","status":"OK","errorThreshold":"1"},{"metric":"new_security_rating","operator":"GREATER_THAN","value":"1","status":"OK","errorThreshold":"1"},{"metric":"new_maintainability_rating","operator":"GREATER_THAN","value":"1","status":"OK","errorThreshold":"1"},{"metric":"new_security_hotspots_reviewed","operator":"LESS_THAN","status":"OK","errorThreshold":"100"}]},"properties":{"sonar.analysis.sqbot":"378080777919s07657a07f7a3e2d05dc75f64edd"}}`) +} + +func TestIsValidWebhookSuccess(t *testing.T) { + actual, _ := isValidWebhook(getRequestData(), "sonarqube-test-webhook-secret", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467", "test-component") + assert.True(t, actual, "Expected successful webhook signature validation") +} + +func TestIsValidWebhookNothingConfiguredOrProvidedSuccess(t *testing.T) { + actual, _ := isValidWebhook(getRequestData(), "", "", "test-component") + assert.True(t, actual, "Webhook signature validation not skipped") +} + +func TestIsValidWebhookSignatureDecodingFailure(t *testing.T) { + actual, err := isValidWebhook(getRequestData(), "sonarqube-test-webhook-secret", "invalid-signature", "test-component") + assert.False(t, actual) + assert.EqualError(t, err, "Error decoding signature for test-component webhook.", "Undetected signature encoding error") +} + +func TestIsValidWebhookSignatureMismatchFailure(t *testing.T) { + actual, err := isValidWebhook(getRequestData(), "sonarqube-test-webhook-secret", "fde6a666b7a1a46c27efb1961c17b46b6cf7aa13db5560e5ac95e801a18a92f3", "test-component") + assert.False(t, actual) + assert.EqualError(t, err, "Signature header does not match the received test-component webhook content. Request rejected.", "Undetected signature mismatch") + // assert.EqualError(t, err, "Signature header received but no test-component webhook secret configured. Request rejected due to possible configuration mismatch.", "Undetected configuration mismatch (1)") +} + +func TestIsValidWebhookEmptySecretConfigurationFailure(t *testing.T) { + actual, err := isValidWebhook(getRequestData(), "", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467", "test-component") + assert.False(t, actual) + assert.EqualError(t, err, "Signature header received but no test-component webhook secret configured. Request rejected due to possible configuration mismatch.", "Undetected configuration mismatch (1)") +} + +func TestIsValidWebhookEmptySignatureConfigurationFailure(t *testing.T) { + actual, err := isValidWebhook(getRequestData(), "sonarqube-test-webhook-secret", "", "test-component") + assert.False(t, actual) + assert.EqualError(t, err, "test-component webhook secret configured but no signature header received. Request rejected due to possible configuration mismatch.", "Undetected configuration mismatch (2)") +} diff --git a/internal/api/sonarqube.go b/internal/api/sonarqube.go index 6a73d49..9ed8b92 100644 --- a/internal/api/sonarqube.go +++ b/internal/api/sonarqube.go @@ -83,6 +83,14 @@ func (h *SonarQubeWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request return } + ok, err := isValidWebhook(raw, settings.SonarQube.Webhook.Secret, r.Header.Get("X-Sonar-Webhook-HMAC-SHA256"), "SonarQube") + if !ok { + log.Print(err.Error()) + rw.WriteHeader(http.StatusPreconditionFailed) + io.WriteString(rw, `{"message": "Webhook validation failed. Request rejected."}`) + return + } + w, ok := webhook.New(raw) if !ok { rw.WriteHeader(http.StatusUnprocessableEntity) diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index d0643ef..10f6326 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -27,6 +27,11 @@ func withValidSonarQubeRequestData(t *testing.T, jsonBody []byte) (*http.Request } func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { + settings.SonarQube = settings.SonarQubeConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } settings.Projects = []settings.Project{ { SonarQube: struct{ Key string }{ @@ -72,7 +77,33 @@ func TestHandleSonarQubeWebhookInvalidJSONBody(t *testing.T) { assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) } +func TestHandleSonarQubeWebhookInvalidWebhookSignature(t *testing.T) { + settings.SonarQube = settings.SonarQubeConfig{ + Webhook: &settings.Webhook{ + Secret: "sonarqube-test-webhook-secret", + }, + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "pr-bot", + }, + }, + } + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + req.Header.Set("X-Sonar-Webhook-HMAC-SHA256", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467") + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusPreconditionFailed, rr.Code) + assert.Equal(t, `{"message": "Webhook validation failed. Request rejected."}`, rr.Body.String()) +} + func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { + settings.SonarQube = settings.SonarQubeConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } settings.Projects = []settings.Project{ { SonarQube: struct{ Key string }{ @@ -89,6 +120,11 @@ func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { } func TestHandleSonarQubeWebhookForBranch(t *testing.T) { + settings.SonarQube = settings.SonarQubeConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } settings.Projects = []settings.Project{ { SonarQube: struct{ Key string }{ diff --git a/internal/settings/gitea.go b/internal/settings/gitea.go index b095e42..3867d65 100644 --- a/internal/settings/gitea.go +++ b/internal/settings/gitea.go @@ -5,7 +5,7 @@ type GiteaRepository struct { Name string } -type giteaConfig struct { +type GiteaConfig struct { Url string Token *Token Webhook *Webhook diff --git a/internal/settings/settings.go b/internal/settings/settings.go index d0238a9..1e1a3d0 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -8,8 +8,8 @@ import ( ) var ( - Gitea giteaConfig - SonarQube sonarQubeConfig + Gitea GiteaConfig + SonarQube SonarQubeConfig Projects []Project ) @@ -62,12 +62,12 @@ func Load(configPath string) { errCallback := func(msg string) { panic(msg) } - Gitea = giteaConfig{ + Gitea = GiteaConfig{ Url: r.GetString("gitea.url"), Token: NewToken(r.GetString, "gitea", errCallback), Webhook: NewWebhook(r.GetString, "gitea", errCallback), } - SonarQube = sonarQubeConfig{ + SonarQube = SonarQubeConfig{ Url: r.GetString("sonarqube.url"), Token: NewToken(r.GetString, "sonarqube", errCallback), Webhook: NewWebhook(r.GetString, "sonarqube", errCallback), diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 3df25d6..91adf57 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -56,7 +56,7 @@ func TestLoadGiteaStructure(t *testing.T) { WriteConfigFile(t, defaultConfig) Load(os.TempDir()) - expected := giteaConfig{ + expected := GiteaConfig{ Url: "https://example.com/gitea", Token: &Token{ Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", @@ -75,7 +75,7 @@ func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { WriteConfigFile(t, defaultConfig) Load(os.TempDir()) - expected := giteaConfig{ + expected := GiteaConfig{ Url: "https://example.com/gitea", Token: &Token{ Value: "injected-token", @@ -97,7 +97,7 @@ func TestLoadSonarQubeStructure(t *testing.T) { WriteConfigFile(t, defaultConfig) Load(os.TempDir()) - expected := sonarQubeConfig{ + expected := SonarQubeConfig{ Url: "https://example.com/sonarqube", Token: &Token{ Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", @@ -131,7 +131,7 @@ projects: `)) Load(os.TempDir()) - expected := sonarQubeConfig{ + expected := SonarQubeConfig{ Url: "https://example.com/sonarqube", Token: &Token{ Value: "fake-sonarqube-token", @@ -155,7 +155,7 @@ func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { WriteConfigFile(t, defaultConfig) Load(os.TempDir()) - expected := sonarQubeConfig{ + expected := SonarQubeConfig{ Url: "https://example.com/sonarqube", Token: &Token{ Value: "injected-token", @@ -207,7 +207,7 @@ projects: os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRETFILE", sonarqubeWebhookSecretFile) os.Setenv("PRBOT_SONARQUBE_TOKEN_FILE", sonarqubeTokenFile) - expectedGitea := giteaConfig{ + expectedGitea := GiteaConfig{ Url: "https://example.com/gitea", Token: &Token{ Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", @@ -219,7 +219,7 @@ projects: }, } - expectedSonarQube := sonarQubeConfig{ + expectedSonarQube := SonarQubeConfig{ Url: "https://example.com/sonarqube", Token: &Token{ Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", diff --git a/internal/settings/sonarqube.go b/internal/settings/sonarqube.go index 645f9d2..3d2d289 100644 --- a/internal/settings/sonarqube.go +++ b/internal/settings/sonarqube.go @@ -2,14 +2,14 @@ package settings import "strings" -type sonarQubeConfig struct { +type SonarQubeConfig struct { Url string Token *Token Webhook *Webhook AdditionalMetrics []string } -func (c *sonarQubeConfig) GetMetricsList() string { +func (c *SonarQubeConfig) GetMetricsList() string { metrics := []string{ "bugs", "vulnerabilities", From dc3969cd05573e7eeab6178b8d66c894d81be5a7 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 22 May 2022 14:03:23 +0200 Subject: [PATCH 102/128] Improve configuration file flexibility Instead of re-inventing the wheel regarding configuration location handling and validation, this introduces a new command flag `--config` allowing for full flexibility of configuration filename and location. This flag can also be defined via environment variable which allows an easy way of starting the bot from command line, inside a Docker container or using the Helm Chart. It makes the custom environment lookup unnecessary and reduces some complexity during startup and for writing tests. Resolves: #10 Signed-off-by: Steven Kriegler --- .gitignore | 2 +- Dockerfile | 1 + README.md | 13 +++++++++ cmd/gitea-sonarqube-bot/main.go | 26 +++++++++-------- cmd/gitea-sonarqube-bot/main_test.go | 22 --------------- helm/Chart.yaml | 4 +-- helm/README.md | 41 ++++++++++++++++++--------- helm/templates/deployment.yaml | 14 ++++++++++ helm/values.yaml | 4 +++ internal/settings/settings.go | 10 +++---- internal/settings/settings_test.go | 42 +++++++++++++++------------- 11 files changed, 103 insertions(+), 76 deletions(-) delete mode 100644 cmd/gitea-sonarqube-bot/main_test.go diff --git a/.gitignore b/.gitignore index a3538d3..2a6750a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ /vendor/ /node_modules/ /helm-releases/ -/gitea-sonarqube-bot +/gitea-sonarqube-bot* /coverage.html /*.log /cover.out diff --git a/Dockerfile b/Dockerfile index c91dd76..93bf360 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,7 @@ ENV HOME=/home/bot EXPOSE 3000 ENV GIN_MODE "release" +ENV GITEA_SQ_BOT_CONFIG_PATH "/home/bot/config/config.yaml" VOLUME ["/home/bot/config/"] RUN ["chmod", "+x", "/usr/local/bin/docker-entrypoint.sh"] diff --git a/README.md b/README.md index 7507ed6..efb8c47 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,19 @@ and execute the following (replace `$TAG` first): docker run --rm -it -p 9000:3000 -v "$(pwd)/config/:/home/bot/config/" justusbunsi/gitea-sonarqube-bot:$TAG ``` +**Starting with v0.2.0** + +By default, the bot expects its configuration file under `./config/config.yaml` next to the bot executable. Inside the Docker image the +corresponding full path is `/home/bot/config/config.yaml`. If you prefer using a different location or even a different filename, you can +also define the environment variable `GITEA_SQ_BOT_CONFIG_PATH` that allows for changing that full path. + +Imagine having a `./config/sqbot.config.yml` on your host that you want to populate inside `/mnt/`, the correct command to run a Docker +container would be: + +```bash +docker run --rm -it -p 9000:3000 -e "GITEA_SQ_BOT_CONFIG_PATH=/mnt/sqbot.config.yml" -v "$(pwd)/config/:/mnt/" justusbunsi/gitea-sonarqube-bot:$TAG +``` + ### Helm Chart See [Helm Chart README](helm/README.md) for detailed instructions. diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index 5c9eb4d..42b5de3 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -4,7 +4,6 @@ import ( "fmt" "log" "os" - "path" "gitea-sonarqube-pr-bot/internal/api" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" @@ -15,23 +14,22 @@ import ( "github.com/urfave/cli/v2" ) -func getConfigLocation() string { - configPath := path.Join("config") - if customConfigPath, ok := os.LookupEnv("PRBOT_CONFIG_PATH"); ok { - configPath = customConfigPath - } - - return configPath -} - func main() { - settings.Load(getConfigLocation()) - app := &cli.App{ Name: "gitea-sonarqube-pr-bot", Usage: "Improve your experience with SonarQube and Gitea", Description: `By default, gitea-sonarqube-pr-bot will start running the webserver if no arguments are passed.`, Action: serveApi, + Flags: []cli.Flag{ + &cli.PathFlag{ + Name: "config", + Aliases: []string{"c"}, + Value: "./config/config.yaml", + Usage: "Full path to configuration file.", + EnvVars: []string{"GITEA_SQ_BOT_CONFIG_PATH"}, + TakesFile: true, + }, + }, } err := app.Run(os.Args) @@ -43,6 +41,10 @@ func main() { func serveApi(c *cli.Context) error { fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") + config := c.Path("config") + settings.Load(config) + fmt.Printf("Config file in use: %s\n", config) + giteaHandler := api.NewGiteaWebhookHandler(giteaSdk.New(), sonarQubeSdk.New()) sqHandler := api.NewSonarQubeWebhookHandler(giteaSdk.New(), sonarQubeSdk.New()) server := api.New(giteaHandler, sqHandler) diff --git a/cmd/gitea-sonarqube-bot/main_test.go b/cmd/gitea-sonarqube-bot/main_test.go deleted file mode 100644 index b48aff4..0000000 --- a/cmd/gitea-sonarqube-bot/main_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetConfigLocationWithDefault(t *testing.T) { - assert.Equal(t, "config", getConfigLocation()) -} - -func TestGetConfigLocationWithEnvironmentOverride(t *testing.T) { - os.Setenv("PRBOT_CONFIG_PATH", "/tmp/") - - assert.Equal(t, "/tmp/", getConfigLocation()) - - t.Cleanup(func() { - os.Unsetenv("PRBOT_CONFIG_PATH") - }) -} diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 23adaee..13162e1 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: gitea-sonarqube-bot description: A Helm Chart for running a bot to communicate between both Gitea and SonarQube type: application -version: 0.1.2 -appVersion: "v0.1.1" +version: 0.2.0 +appVersion: "v0.2.0" home: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/ maintainers: - name: Steven Kriegler diff --git a/helm/README.md b/helm/README.md index 4e929a1..3cd5ccc 100644 --- a/helm/README.md +++ b/helm/README.md @@ -1,5 +1,19 @@ # Gitea SonarQube Bot +_Gitea SonarQube Bot_ is a bot that receives messages from both SonarQube and Gitea to help developers +being productive. The idea behind this project is the missing ALM integration of Gitea in SonarQube. Unfortunately, +this [won't be added in near future](https://github.com/SonarSource/sonarqube/pull/3248#issuecomment-701334327). +_Gitea SonarQube Bot_ aims to fill the gap between working on pull requests and being notified on quality changes. + +- [Gitea SonarQube Bot](#gitea-sonarqube-bot) + - [Installation](#installation) + - [Parameters](#parameters) + - [Common parameters](#common-parameters) + - [App parameters](#app-parameters) + - [Security parameters](#security-parameters) + - [Traffic exposure parameters](#traffic-exposure-parameters) + - [License](#license) + ## Installation ```bash @@ -35,18 +49,19 @@ for full configuration options. ### App parameters -| Name | Description | Value | -| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` | -| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` | -| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` | -| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` | -| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` | -| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` | -| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` | -| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` | -| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | -| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | +| Name | Description | Value | +| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| `app.configLocationOverride` | Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) | `""` | +| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` | +| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` | +| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` | +| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` | +| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` | +| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` | +| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` | +| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` | +| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | +| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | ### Security parameters @@ -79,4 +94,4 @@ for full configuration options. ## License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for the full license text. +This project is licensed under the MIT License. See the [LICENSE](https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/helm/LICENSE) file for the full license text. diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 2a04847..481b9d3 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -31,6 +31,11 @@ spec: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.app.configLocationOverride }} + env: + - name: GITEA_SQ_BOT_CONFIG_PATH + value: "{{ .Values.app.configLocationOverride }}" + {{- end}} ports: - name: http containerPort: 3000 @@ -47,7 +52,11 @@ spec: {{- toYaml .Values.resources | nindent 12 }} volumeMounts: - name: sq-bot-config + {{- if .Values.app.configLocationOverride }} + mountPath: {{ dir .Values.app.configLocationOverride }} + {{- else }} mountPath: /home/bot/config + {{- end }} readOnly: true {{- if .Values.volumeMounts }} {{- toYaml .Values.volumeMounts | nindent 12 }} @@ -68,6 +77,11 @@ spec: - name: sq-bot-config secret: secretName: {{ include "helm.fullname" . }} + {{- if .Values.app.configLocationOverride }} + items: + - key: config.yaml + path: {{ base .Values.app.configLocationOverride }} + {{- end }} {{- if .Values.volumes }} {{- toYaml .Values.volumes | nindent 8 }} {{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index 6ac32a8..377758b 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -53,6 +53,10 @@ podAnnotations: {} # @section App parameters app: + # @param app.configLocationOverride Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) + # Setting this will also change the mount point for `.Values.app.configuration` to the directory part of the override value. + configLocationOverride: "" + # This object represents the [config.yaml](https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/config/config.example.yaml) provided to the application. configuration: # Gitea related configuration. Necessary for adding/updating comments on repository pull requests diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 1e1a3d0..373ccaf 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -13,10 +13,9 @@ var ( Projects []Project ) -func newConfigReader() *viper.Viper { +func newConfigReader(configFile string) *viper.Viper { v := viper.New() - v.SetConfigName("config.yaml") - v.SetConfigType("yaml") + v.SetConfigFile(configFile) v.SetEnvPrefix("prbot") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AllowEmptyEnv(true) @@ -38,9 +37,8 @@ func newConfigReader() *viper.Viper { return v } -func Load(configPath string) { - r := newConfigReader() - r.AddConfigPath(configPath) +func Load(configFile string) { + r := newConfigReader(configFile) err := r.ReadInConfig() if err != nil { diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 91adf57..168c04f 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -31,7 +31,7 @@ projects: name: pr-bot `) -func WriteConfigFile(t *testing.T, content []byte) { +func WriteConfigFile(t *testing.T, content []byte) string { dir := os.TempDir() config := path.Join(dir, "config.yaml") @@ -40,21 +40,23 @@ func WriteConfigFile(t *testing.T, content []byte) { }) _ = ioutil.WriteFile(config, content, 0444) + + return config } func TestLoadWithMissingFile(t *testing.T) { - assert.Panics(t, func() { Load(os.TempDir()) }, "No panic while reading missing file") + assert.Panics(t, func() { Load(path.Join(os.TempDir(), "config.yaml")) }, "No panic while reading missing file") } func TestLoadWithExistingFile(t *testing.T) { - WriteConfigFile(t, defaultConfig) + c := WriteConfigFile(t, defaultConfig) - assert.NotPanics(t, func() { Load(os.TempDir()) }, "Unexpected panic while reading existing file") + assert.NotPanics(t, func() { Load(c) }, "Unexpected panic while reading existing file") } func TestLoadGiteaStructure(t *testing.T) { - WriteConfigFile(t, defaultConfig) - Load(os.TempDir()) + c := WriteConfigFile(t, defaultConfig) + Load(c) expected := GiteaConfig{ Url: "https://example.com/gitea", @@ -72,8 +74,8 @@ func TestLoadGiteaStructure(t *testing.T) { func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { os.Setenv("PRBOT_GITEA_WEBHOOK_SECRET", "injected-webhook-secret") os.Setenv("PRBOT_GITEA_TOKEN_VALUE", "injected-token") - WriteConfigFile(t, defaultConfig) - Load(os.TempDir()) + c := WriteConfigFile(t, defaultConfig) + Load(c) expected := GiteaConfig{ Url: "https://example.com/gitea", @@ -94,8 +96,8 @@ func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { } func TestLoadSonarQubeStructure(t *testing.T) { - WriteConfigFile(t, defaultConfig) - Load(os.TempDir()) + c := WriteConfigFile(t, defaultConfig) + Load(c) expected := SonarQubeConfig{ Url: "https://example.com/sonarqube", @@ -112,7 +114,7 @@ func TestLoadSonarQubeStructure(t *testing.T) { } func TestLoadSonarQubeStructureWithAdditionalMetrics(t *testing.T) { - WriteConfigFile(t, []byte( + c := WriteConfigFile(t, []byte( `gitea: url: https://example.com/gitea token: @@ -129,7 +131,7 @@ projects: owner: example-organization name: pr-bot `)) - Load(os.TempDir()) + Load(c) expected := SonarQubeConfig{ Url: "https://example.com/sonarqube", @@ -152,8 +154,8 @@ projects: func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRET", "injected-webhook-secret") os.Setenv("PRBOT_SONARQUBE_TOKEN_VALUE", "injected-token") - WriteConfigFile(t, defaultConfig) - Load(os.TempDir()) + c := WriteConfigFile(t, defaultConfig) + Load(c) expected := SonarQubeConfig{ Url: "https://example.com/sonarqube", @@ -186,7 +188,7 @@ func TestLoadStructureWithFileReferenceResolving(t *testing.T) { sonarqubeTokenFile := path.Join(os.TempDir(), "token-secret-sonarqube") _ = ioutil.WriteFile(sonarqubeTokenFile, []byte(`a09eb5785b25bb2cbacf48808a677a0709f02d8e`), 0444) - WriteConfigFile(t, []byte( + c := WriteConfigFile(t, []byte( `gitea: url: https://example.com/gitea token: @@ -232,7 +234,7 @@ projects: AdditionalMetrics: []string{}, } - Load(os.TempDir()) + Load(c) assert.EqualValues(t, expectedGitea, Gitea) assert.EqualValues(t, expectedSonarQube, SonarQube) @@ -249,8 +251,8 @@ projects: } func TestLoadProjectsStructure(t *testing.T) { - WriteConfigFile(t, defaultConfig) - Load(os.TempDir()) + c := WriteConfigFile(t, defaultConfig) + Load(c) expectedProjects := []Project{ { @@ -283,7 +285,7 @@ sonarqube: secret: haxxor-sonarqube-secret projects: [] `) - WriteConfigFile(t, invalidConfig) + c := WriteConfigFile(t, invalidConfig) - assert.Panics(t, func() { Load(os.TempDir()) }, "No panic for empty project mapping that is required") + assert.Panics(t, func() { Load(c) }, "No panic for empty project mapping that is required") } From ce13a040b8a0421e60a1c8b76e409f19a93679d5 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 22 May 2022 14:25:03 +0200 Subject: [PATCH 103/128] Introduce changelog Signed-off-by: Steven Kriegler --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ README.md | 5 +++++ helm/README.md | 4 ++++ 3 files changed, 42 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ab9182b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +## v0.2.0 + +### 🤖 Application + +- Add webhook secret validation +- Improve configuration file flexibility +- Stop log output for `/ping` and `/favicon.ico` endpoints + +### 🐳 Docker image + +- Add `GITEA_SQ_BOT_CONFIG_PATH` environment variable + +### ☸️ Helm Chart + +- Add `.Values.app.configLocationOverride` parameter +- Bump default image tag to `v0.2.0` + +## v0.1.1 + +### ☸️ Helm Chart + +- Bump default image tag to `v0.1.1` + +### 👻 Maintenance + +- Bump Golang version to 1.18 +- Update dependencies to newest versions + +## v0.1.0 + +Initial release diff --git a/README.md b/README.md index efb8c47..be3fcb3 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Luckily, both endpoints have a proper REST API to communicate with each others. - [SonarQube](#sonarqube) - [Gitea](#gitea) - [CI system](#ci-system) + - [Changelog](#changelog) - [Contributing](#contributing) - [License](#license) - [Screenshots](#screenshots) @@ -102,6 +103,10 @@ To mitigate that situation, the bot will look inside the `properties` object for key can contain the actual commit hash to use for updating the status in Gitea. See [SonarQube docs](https://docs.sonarqube.org/latest/project-administration/webhooks) for details. +## Changelog + +See [CHANGELOG.md](./CHANGELOG.md) for a complete list of changes. + ## Contributing Expected workflow is: Fork -> Patch -> Push -> Pull Request diff --git a/helm/README.md b/helm/README.md index 3cd5ccc..20ba017 100644 --- a/helm/README.md +++ b/helm/README.md @@ -26,6 +26,10 @@ You have to modify the `app.configuration` values. Otherwise, the bot won't star to your Gitea instance. See [config.example.yaml](https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/config/config.example.yaml) for full configuration options. +## Changelog + +You can find a full changelog in the [main repository](https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/CHANGELOG.md) of this project. + ## Parameters ### Common parameters From 62c0155813ee9a851b0c5641198967c99b67d563 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 22 May 2022 14:39:22 +0200 Subject: [PATCH 104/128] Publish 0.2.0 Signed-off-by: Steven Kriegler --- gitea-sonarqube-bot-0.2.0.tgz | Bin 0 -> 8156 bytes index.yaml | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 gitea-sonarqube-bot-0.2.0.tgz diff --git a/gitea-sonarqube-bot-0.2.0.tgz b/gitea-sonarqube-bot-0.2.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..a65c534c54e414a8e671619763a8919d77447ed3 GIT binary patch literal 8156 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 C;t)Il literal 0 HcmV?d00001 diff --git a/index.yaml b/index.yaml index b42f153..33c2794 100644 --- a/index.yaml +++ b/index.yaml @@ -1,6 +1,37 @@ apiVersion: v1 entries: gitea-sonarqube-bot: + - annotations: + artifacthub.io/links: | + - name: support + url: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/issues + - name: Container image + url: https://hub.docker.com/r/justusbunsi/gitea-sonarqube-bot/ + apiVersion: v2 + appVersion: v0.2.0 + created: "2022-05-22T12:37:00.1709851Z" + description: A Helm Chart for running a bot to communicate between both Gitea + and SonarQube + digest: 5673f7805d02789efd56379c6b85793bfc44b700f6ece943214fc043176986be + home: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/ + keywords: + - code-quality + - code-review + - git + - gitea + - pull-request + - sonarqube + - sonarcloud + maintainers: + - email: sk.bunsenbrenner@gmail.com + name: Steven Kriegler + name: gitea-sonarqube-bot + sources: + - https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/helm + type: application + urls: + - https://codeberg.org/justusbunsi/gitea-sonarqube-bot/raw/branch/charts/gitea-sonarqube-bot-0.2.0.tgz + version: 0.2.0 - apiVersion: v2 appVersion: v0.1.1 created: "2022-05-14T00:29:10.323242Z" @@ -49,4 +80,4 @@ entries: urls: - https://codeberg.org/justusbunsi/gitea-sonarqube-bot/raw/branch/charts/gitea-sonarqube-bot-0.1.0.tgz version: 0.1.0 -generated: "2022-05-14T00:29:10.3176402Z" +generated: "2022-05-22T12:37:00.1653305Z" From acfa4fa203dd071bb7f0c326c78277983ef70400 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 22 May 2022 14:54:58 +0200 Subject: [PATCH 105/128] Add "Verified publisher" requirements Signed-off-by: Steven Kriegler --- artifacthub-repo.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 artifacthub-repo.yml diff --git a/artifacthub-repo.yml b/artifacthub-repo.yml new file mode 100644 index 0000000..0270b9a --- /dev/null +++ b/artifacthub-repo.yml @@ -0,0 +1 @@ +repositoryID: c473d8ab-1691-4eb5-a413-ef5d8b7bd786 From 0cc1cdc6c84990fd59a6bf8b9c60e52448efa281 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Fri, 27 May 2022 15:35:55 +0200 Subject: [PATCH 106/128] Add NodeJS and NPM requirements Signed-off-by: Steven Kriegler --- .npmrc | 2 ++ package-lock.json | 4 ++++ package.json | 5 +++++ 3 files changed, 11 insertions(+) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..f6ffbc1 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +# .npmrc +engine-strict=true diff --git a/package-lock.json b/package-lock.json index 06f1dff..decc27f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "license": "MIT", "dependencies": { "readme-generator-for-helm": "^1.3.1" + }, + "engines": { + "node": ">=12.21.0", + "npm": ">=8.0.0" } }, "node_modules/balanced-match": { diff --git a/package.json b/package.json index ec6ef0a..3652b97 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,11 @@ "pull request", "bot" ], + "engineStrict": true, + "engines": { + "node": ">=12.21.0", + "npm": ">=8.0.0" + }, "scripts": { "helm-params": "cd node_modules/readme-generator-for-helm && node bin/index.js --config ./../../helm/readme-generator-config.json --readme ./../../helm/README.md --values ./../../helm/values.yaml" }, From 471b25e6824e3688fe7d44a7630842480b7307bf Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sun, 12 Jun 2022 13:28:31 +0200 Subject: [PATCH 107/128] Remove debug logging Signed-off-by: Steven Kriegler --- internal/api/request_validation.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/api/request_validation.go b/internal/api/request_validation.go index 86008da..a1bbb39 100644 --- a/internal/api/request_validation.go +++ b/internal/api/request_validation.go @@ -5,12 +5,9 @@ import ( "crypto/sha256" "encoding/hex" "fmt" - "log" ) func isValidWebhook(message []byte, key string, signature string, component string) (bool, error) { - log.Printf("'%s'", signature) - if key == "" && signature == "" { // No webhook token configured and no signature header received. Skipping request validation. return true, nil From eb3cb301fc75827319ccb155c4e5002968430963 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Fri, 17 Jun 2022 13:34:06 +0200 Subject: [PATCH 108/128] Allow changing the port This introduces a new application option `--port`/`-p` to switch the listening port from 3000 (default) to another port. Docker image can be configured using the corresponding environment variable `GITEA_SQ_BOT_PORT`. Helm Chart allows setting `.Values.app.listeningPort` Resolves: #25 Signed-off-by: Steven Kriegler --- CHANGELOG.md | 14 ++++++++++++++ Dockerfile | 1 + README.md | 9 +++++++++ cmd/gitea-sonarqube-bot/main.go | 13 ++++++++++--- helm/Chart.yaml | 2 +- helm/README.md | 27 ++++++++++++++------------- helm/templates/deployment.yaml | 6 ++++-- helm/values.yaml | 3 +++ 8 files changed, 56 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9182b..d33dfda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## v0.2.1 + +### 🤖 Application + +- Allow configuring listening port + +### 🐳 Docker image + +- Add `GITEA_SQ_BOT_PORT` environment variable + +### ☸️ Helm Chart + +- Add `.Values.app.listeningPort` parameter + ## v0.2.0 ### 🤖 Application diff --git a/Dockerfile b/Dockerfile index 93bf360..c6f3eec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,6 +41,7 @@ ENV HOME=/home/bot EXPOSE 3000 ENV GIN_MODE "release" ENV GITEA_SQ_BOT_CONFIG_PATH "/home/bot/config/config.yaml" +ENV GITEA_SQ_BOT_PORT "3000" VOLUME ["/home/bot/config/"] RUN ["chmod", "+x", "/usr/local/bin/docker-entrypoint.sh"] diff --git a/README.md b/README.md index be3fcb3..d030330 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,15 @@ See [config.example.yaml](config/config.example.yaml) for a full configuration s ## Installation +Supported environment variables for application runtime configuration: + +| Environment Variable | Purpose | +|-----------------------------|---------------------------------| +| `GITEA_SQ_BOT_PORT` | Port the bot will listen on | +| `GITEA_SQ_BOT_CONFIG_PATH` | Full path to configuration file | + +For detailed information, use the `--help` flag. + ### Docker Create a directory `config` and place your [config.yaml](config/config.example.yaml) inside it. Open a terminal next to this directory diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index 42b5de3..4cf2821 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -16,9 +16,9 @@ import ( func main() { app := &cli.App{ - Name: "gitea-sonarqube-pr-bot", + Name: "gitea-sonarqube-bot", Usage: "Improve your experience with SonarQube and Gitea", - Description: `By default, gitea-sonarqube-pr-bot will start running the webserver if no arguments are passed.`, + Description: `Start an instance of gitea-sonarqube-bot to integrate SonarQube analysis into Gitea Pull Requests.`, Action: serveApi, Flags: []cli.Flag{ &cli.PathFlag{ @@ -29,6 +29,13 @@ func main() { EnvVars: []string{"GITEA_SQ_BOT_CONFIG_PATH"}, TakesFile: true, }, + &cli.IntFlag{ + Name: "port", + Aliases: []string{"p"}, + Value: 3000, + Usage: "Port the bot will listen on.", + EnvVars: []string{"GITEA_SQ_BOT_PORT"}, + }, }, } @@ -49,5 +56,5 @@ func serveApi(c *cli.Context) error { sqHandler := api.NewSonarQubeWebhookHandler(giteaSdk.New(), sonarQubeSdk.New()) server := api.New(giteaHandler, sqHandler) - return endless.ListenAndServe(":3000", server.Engine) + return endless.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), server.Engine) } diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 13162e1..6061391 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: gitea-sonarqube-bot description: A Helm Chart for running a bot to communicate between both Gitea and SonarQube type: application -version: 0.2.0 +version: 0.2.1 appVersion: "v0.2.0" home: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/ maintainers: diff --git a/helm/README.md b/helm/README.md index 20ba017..a7cb6cf 100644 --- a/helm/README.md +++ b/helm/README.md @@ -53,19 +53,20 @@ You can find a full changelog in the [main repository](https://codeberg.org/just ### App parameters -| Name | Description | Value | -| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| `app.configLocationOverride` | Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) | `""` | -| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` | -| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` | -| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` | -| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` | -| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` | -| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` | -| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` | -| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` | -| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | -| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | +| Name | Description | Value | +| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| `app.configLocationOverride` | Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) | `""` | +| `app.listeningPort` | Port the application will listening on inside the pod container. **Available since Chart version `0.2.1`. Requires at least image tag `v0.2.1`**. | `3000` | +| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` | +| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` | +| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` | +| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` | +| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` | +| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` | +| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` | +| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` | +| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | +| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | ### Security parameters diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 481b9d3..b5b84c4 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -31,14 +31,16 @@ spec: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} - {{- if .Values.app.configLocationOverride }} env: + {{- if .Values.app.configLocationOverride }} - name: GITEA_SQ_BOT_CONFIG_PATH value: "{{ .Values.app.configLocationOverride }}" {{- end}} + - name: GITEA_SQ_BOT_PORT + value: "{{ .Values.app.listeningPort }}" ports: - name: http - containerPort: 3000 + containerPort: {{ .Values.app.listeningPort }} protocol: TCP livenessProbe: httpGet: diff --git a/helm/values.yaml b/helm/values.yaml index 377758b..d42d99e 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -57,6 +57,9 @@ app: # Setting this will also change the mount point for `.Values.app.configuration` to the directory part of the override value. configLocationOverride: "" + # @param app.listeningPort Port the application will listening on inside the pod container. **Available since Chart version `0.2.1`. Requires at least image tag `v0.2.1`**. + listeningPort: 3000 + # This object represents the [config.yaml](https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/config/config.example.yaml) provided to the application. configuration: # Gitea related configuration. Necessary for adding/updating comments on repository pull requests From 02ad0c0bf0a656008fdec2fca97bfc4986256d26 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Fri, 17 Jun 2022 20:19:59 +0200 Subject: [PATCH 109/128] Improve error handling of SonarQube client Due to unhandled errors within the SonarQube client, users may be presented with Go panics or just don't know what the root cause of a non-working bot is. Now it is possible to identify network errors, authentication issues or an incorrect bot configuration regarding SonarQube. Fixes: #20 Signed-off-by: Steven Kriegler --- internal/clients/sonarqube/measures.go | 1 + internal/clients/sonarqube/pulls.go | 1 + internal/clients/sonarqube/sonarqube.go | 103 ++-- internal/clients/sonarqube/sonarqube_test.go | 489 +++++++++++++++++++ 4 files changed, 561 insertions(+), 33 deletions(-) create mode 100644 internal/clients/sonarqube/sonarqube_test.go diff --git a/internal/clients/sonarqube/measures.go b/internal/clients/sonarqube/measures.go index 08bb24c..ba74210 100644 --- a/internal/clients/sonarqube/measures.go +++ b/internal/clients/sonarqube/measures.go @@ -28,6 +28,7 @@ type MeasuresComponent struct { type MeasuresResponse struct { Component MeasuresComponent `json:"component"` Metrics []MeasuresComponentMetric `json:"metrics"` + Errors []Error `json:"errors"` } func (mr *MeasuresResponse) GetRenderedMarkdownTable() string { diff --git a/internal/clients/sonarqube/pulls.go b/internal/clients/sonarqube/pulls.go index 567ef8d..90fb0cc 100644 --- a/internal/clients/sonarqube/pulls.go +++ b/internal/clients/sonarqube/pulls.go @@ -9,6 +9,7 @@ type PullRequest struct { type PullsResponse struct { PullRequests []PullRequest `json:"pullRequests"` + Errors []Error `json:"errors"` } func (r *PullsResponse) GetPullRequest(name string) *PullRequest { diff --git a/internal/clients/sonarqube/sonarqube.go b/internal/clients/sonarqube/sonarqube.go index c834345..249349e 100644 --- a/internal/clients/sonarqube/sonarqube.go +++ b/internal/clients/sonarqube/sonarqube.go @@ -38,6 +38,38 @@ func GetRenderedQualityGate(qg string) string { return fmt.Sprintf("**Quality Gate**: %s", status) } +func retrieveDataFromApi(sdk *SonarQubeSdk, request *http.Request, wrapper interface{}) error { + request.Header.Add("Authorization", sdk.basicAuth()) + rawResponse, err := sdk.client.Do(request) + if err != nil { + return err + } + + if rawResponse.StatusCode == http.StatusUnauthorized { + return fmt.Errorf("missing or invalid API token") + } + + if rawResponse.Body != nil { + defer rawResponse.Body.Close() + } + + body, err := sdk.bodyReader(rawResponse.Body) + if err != nil { + return err + } + + err = json.Unmarshal(body, wrapper) + if err != nil { + return err + } + + return nil +} + +type Error struct { + Message string `json:"msg"` +} + type SonarQubeSdkInterface interface { GetMeasures(string, string) (*MeasuresResponse, error) GetPullRequestUrl(string, int64) string @@ -52,44 +84,49 @@ type CommentComposeData struct { QualityGate string } +type ClientInterface interface { + Do(req *http.Request) (*http.Response, error) +} + +type BodyReader func(io.Reader) ([]byte, error) +type HttpRequest func(method string, target string, body io.Reader) (*http.Request, error) + type SonarQubeSdk struct { - client *http.Client - baseUrl string - token string + client ClientInterface + bodyReader BodyReader + httpRequest HttpRequest + baseUrl string + token string } func (sdk *SonarQubeSdk) GetPullRequestUrl(project string, index int64) string { return fmt.Sprintf("%s/dashboard?id=%s&pullRequest=%s", sdk.baseUrl, project, PRNameFromIndex(index)) } -func (sdk *SonarQubeSdk) fetchPullRequests(project string) *PullsResponse { +func (sdk *SonarQubeSdk) fetchPullRequests(project string) (*PullsResponse, error) { url := fmt.Sprintf("%s/api/project_pull_requests/list?project=%s", sdk.baseUrl, project) - req, err := http.NewRequest(http.MethodGet, url, nil) + request, err := sdk.httpRequest(http.MethodGet, url, nil) if err != nil { - log.Printf("Cannot initialize Request: %s", err.Error()) - return nil - } - req.Header.Add("Authorization", sdk.basicAuth()) - rawResp, _ := sdk.client.Do(req) - if rawResp.Body != nil { - defer rawResp.Body.Close() + return nil, err } - body, _ := io.ReadAll(rawResp.Body) response := &PullsResponse{} - err = json.Unmarshal(body, &response) + err = retrieveDataFromApi(sdk, request, response) if err != nil { - log.Printf("cannot parse response from SonarQube: %s", err.Error()) - return nil + return nil, err } - return response + if len(response.Errors) != 0 { + return nil, fmt.Errorf("%s", response.Errors[0].Message) + } + + return response, nil } func (sdk *SonarQubeSdk) GetPullRequest(project string, index int64) (*PullRequest, error) { - response := sdk.fetchPullRequests(project) - if response == nil { - return nil, fmt.Errorf("unable to retrieve pull requests from SonarQube") + response, err := sdk.fetchPullRequests(project) + if err != nil { + return nil, fmt.Errorf("fetching pull requests failed: %w", err) } name := PRNameFromIndex(index) @@ -103,21 +140,19 @@ func (sdk *SonarQubeSdk) GetPullRequest(project string, index int64) (*PullReque func (sdk *SonarQubeSdk) GetMeasures(project string, branch string) (*MeasuresResponse, error) { url := fmt.Sprintf("%s/api/measures/component?additionalFields=metrics&metricKeys=%s&component=%s&pullRequest=%s", sdk.baseUrl, settings.SonarQube.GetMetricsList(), project, branch) - req, err := http.NewRequest(http.MethodGet, url, nil) + request, err := sdk.httpRequest(http.MethodGet, url, nil) if err != nil { - return nil, fmt.Errorf("cannot initialize Request: %w", err) - } - req.Header.Add("Authorization", sdk.basicAuth()) - rawResp, _ := sdk.client.Do(req) - if rawResp.Body != nil { - defer rawResp.Body.Close() + return nil, err } - body, _ := io.ReadAll(rawResp.Body) response := &MeasuresResponse{} - err = json.Unmarshal(body, &response) + err = retrieveDataFromApi(sdk, request, response) if err != nil { - return nil, fmt.Errorf("cannot parse response from SonarQube: %w", err) + return nil, err + } + + if len(response.Errors) != 0 { + return nil, fmt.Errorf("%s", response.Errors[0].Message) } return response, nil @@ -147,8 +182,10 @@ func (sdk *SonarQubeSdk) basicAuth() string { func New() *SonarQubeSdk { return &SonarQubeSdk{ - client: &http.Client{}, - baseUrl: settings.SonarQube.Url, - token: settings.SonarQube.Token.Value, + client: &http.Client{}, + bodyReader: io.ReadAll, + httpRequest: http.NewRequest, + baseUrl: settings.SonarQube.Url, + token: settings.SonarQube.Token.Value, } } diff --git a/internal/clients/sonarqube/sonarqube_test.go b/internal/clients/sonarqube/sonarqube_test.go new file mode 100644 index 0000000..bb9ef98 --- /dev/null +++ b/internal/clients/sonarqube/sonarqube_test.go @@ -0,0 +1,489 @@ +package sonarqube + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "gitea-sonarqube-pr-bot/internal/settings" + + "github.com/stretchr/testify/assert" +) + +type ClientMock struct { + responseError error + handler http.HandlerFunc + recoder *httptest.ResponseRecorder +} + +func (c *ClientMock) Do(req *http.Request) (*http.Response, error) { + c.handler.ServeHTTP(c.recoder, req) + + return &http.Response{ + StatusCode: c.recoder.Code, + Body: c.recoder.Result().Body, + }, c.responseError +} + +func TestParsePRIndexSuccess(t *testing.T) { + actual, _ := ParsePRIndex("PR-1337") + assert.Equal(t, 1337, actual, "PR index parsing is broken") +} + +func TestParsePRIndexNonIntegerFailure(t *testing.T) { + _, err := ParsePRIndex("PR-invalid") + assert.EqualErrorf(t, err, "branch name 'PR-invalid' does not match regex '^PR-(\\d+)$'", "Integer parsing succeeds unexpectedly") +} + +func TestPRNameFromIndex(t *testing.T) { + assert.Equal(t, "PR-1337", PRNameFromIndex(1337)) +} + +func TestGetRenderedQualityGateSuccess(t *testing.T) { + actual := GetRenderedQualityGate("OK") + + assert.Contains(t, actual, ":white_check_mark:", "Undetected successful quality gate during status rendering") +} + +func TestGetRenderedQualityGateFailure(t *testing.T) { + actual := GetRenderedQualityGate("ERROR") + + assert.Contains(t, actual, ":x:", "Undetected failed quality gate during status rendering") +} + +func TestGetPullRequestUrl(t *testing.T) { + sdk := &SonarQubeSdk{ + baseUrl: "https://sonarqube.example.com", + } + + actual := sdk.GetPullRequestUrl("test-project", 1337) + assert.Equal(t, "https://sonarqube.example.com/dashboard?id=test-project&pullRequest=PR-1337", actual, "PR Dashboard URL building broken") +} + +func TestRetrieveDataFromApiSuccess(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + wrapper := &PullsResponse{} + err := retrieveDataFromApi(sdk, request, wrapper) + + assert.Nil(t, err, "Successful data retrieval broken and throws error") + assert.Equal(t, "Basic dGVzdC10b2tlbjo=", request.Header.Get("Authorization"), "Authorization header not set") + assert.Equal(t, "PR-1", wrapper.PullRequests[0].Key, "Unmarshallowing into wrapper broken") +} + +func TestRetrieveDataFromApiRequestError(t *testing.T) { + expected := fmt.Errorf("This error indicates an error while performing the request") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}), + recoder: httptest.NewRecorder(), + responseError: expected, + }, + bodyReader: io.ReadAll, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + err := retrieveDataFromApi(sdk, request, &PullsResponse{}) + + assert.ErrorIs(t, err, expected, "Undetected request performing error") +} + +func TestRetrieveDataFromApiUnauthorized(t *testing.T) { + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + recorder.Code = http.StatusUnauthorized + }) + sdk := &SonarQubeSdk{ + token: "simulated-invalid-token", + client: &ClientMock{ + handler: handler, + recoder: recorder, + responseError: nil, + }, + bodyReader: io.ReadAll, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + err := retrieveDataFromApi(sdk, request, &PullsResponse{}) + + assert.Errorf(t, err, "missing or invalid API token", "Undetected unauthorized error") +} + +func TestRetrieveDataFromApiBodyReadError(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + expected := fmt.Errorf("Error reading body content") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: func(r io.Reader) ([]byte, error) { + return []byte(``), expected + }, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + err := retrieveDataFromApi(sdk, request, &PullsResponse{}) + + assert.ErrorIs(t, err, expected, "Undetected body processing error") +} + +func TestRetrieveDataFromApiBodyUnmarshalError(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullReq`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + err := retrieveDataFromApi(sdk, request, &PullsResponse{}) + + assert.Errorf(t, err, "unexpected end of JSON input", "Undetected body unmarshal error") +} + +func TestFetchPullRequestsSuccess(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + actual, err := sdk.fetchPullRequests("test-project") + + assert.Nil(t, err, "Successful data retrieval broken and throws error") + assert.IsType(t, &PullsResponse{}, actual, "Happy path broken") +} + +func TestFetchPullRequestsRequestBuildingFailure(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + expected := fmt.Errorf("Some simulated error") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return nil, expected + }, + } + + _, err := sdk.fetchPullRequests("test-project") + + assert.Equal(t, expected, err, "Unexpected error instance returned") +} + +func TestFetchPullRequestsRequestError(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + expected := fmt.Errorf("Some simulated error") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: expected, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + _, err := sdk.fetchPullRequests("test-project") + + assert.Equal(t, expected, err) +} + +func TestFetchPullRequestsErrorsInResponse(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"errors":[{"msg":"Project 'test-project' not found"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + _, err := sdk.fetchPullRequests("test-project") + + assert.Errorf(t, err, "Project 'test-project' not found", "Response error parsing broken") +} + +func TestGetPullRequestSuccess(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + actual, err := sdk.GetPullRequest("test-project", 1) + + assert.Nil(t, err, "Successful data retrieval broken and throws error") + assert.IsType(t, &PullRequest{}, actual, "Happy path broken") +} + +func TestGetPullRequestFetchError(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + expected := fmt.Errorf("Some simulated error") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: expected, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + _, err := sdk.GetPullRequest("test-project", 1) + + assert.Errorf(t, err, "fetching pull requests failed", "Incorrect edge case is throwing errors") + assert.Errorf(t, err, "Some simulated error", "Unexpected error cause") +} + +func TestGetPullRequestUnknownPR(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + _, err := sdk.GetPullRequest("test-project", 1337) + + assert.Errorf(t, err, "no pull request found with name 'PR-1337'") +} + +func TestGetMeasuresSuccess(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"0","bestValue":true}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + actual, err := sdk.GetMeasures("test-project", "PR-1") + + assert.Nil(t, err, "Successful data retrieval broken and throws error") + assert.IsType(t, &MeasuresResponse{}, actual, "Happy path broken") +} + +func TestGetMeasuresRequestBuildingFailure(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"0","bestValue":true}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) + }) + expected := fmt.Errorf("Some simulated error") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return nil, expected + }, + } + + _, err := sdk.GetMeasures("test-project", "PR-1") + + assert.Equal(t, expected, err, "Unexpected error instance returned") +} + +func TestGetMeasuresRequestError(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"0","bestValue":true}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) + }) + expected := fmt.Errorf("Some simulated error") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: expected, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + _, err := sdk.GetMeasures("test-project", "PR-1") + + assert.Equal(t, expected, err) +} + +func TestGetMeasuresErrorsInResponse(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"errors":[{"msg":"Component 'non-existing-project' of pull request 'PR-1' not found"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + _, err := sdk.GetMeasures("non-existing-project", "PR-1") + + assert.Errorf(t, err, "Component 'non-existing-project' of pull request 'PR-1' not found", "Response error parsing broken") +} + +func TestComposeGiteaCommentSuccess(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"10","bestValue":false}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + actual, err := sdk.ComposeGiteaComment(&CommentComposeData{ + Key: "test-project", + PRName: "PR-1", + Url: "https://sonarqube.example.com", + QualityGate: "OK", + }) + + assert.Nil(t, err, "Successful comment composing throwing errors") + assert.Contains(t, actual, ":white_check_mark:", "Happy path [Quality Gate] broken") + assert.Contains(t, actual, "| Metric | Current |", "Happy path [Metrics Header] broken") + assert.Contains(t, actual, "| Bugs | 10 |", "Happy path [Metrics Values] broken") + assert.Contains(t, actual, "https://sonarqube.example.com", "Happy path [Link] broken") + assert.Contains(t, actual, "/sq-bot review", "Happy path [Command] broken") +} + +func TestComposeGiteaCommentError(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"10","bestValue":false}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) + }) + expected := fmt.Errorf("Expected error from GetMeasures") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return nil, expected + }, + } + + _, err := sdk.ComposeGiteaComment(&CommentComposeData{ + Key: "test-project", + PRName: "PR-1", + Url: "https://sonarqube.example.com", + QualityGate: "OK", + }) + + assert.Errorf(t, err, expected.Error(), "Undetected error while composing comment") +} + +func TestNew(t *testing.T) { + settings.SonarQube = settings.SonarQubeConfig{ + Url: "http://example.com", + Token: &settings.Token{ + Value: "test-token", + }, + } + assert.IsType(t, &SonarQubeSdk{}, New(), "") +} From 4aad9c3e1783f035b2365e5ce959e7ae620cd4c6 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Fri, 17 Jun 2022 20:46:51 +0200 Subject: [PATCH 110/128] Improve docker setup instructions - Be clear about where to run the commands - Provide sample command for port change Signed-off-by: Steven Kriegler --- README.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d030330..68c1141 100644 --- a/README.md +++ b/README.md @@ -55,24 +55,21 @@ See [config.example.yaml](config/config.example.yaml) for a full configuration s Supported environment variables for application runtime configuration: -| Environment Variable | Purpose | -|-----------------------------|---------------------------------| -| `GITEA_SQ_BOT_PORT` | Port the bot will listen on | -| `GITEA_SQ_BOT_CONFIG_PATH` | Full path to configuration file | +| Environment Variable | Purpose | Since | +|-----------------------------|---------------------------------|--------| +| `GITEA_SQ_BOT_PORT` | Port the bot will listen on | v0.2.1 | +| `GITEA_SQ_BOT_CONFIG_PATH` | Full path to configuration file | v0.2.0 | For detailed information, use the `--help` flag. ### Docker -Create a directory `config` and place your [config.yaml](config/config.example.yaml) inside it. Open a terminal next to this directory -and execute the following (replace `$TAG` first): +Create a directory `config` and place your [config.yaml](config/config.example.yaml) inside it. Open a terminal inside the newly created directory and execute the following command (replace `$TAG` first): ```bash -docker run --rm -it -p 9000:3000 -v "$(pwd)/config/:/home/bot/config/" justusbunsi/gitea-sonarqube-bot:$TAG +docker run --rm -it -p 9000:3000 -v "$(pwd):/home/bot/config/" justusbunsi/gitea-sonarqube-bot:$TAG ``` -**Starting with v0.2.0** - By default, the bot expects its configuration file under `./config/config.yaml` next to the bot executable. Inside the Docker image the corresponding full path is `/home/bot/config/config.yaml`. If you prefer using a different location or even a different filename, you can also define the environment variable `GITEA_SQ_BOT_CONFIG_PATH` that allows for changing that full path. @@ -84,6 +81,15 @@ container would be: docker run --rm -it -p 9000:3000 -e "GITEA_SQ_BOT_CONFIG_PATH=/mnt/sqbot.config.yml" -v "$(pwd)/config/:/mnt/" justusbunsi/gitea-sonarqube-bot:$TAG ``` +If there are port mapping issues, you can use any other free port from your host. If you wish to use another port for the bot itself, you can override the default port `3000` by using the environment variable `GITEA_SQ_BOT_PORT`. Let's say you want to consistently use port _9001_ inside and outside the container, a correct command would be: + +```bash +# your terminals' pwd is the bot config directory +docker run --rm -it -p 9001:9001 -e "GITEA_SQ_BOT_PORT=9001" -v "$(pwd):/home/bot/config/" justusbunsi/gitea-sonarqube-bot:$TAG +``` + + + ### Helm Chart See [Helm Chart README](helm/README.md) for detailed instructions. From 685c834b611e8ff742f02e4ba1a0d8ed56300346 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 18 Jun 2022 14:03:56 +0200 Subject: [PATCH 111/128] Allow pull request naming pattern customization (#28) Fixes: #3 Signed-off-by: Steven Kriegler --- CHANGELOG.md | 2 + config/config.example.yaml | 14 ++++ helm/README.md | 30 ++++---- helm/values.yaml | 7 ++ internal/api/gitea_test.go | 14 ++++ internal/api/sonarqube_test.go | 22 ++++++ internal/clients/sonarqube/main_test.go | 14 ++++ internal/clients/sonarqube/sonarqube.go | 8 +- internal/clients/sonarqube/sonarqube_test.go | 47 ++++++++++++ internal/settings/pattern.go | 8 ++ internal/settings/settings.go | 8 ++ internal/settings/settings_test.go | 80 ++++++++++++++++++++ internal/webhooks/sonarqube/webhook_test.go | 19 +++++ 13 files changed, 254 insertions(+), 19 deletions(-) create mode 100644 internal/clients/sonarqube/main_test.go create mode 100644 internal/settings/pattern.go diff --git a/CHANGELOG.md b/CHANGELOG.md index d33dfda..d190088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### 🤖 Application - Allow configuring listening port +- Allow changing naming pattern for Pull Requests ### 🐳 Docker image @@ -13,6 +14,7 @@ ### ☸️ Helm Chart - Add `.Values.app.listeningPort` parameter +- Add `.Values.app.configuration.namingPattern` parameters ## v0.2.0 diff --git a/config/config.example.yaml b/config/config.example.yaml index cd48617..dba5457 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -57,3 +57,17 @@ projects: gitea: owner: justusbunsi name: example-repo + +# Define pull request names from SonarScanner analysis. Default pattern matches the Jenkins Gitea plugin schema. +namingPattern: + # Regular expression that MUST HAVE exactly ONE GROUP that matches the integer part of the PR. + # That integer part is identical to the pull request ID in Gitea. + regex: "^PR-(\\d+)$" + + # Valid Go format string. It MUST have one integer placeholder which will be replaced by the pull request ID. + # See: https://pkg.go.dev/fmt#hdr-Printing + template: "PR-%d" + + # Example for integer-only names + # # regex: "^(\\d+)$" + # # template: "%d" diff --git a/helm/README.md b/helm/README.md index a7cb6cf..8e86536 100644 --- a/helm/README.md +++ b/helm/README.md @@ -53,20 +53,22 @@ You can find a full changelog in the [main repository](https://codeberg.org/just ### App parameters -| Name | Description | Value | -| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| `app.configLocationOverride` | Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) | `""` | -| `app.listeningPort` | Port the application will listening on inside the pod container. **Available since Chart version `0.2.1`. Requires at least image tag `v0.2.1`**. | `3000` | -| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` | -| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` | -| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` | -| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` | -| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` | -| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` | -| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` | -| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` | -| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | -| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | +| Name | Description | Value | +| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| `app.configLocationOverride` | Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) | `""` | +| `app.listeningPort` | Port the application will listening on inside the pod container. **Available since Chart version `0.2.1`. Requires at least image tag `v0.2.1`**. | `3000` | +| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` | +| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` | +| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` | +| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` | +| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` | +| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` | +| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` | +| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` | +| `app.configuration.namingPattern.regex` | Regular expression that MUST HAVE exactly ONE GROUP that matches the integer part of the PR. That integer part is identical to the pull request ID in Gitea. | `^PR-(\d+)$` | +| `app.configuration.namingPattern.template` | Valid Go format string. It MUST have one integer placeholder which will be replaced by the pull request ID. See: https://pkg.go.dev/fmt#hdr-Printing | `PR-%d` | +| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | +| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | ### Security parameters diff --git a/helm/values.yaml b/helm/values.yaml index d42d99e..7da6a6f 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -129,6 +129,13 @@ app: owner: "" name: "" + # Define pull request names from SonarScanner analysis. Default pattern matches the Jenkins Gitea plugin schema. + # @param app.configuration.namingPattern.regex Regular expression that MUST HAVE exactly ONE GROUP that matches the integer part of the PR. That integer part is identical to the pull request ID in Gitea. + # @param app.configuration.namingPattern.template Valid Go format string. It MUST have one integer placeholder which will be replaced by the pull request ID. See: https://pkg.go.dev/fmt#hdr-Printing + namingPattern: + regex: "^PR-(\\d+)$" + template: "PR-%d" + # @param volumes If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly volumes: [] # - name: gitea-connection diff --git a/internal/api/gitea_test.go b/internal/api/gitea_test.go index c0083f8..74a407b 100644 --- a/internal/api/gitea_test.go +++ b/internal/api/gitea_test.go @@ -40,6 +40,9 @@ func withValidGiteaSynchronizeRequestData(t *testing.T, jsonBody []byte) (*http. } func TestHandleGiteaCommentWebhookSuccess(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } settings.Gitea = settings.GiteaConfig{ Webhook: &settings.Webhook{ Secret: "", @@ -61,9 +64,16 @@ func TestHandleGiteaCommentWebhookSuccess(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestHandleGiteaCommentWebhookInvalidJSONBody(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } settings.Gitea = settings.GiteaConfig{ Webhook: &settings.Webhook{ Secret: "", @@ -82,6 +92,10 @@ func TestHandleGiteaCommentWebhookInvalidJSONBody(t *testing.T) { assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestHandleGiteaCommentInvalidWebhookSignature(t *testing.T) { diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index 10f6326..d4efd3b 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -4,6 +4,7 @@ import ( "bytes" "net/http" "net/http/httptest" + "regexp" "testing" "gitea-sonarqube-pr-bot/internal/settings" @@ -27,6 +28,9 @@ func withValidSonarQubeRequestData(t *testing.T, jsonBody []byte) (*http.Request } func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } settings.SonarQube = settings.SonarQubeConfig{ Webhook: &settings.Webhook{ Secret: "", @@ -44,6 +48,10 @@ func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { @@ -99,6 +107,9 @@ func TestHandleSonarQubeWebhookInvalidWebhookSignature(t *testing.T) { } func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } settings.SonarQube = settings.SonarQubeConfig{ Webhook: &settings.Webhook{ Secret: "", @@ -117,9 +128,16 @@ func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestHandleSonarQubeWebhookForBranch(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } settings.SonarQube = settings.SonarQubeConfig{ Webhook: &settings.Webhook{ Secret: "", @@ -138,4 +156,8 @@ func TestHandleSonarQubeWebhookForBranch(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, `{"message": "Ignore Hook for non-PR analysis."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) } diff --git a/internal/clients/sonarqube/main_test.go b/internal/clients/sonarqube/main_test.go new file mode 100644 index 0000000..40fa850 --- /dev/null +++ b/internal/clients/sonarqube/main_test.go @@ -0,0 +1,14 @@ +package sonarqube + +import ( + "io/ioutil" + "log" + "os" + "testing" +) + +// SETUP: mute logs +func TestMain(m *testing.M) { + log.SetOutput(ioutil.Discard) + os.Exit(m.Run()) +} diff --git a/internal/clients/sonarqube/sonarqube.go b/internal/clients/sonarqube/sonarqube.go index 249349e..aecfb5f 100644 --- a/internal/clients/sonarqube/sonarqube.go +++ b/internal/clients/sonarqube/sonarqube.go @@ -7,7 +7,6 @@ import ( "io" "log" "net/http" - "regexp" "strconv" "strings" @@ -16,17 +15,16 @@ import ( ) func ParsePRIndex(name string) (int, error) { - re := regexp.MustCompile(`^PR-(\d+)$`) - res := re.FindSubmatch([]byte(name)) + res := settings.Pattern.RegExp.FindSubmatch([]byte(name)) if len(res) != 2 { - return 0, fmt.Errorf("branch name '%s' does not match regex '%s'", name, re.String()) + return 0, fmt.Errorf("branch name '%s' does not match regex '%s'", name, settings.Pattern.RegExp.String()) } return strconv.Atoi(string(res[1])) } func PRNameFromIndex(index int64) string { - return fmt.Sprintf("PR-%d", index) + return fmt.Sprintf(settings.Pattern.Template, index) } func GetRenderedQualityGate(qg string) string { diff --git a/internal/clients/sonarqube/sonarqube_test.go b/internal/clients/sonarqube/sonarqube_test.go index bb9ef98..d3ef651 100644 --- a/internal/clients/sonarqube/sonarqube_test.go +++ b/internal/clients/sonarqube/sonarqube_test.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "net/http/httptest" + "regexp" "testing" "gitea-sonarqube-pr-bot/internal/settings" @@ -28,17 +29,41 @@ func (c *ClientMock) Do(req *http.Request) (*http.Response, error) { } func TestParsePRIndexSuccess(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } + actual, _ := ParsePRIndex("PR-1337") assert.Equal(t, 1337, actual, "PR index parsing is broken") + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestParsePRIndexNonIntegerFailure(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } + _, err := ParsePRIndex("PR-invalid") assert.EqualErrorf(t, err, "branch name 'PR-invalid' does not match regex '^PR-(\\d+)$'", "Integer parsing succeeds unexpectedly") + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestPRNameFromIndex(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } + assert.Equal(t, "PR-1337", PRNameFromIndex(1337)) + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestGetRenderedQualityGateSuccess(t *testing.T) { @@ -57,9 +82,16 @@ func TestGetPullRequestUrl(t *testing.T) { sdk := &SonarQubeSdk{ baseUrl: "https://sonarqube.example.com", } + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } actual := sdk.GetPullRequestUrl("test-project", 1337) assert.Equal(t, "https://sonarqube.example.com/dashboard?id=test-project&pullRequest=PR-1337", actual, "PR Dashboard URL building broken") + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestRetrieveDataFromApiSuccess(t *testing.T) { @@ -259,6 +291,9 @@ func TestFetchPullRequestsErrorsInResponse(t *testing.T) { } func TestGetPullRequestSuccess(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) }) @@ -279,6 +314,10 @@ func TestGetPullRequestSuccess(t *testing.T) { assert.Nil(t, err, "Successful data retrieval broken and throws error") assert.IsType(t, &PullRequest{}, actual, "Happy path broken") + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestGetPullRequestFetchError(t *testing.T) { @@ -306,6 +345,10 @@ func TestGetPullRequestFetchError(t *testing.T) { } func TestGetPullRequestUnknownPR(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) }) @@ -325,6 +368,10 @@ func TestGetPullRequestUnknownPR(t *testing.T) { _, err := sdk.GetPullRequest("test-project", 1337) assert.Errorf(t, err, "no pull request found with name 'PR-1337'") + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestGetMeasuresSuccess(t *testing.T) { diff --git a/internal/settings/pattern.go b/internal/settings/pattern.go new file mode 100644 index 0000000..32f2e07 --- /dev/null +++ b/internal/settings/pattern.go @@ -0,0 +1,8 @@ +package settings + +import "regexp" + +type PatternConfig struct { + RegExp *regexp.Regexp + Template string +} diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 373ccaf..18a8349 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -2,6 +2,7 @@ package settings import ( "fmt" + "regexp" "strings" "github.com/spf13/viper" @@ -11,6 +12,7 @@ var ( Gitea GiteaConfig SonarQube SonarQubeConfig Projects []Project + Pattern *PatternConfig ) func newConfigReader(configFile string) *viper.Viper { @@ -33,6 +35,8 @@ func newConfigReader(configFile string) *viper.Viper { v.SetDefault("sonarqube.webhook.secretFile", "") v.SetDefault("sonarqube.additionalMetrics", []string{}) v.SetDefault("projects", []Project{}) + v.SetDefault("namingPattern.regex", `^PR-(\d+)$`) + v.SetDefault("namingPattern.template", "PR-%d") return v } @@ -71,4 +75,8 @@ func Load(configFile string) { Webhook: NewWebhook(r.GetString, "sonarqube", errCallback), AdditionalMetrics: r.GetStringSlice("sonarqube.additionalMetrics"), } + Pattern = &PatternConfig{ + RegExp: regexp.MustCompile(r.GetString("namingPattern.regex")), + Template: r.GetString("namingPattern.template"), + } } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 168c04f..295ed36 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "path" + "regexp" "testing" "github.com/stretchr/testify/assert" @@ -29,6 +30,9 @@ projects: gitea: owner: example-organization name: pr-bot +namingPattern: + regex: "^PR-(\\d+)$" + template: "PR-%d" `) func WriteConfigFile(t *testing.T, content []byte) string { @@ -289,3 +293,79 @@ projects: [] assert.Panics(t, func() { Load(c) }, "No panic for empty project mapping that is required") } + +func TestLoadNamingPatternStructure(t *testing.T) { + c := WriteConfigFile(t, defaultConfig) + Load(c) + + expected := &PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + Template: "PR-%d", + } + + assert.EqualValues(t, expected, Pattern) +} + +func TestLoadNamingPatternStructureWithInternalDefaults(t *testing.T) { + c := WriteConfigFile(t, []byte( + `gitea: + url: https://example.com/gitea + token: + value: fake-gitea-token +sonarqube: + url: https://example.com/sonarqube + token: + value: fake-sonarqube-token + additionalMetrics: "new_security_hotspots" +projects: + - sonarqube: + key: gitea-sonarqube-pr-bot + gitea: + owner: example-organization + name: pr-bot +`)) + Load(c) + + expected := &PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + Template: "PR-%d", + } + + assert.EqualValues(t, expected, Pattern) +} + +func TestLoadNamingPatternStructureInjectedEnvs(t *testing.T) { + os.Setenv("PRBOT_NAMINGPATTERN_REGEX", "test-(\\d+)-pullrequest") + os.Setenv("PRBOT_NAMINGPATTERN_TEMPLATE", "test-%d-pullrequest") + c := WriteConfigFile(t, defaultConfig) + Load(c) + + expected := &PatternConfig{ + RegExp: regexp.MustCompile(`test-(\d+)-pullrequest`), + Template: "test-%d-pullrequest", + } + + assert.EqualValues(t, expected, Pattern) + + t.Cleanup(func() { + os.Unsetenv("PRBOT_NAMINGPATTERN_REGEX") + os.Unsetenv("PRBOT_NAMINGPATTERN_TEMPLATE") + }) +} + +func TestLoadNamingPatternStructureMixedInput(t *testing.T) { + os.Setenv("PRBOT_NAMINGPATTERN_REGEX", "test-(\\d+)-pullrequest") + c := WriteConfigFile(t, defaultConfig) + Load(c) + + expected := &PatternConfig{ + RegExp: regexp.MustCompile(`test-(\d+)-pullrequest`), + Template: "PR-%d", + } + + assert.EqualValues(t, expected, Pattern) + + t.Cleanup(func() { + os.Unsetenv("PRBOT_NAMINGPATTERN_REGEX") + }) +} diff --git a/internal/webhooks/sonarqube/webhook_test.go b/internal/webhooks/sonarqube/webhook_test.go index 941e48f..4c8ac9d 100644 --- a/internal/webhooks/sonarqube/webhook_test.go +++ b/internal/webhooks/sonarqube/webhook_test.go @@ -1,18 +1,29 @@ package sonarqube import ( + "regexp" "testing" + "gitea-sonarqube-pr-bot/internal/settings" + "github.com/stretchr/testify/assert" ) func TestNewWebhook(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } + raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) response, ok := New(raw) assert.NotNil(t, response) assert.Equal(t, 1337, response.PRIndex) assert.True(t, ok) + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestNewWebhookInvalidJSON(t *testing.T) { @@ -23,8 +34,16 @@ func TestNewWebhookInvalidJSON(t *testing.T) { } func TestNewWebhookInvalidBranchName(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } + raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "invalid", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) _, ok := New(raw) assert.False(t, ok) + + t.Cleanup(func() { + settings.Pattern = nil + }) } From 385252cd724973ca6515ea7313abdb3872038640 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 18 Jun 2022 14:46:33 +0200 Subject: [PATCH 112/128] Prepare v0.2.1 release With PR #17 the Helm Chart parameters for webhook secrets were missing in the README parameters. This is now fixed. A checksum for bot configuration secret resource ensures replacement of the pod when there is a configuration change. Additional: - Bump Chart default image version - Add bug fix notes to changelog Signed-off-by: Steven Kriegler --- CHANGELOG.md | 1 + helm/Chart.yaml | 2 +- helm/README.md | 36 +++++++++++++++++++--------------- helm/templates/deployment.yaml | 3 ++- helm/values.yaml | 6 ++++-- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d190088..543652d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Allow configuring listening port - Allow changing naming pattern for Pull Requests +- Improve error handling for SonarQube communication ### 🐳 Docker image diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 6061391..6c020fb 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -3,7 +3,7 @@ name: gitea-sonarqube-bot description: A Helm Chart for running a bot to communicate between both Gitea and SonarQube type: application version: 0.2.1 -appVersion: "v0.2.0" +appVersion: "v0.2.1" home: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/ maintainers: - name: Steven Kriegler diff --git a/helm/README.md b/helm/README.md index 8e86536..a56efda 100644 --- a/helm/README.md +++ b/helm/README.md @@ -53,22 +53,26 @@ You can find a full changelog in the [main repository](https://codeberg.org/just ### App parameters -| Name | Description | Value | -| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -| `app.configLocationOverride` | Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) | `""` | -| `app.listeningPort` | Port the application will listening on inside the pod container. **Available since Chart version `0.2.1`. Requires at least image tag `v0.2.1`**. | `3000` | -| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` | -| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` | -| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` | -| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` | -| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` | -| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` | -| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` | -| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` | -| `app.configuration.namingPattern.regex` | Regular expression that MUST HAVE exactly ONE GROUP that matches the integer part of the PR. That integer part is identical to the pull request ID in Gitea. | `^PR-(\d+)$` | -| `app.configuration.namingPattern.template` | Valid Go format string. It MUST have one integer placeholder which will be replaced by the pull request ID. See: https://pkg.go.dev/fmt#hdr-Printing | `PR-%d` | -| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | -| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | +| Name | Description | Value | +| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| `app.configLocationOverride` | Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) | `""` | +| `app.listeningPort` | Port the application will listening on inside the pod container. **Available since Chart version `0.2.1`. Requires at least image tag `v0.2.1`**. | `3000` | +| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` | +| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` | +| `app.configuration.gitea.webhook.secret` | Secret for signature header (in plaintext) | `""` | +| `app.configuration.gitea.webhook.secretFile` | Path to file containing the plain text secret. Alternative to inline `app.configuration.gitea.webhook.secret` | | +| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` | +| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` | +| `app.configuration.sonarqube.webhook.secret` | Secret for signature header (in plaintext) | `""` | +| `app.configuration.sonarqube.webhook.secretFile` | Path to file containing the plain text secret. Alternative to inline `app.configuration.sonarqube.webhook.secret` | | +| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` | +| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` | +| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` | +| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` | +| `app.configuration.namingPattern.regex` | Regular expression that MUST HAVE exactly ONE GROUP that matches the integer part of the PR. That integer part is identical to the pull request ID in Gitea. | `^PR-(\d+)$` | +| `app.configuration.namingPattern.template` | Valid Go format string. It MUST have one integer placeholder which will be replaced by the pull request ID. See: https://pkg.go.dev/fmt#hdr-Printing | `PR-%d` | +| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | +| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | ### Security parameters diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index b5b84c4..bdb8a1a 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -11,8 +11,9 @@ spec: {{- include "helm.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + checksum/config: {{ include (print $.Template.BasePath "/config.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} labels: diff --git a/helm/values.yaml b/helm/values.yaml index 7da6a6f..d76e407 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -79,7 +79,8 @@ app: # request will be ignored. # The bot looks for `X-Gitea-Signature` header containing the sha256 hmac hash of the plain text secret. If the header # exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be validated. - # @skip app.configuration.gitea.webhook + # @param app.configuration.gitea.webhook.secret Secret for signature header (in plaintext) + # @extra app.configuration.gitea.webhook.secretFile Path to file containing the plain text secret. Alternative to inline `app.configuration.gitea.webhook.secret` webhook: secret: "" # # or path to file containing the plain text secret @@ -103,7 +104,8 @@ app: # The bot looks for `X-Sonar-Webhook-HMAC-SHA256` header containing the sha256 hmac hash of the plain text secret. # If the header exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be # validated. - # @skip app.configuration.sonarqube.webhook + # @param app.configuration.sonarqube.webhook.secret Secret for signature header (in plaintext) + # @extra app.configuration.sonarqube.webhook.secretFile Path to file containing the plain text secret. Alternative to inline `app.configuration.sonarqube.webhook.secret` webhook: secret: "" # # or path to file containing the plain text secret From 1a63c7674f031850af3cf9a61b5695e73e8959f0 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Sat, 18 Jun 2022 15:23:37 +0200 Subject: [PATCH 113/128] Publish 0.2.1 Signed-off-by: Steven Kriegler --- gitea-sonarqube-bot-0.2.1.tgz | Bin 0 -> 8894 bytes index.yaml | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 gitea-sonarqube-bot-0.2.1.tgz diff --git a/gitea-sonarqube-bot-0.2.1.tgz b/gitea-sonarqube-bot-0.2.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..4a4c778afe517f030d8478bd8de9997884bc28ff GIT binary patch literal 8894 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ô{i@yW7}%{lx#@BW-lrt2hTM3JQ~b6M3;_@9p7c zVr#r_+;c*PVnNkHc)sOB!VjBobG6s+vME1>PX+C(08g3;;BC4xfL-yA>XXeivP9k!f6;Rl&==+)iAyz{4g9~?IM^@n|H1D5Q~ba0 zk`8IKU}QQIu;p#TUSn@>DY~cAOB}fa;mWi2cmY#}LV^7qm@up^pm;Mm$KFvs?ZSoR zT~0%wW9#G@fk&f-qpqq*AwnnO0x^BN3UN+7q8!cn)Qb~mIhrY9OUy7-2Bt)1TLJMg z3LFze&VEv|gvKg9n_xtlAv;EeOC0QHb=6NpP<~w{0l1^D%j_sTcVJF^GLipd)vPEU z2ZYadz$dcmaV+o-aQV{1p)7?$zfKu&90ZO>BZ9fTmSeHovBBb$XMABuqy+E9%!C|B zT0*!piCIYaOkYX#DR8PFDsREY1yT~QO`1U3I}mR=&TxhSjp;d7ZPEQ3Qlah!sf(V3 zXWaAKPl*dkWAi9~TPV{UCc$%oLP1cVmR{7Hs?bc=afTn-FgSfT{H@h*LuUZJ{^_UA zQTqrgtpRifl^yu4GyHJ+aR|S)`u$dS_&c1wgI4!<__fnL+JW|e_WJF?08aZ(=cISs zX&>!Cr+aw(@u<^%4{tvXp?f-nO=2izjr!lL#ulP-P3NT`>x;VzHgtjyF(W`UFe=d z`%}9+gu#c_@v#Kuv_1|$oc1MtaCqALz2ABNVF({ikB{2@0laO)ai{h6xUE5Te}}{4 zR_9~~j#?+J_ia_`boim&cVsq=9sKs8Eni7^tuC|@|Y{ubfci3+o4tHR9+8-uG zf9nj|JJ9NP29hA}`lly5jwI*lJDH`^mF2o^U5g|&KhxS3uZWoz{lq}pi=ocW2`0^ zjQX)B=M36$yELa^jAtZKRmMfWiLlUR!vcqM3Xm%VpKStY|uGHO-(eZvy}q`8iEN5 zZ-O6k*Kum_xdiH$t!fFsZ#w`r_}mG(KtZ5L`DLq0GCxg@Ug8J!%T_gYin?ulsKYP$AYYOh1)E zE~~gK-t=fRa-83--&8@3?W>06azvPs3Cx)ZrICrWqxecKdjaA=1PDg7n@Y?DQZ}>> zk8B-*3tAosae&h>li6sHj~{N*nNX%(b>y$5>c}aQy{c6>RJ<0@sivHV!|$)KoYsSTRIBQHrh)~x%2>OvrS&&Sg{Bx@TX`+8e8gQr za&0ghn<6lU6W@&a=l~?Zur(^)%gs8M7;U@o;zi54KL$=h4}-eCpE$Q0HQYV7F>+N* zcEY5|V52FfHqPi==X&wNg)KQr)3ZU1gA^S;7Kj9VNjE3t$XzwRiSIxiY%lsJS7t#{ zj|#-7!x5?wY}8K`(1xwv-Kf>Os?~R9meGEr(HIrUQ5J7IWD=?g+RQS zIyR3s{9{_Vak@J>-l-1Ob$r*L_>W7D{LgZ7a^!#BM2@BC-XlFa9w>(RV>NTrRG1~N zAjish-6LJcx~SbhvURskn=#9ZK;t7@$J&77Kdy1NPj@9ph~}9k&vmiVe^}G*k~Skp zN!0M%%K2Y8g7|I8p@VSyKW{%9ZAK2eRF;m8zhd<9yN(VOb$pZ=9mXlq#@g)auM-yg zPBh4qV@b1QJ(F!hlYeD$=oQq`fZexPM*U&Sr^#aHZI(HI*z#rqSdYWfi=8C{^6GA( z&>w=jBf;eKtPEXL+!07iZ5&Sh{wNmE)q4hI=w^+9La84UqCM5)c8_I6nDiobS4T~M zSaL2?cULWy8tQHm_FT@{?hyI>fzZv3-PIA)3p;Kr!>K<6R`)^oBTtO`wA9M@?T?CW zb}(oSvNk~bhMMHDw8)w>O`EEX$5I(<>di?p-rKj|i{$}Za0ssL2 M|4+?-y8yTV0J-L94FCWD literal 0 HcmV?d00001 diff --git a/index.yaml b/index.yaml index 33c2794..ea56f2a 100644 --- a/index.yaml +++ b/index.yaml @@ -1,6 +1,37 @@ apiVersion: v1 entries: gitea-sonarqube-bot: + - annotations: + artifacthub.io/links: | + - name: support + url: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/issues + - name: Container image + url: https://hub.docker.com/r/justusbunsi/gitea-sonarqube-bot/ + apiVersion: v2 + appVersion: v0.2.1 + created: "2022-06-18T13:19:50.9149899Z" + description: A Helm Chart for running a bot to communicate between both Gitea + and SonarQube + digest: 2126ba10afac068d3a22efde89928cb2a45f71cd50ab07ff6fe69ab00f45fd9c + home: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/ + keywords: + - code-quality + - code-review + - git + - gitea + - pull-request + - sonarqube + - sonarcloud + maintainers: + - email: sk.bunsenbrenner@gmail.com + name: Steven Kriegler + name: gitea-sonarqube-bot + sources: + - https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/helm + type: application + urls: + - https://codeberg.org/justusbunsi/gitea-sonarqube-bot/raw/branch/charts/gitea-sonarqube-bot-0.2.1.tgz + version: 0.2.1 - annotations: artifacthub.io/links: | - name: support @@ -80,4 +111,4 @@ entries: urls: - https://codeberg.org/justusbunsi/gitea-sonarqube-bot/raw/branch/charts/gitea-sonarqube-bot-0.1.0.tgz version: 0.1.0 -generated: "2022-05-22T12:37:00.1653305Z" +generated: "2022-06-18T13:19:50.9095662Z" From c6d62861a65c3f4bc63ee954f1d429ed45ca12aa Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Thu, 7 Jul 2022 15:11:46 +0200 Subject: [PATCH 114/128] Switch to original readme-generator-for-helm The NPM package is a outdated fork from the Bitnami repository. Signed-off-by: Steven Kriegler --- helm/readme-generator-config.json | 19 ---- helm/values.yaml | 178 +++++++++++++++--------------- package-lock.json | 129 +++++++++++++++------- package.json | 6 +- 4 files changed, 179 insertions(+), 153 deletions(-) delete mode 100644 helm/readme-generator-config.json diff --git a/helm/readme-generator-config.json b/helm/readme-generator-config.json deleted file mode 100644 index a0ade9c..0000000 --- a/helm/readme-generator-config.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "comments": { - "format": "#" - }, - "tags": { - "param": "@param", - "section": "@section", - "skip": "@skip", - "extra": "@extra" - }, - "modifiers": { - "array": "array", - "object": "object", - "string": "string" - }, - "regexp": { - "paramsSectionTitle": "Parameters" - } -} diff --git a/helm/values.yaml b/helm/values.yaml index d76e407..4ecf6bb 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -1,32 +1,32 @@ -# @section Common parameters +## @section Common parameters -# @param replicaCount Number of replicas for the bot +## @param replicaCount Number of replicas for the bot replicaCount: 1 -# ref: https://hub.docker.com/r/justusbunsi/gitea-sonarqube-bot/tags/ -# @param image.repository Image repository -# @param image.pullPolicy Image pull policy -# @param image.tag Image tag (Overrides the image tag whose default is the chart `appVersion`) +## ref: https://hub.docker.com/r/justusbunsi/gitea-sonarqube-bot/tags/ +## @param image.repository Image repository +## @param image.pullPolicy Image pull policy +## @param image.tag Image tag (Overrides the image tag whose default is the chart `appVersion`) image: repository: justusbunsi/gitea-sonarqube-bot pullPolicy: IfNotPresent tag: "" -# @param imagePullSecrets Specify docker-registry secret names as an array +## @param imagePullSecrets Specify docker-registry secret names as an array imagePullSecrets: [] -# @param nameOverride String to partially override common.names.fullname template (will maintain the release name) +## @param nameOverride String to partially override common.names.fullname template (will maintain the release name) nameOverride: "" -# @param fullnameOverride String to fully override common.names.fullname template +## @param fullnameOverride String to fully override common.names.fullname template fullnameOverride: "" -# We usually recommend not to specify default resources and to leave this as a conscious -# choice for the user. This also increases chances charts run on environments with little -# resources, such as Minikube. If you do want to specify resources, uncomment the following -# lines, adjust them as necessary, and remove the curly braces after 'resources:'. -# @param resources.limits The resources limits for the container -# @param resources.requests The requested resources for the container +## We usually recommend not to specify default resources and to leave this as a conscious +## choice for the user. This also increases chances charts run on environments with little +## resources, such as Minikube. If you do want to specify resources, uncomment the following +## lines, adjust them as necessary, and remove the curly braces after 'resources:'. +## @param resources.limits The resources limits for the container +## @param resources.requests The requested resources for the container resources: limits: {} # cpu: 100m @@ -35,110 +35,110 @@ resources: # cpu: 100m # memory: 128Mi -# @param nodeSelector Node labels for pod assignment. Evaluated as a template. -# ref: https://kubernetes.io/docs/user-guide/node-selection/ +## @param nodeSelector Node labels for pod assignment. Evaluated as a template. +## ref: https://kubernetes.io/docs/user-guide/node-selection/ nodeSelector: {} -# @param tolerations Tolerations for pod assignment. Evaluated as a template. -# ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +## @param tolerations Tolerations for pod assignment. Evaluated as a template. +## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ tolerations: [] -# @param affinity Affinity for pod assignment. Evaluated as a template. -# ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity +## @param affinity Affinity for pod assignment. Evaluated as a template. +## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity affinity: {} -# @param podAnnotations Pod annotations. +## @param podAnnotations Pod annotations. podAnnotations: {} -# @section App parameters +## @section App parameters app: - # @param app.configLocationOverride Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) - # Setting this will also change the mount point for `.Values.app.configuration` to the directory part of the override value. + ## @param app.configLocationOverride Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) + ## Setting this will also change the mount point for `.Values.app.configuration` to the directory part of the override value. configLocationOverride: "" - # @param app.listeningPort Port the application will listening on inside the pod container. **Available since Chart version `0.2.1`. Requires at least image tag `v0.2.1`**. + ## @param app.listeningPort Port the application will listening on inside the pod container. **Available since Chart version `0.2.1`. Requires at least image tag `v0.2.1`**. listeningPort: 3000 - # This object represents the [config.yaml](https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/config/config.example.yaml) provided to the application. + ## This object represents the [config.yaml](https://codeberg.org/justusbunsi/gitea-sonarqube-bot/src/branch/main/config/config.example.yaml) provided to the application. configuration: - # Gitea related configuration. Necessary for adding/updating comments on repository pull requests + ## Gitea related configuration. Necessary for adding/updating comments on repository pull requests gitea: - # @param app.configuration.gitea.url Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. + ## @param app.configuration.gitea.url Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. url: "" - # Created access token for the user that shall be used as bot account. - # User needs "Read project" permissions with access to "Pull Requests" - # @param app.configuration.gitea.token.value Gitea token as plain text. Can be replaced with `file` key containing path to file. + ## Created access token for the user that shall be used as bot account. + ## User needs "Read project" permissions with access to "Pull Requests" + ## @param app.configuration.gitea.token.value Gitea token as plain text. Can be replaced with `file` key containing path to file. token: value: "" # # or path to file containing the plain text secret # file: /bot/secrets/gitea/user-token - # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the - # request will be ignored. - # The bot looks for `X-Gitea-Signature` header containing the sha256 hmac hash of the plain text secret. If the header - # exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be validated. - # @param app.configuration.gitea.webhook.secret Secret for signature header (in plaintext) - # @extra app.configuration.gitea.webhook.secretFile Path to file containing the plain text secret. Alternative to inline `app.configuration.gitea.webhook.secret` + ## If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the + ## request will be ignored. + ## The bot looks for `X-Gitea-Signature` header containing the sha256 hmac hash of the plain text secret. If the header + ## exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be validated. + ## @param app.configuration.gitea.webhook.secret Secret for signature header (in plaintext) + ## @extra app.configuration.gitea.webhook.secretFile Path to file containing the plain text secret. Alternative to inline `app.configuration.gitea.webhook.secret` webhook: secret: "" # # or path to file containing the plain text secret # secretFile: /bot/secrets/gitea/webhook-secret - # SonarQube related configuration. Necessary for requesting data from the API and processing the webhook. + ## SonarQube related configuration. Necessary for requesting data from the API and processing the webhook. sonarqube: - # @param app.configuration.sonarqube.url Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. + ## @param app.configuration.sonarqube.url Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. url: "" - # Created access token for the user that shall be used as bot account. - # User needs "Browse on project" permissions - # @param app.configuration.sonarqube.token.value SonarQube token as plain text. Can be replaced with `file` key containing path to file. + ## Created access token for the user that shall be used as bot account. + ## User needs "Browse on project" permissions + ## @param app.configuration.sonarqube.token.value SonarQube token as plain text. Can be replaced with `file` key containing path to file. token: value: "" # # or path to file containing the plain text secret # file: /bot/secrets/sonarqube/user-token - # If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the - # request will be ignored. - # The bot looks for `X-Sonar-Webhook-HMAC-SHA256` header containing the sha256 hmac hash of the plain text secret. - # If the header exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be - # validated. - # @param app.configuration.sonarqube.webhook.secret Secret for signature header (in plaintext) - # @extra app.configuration.sonarqube.webhook.secretFile Path to file containing the plain text secret. Alternative to inline `app.configuration.sonarqube.webhook.secret` + ## If the sent webhook has a signature header, the bot validates the request payload. If the value does not match, the + ## request will be ignored. + ## The bot looks for `X-Sonar-Webhook-HMAC-SHA256` header containing the sha256 hmac hash of the plain text secret. + ## If the header exists and no webhookSecret is defined here, the bot will ignore the request, because it cannot be + ## validated. + ## @param app.configuration.sonarqube.webhook.secret Secret for signature header (in plaintext) + ## @extra app.configuration.sonarqube.webhook.secretFile Path to file containing the plain text secret. Alternative to inline `app.configuration.sonarqube.webhook.secret` webhook: secret: "" # # or path to file containing the plain text secret # secretFile: /bot/secrets/sonarqube/webhook-secret - # Some useful metrics depend on the edition in use. There are various ones like code_smells, vulnerabilities, bugs, etc. - # By default the bot will extract "bugs,vulnerabilities,code_smells" - # @param app.configuration.sonarqube.additionalMetrics Setting this option you can extend that default list by your own metrics. + ## Some useful metrics depend on the edition in use. There are various ones like code_smells, vulnerabilities, bugs, etc. + ## By default the bot will extract "bugs,vulnerabilities,code_smells" + ## @param app.configuration.sonarqube.additionalMetrics Setting this option you can extend that default list by your own metrics. additionalMetrics: [] # - "new_security_hotspots" - # List of project mappings to take care of. Webhooks for other projects will be ignored. - # At least one must be configured. Otherwise all webhooks (no matter which source) because the bot cannot map on its own. - # @param app.configuration.projects[0].sonarqube.key Project key inside SonarQube - # @param app.configuration.projects[0].gitea.owner Repository owner inside Gitea - # @param app.configuration.projects[0].gitea.name Repository name inside Gitea + ## List of project mappings to take care of. Webhooks for other projects will be ignored. + ## At least one must be configured. Otherwise all webhooks (no matter which source) because the bot cannot map on its own. + ## @param app.configuration.projects[0].sonarqube.key Project key inside SonarQube + ## @param app.configuration.projects[0].gitea.owner Repository owner inside Gitea + ## @param app.configuration.projects[0].gitea.name Repository name inside Gitea projects: - sonarqube: key: "" - # A repository specification contains the owner name and the repository name itself. The owner can be the name of a - # real account or an organization in which the repository is located. + ## A repository specification contains the owner name and the repository name itself. The owner can be the name of a + ## real account or an organization in which the repository is located. gitea: owner: "" name: "" - # Define pull request names from SonarScanner analysis. Default pattern matches the Jenkins Gitea plugin schema. - # @param app.configuration.namingPattern.regex Regular expression that MUST HAVE exactly ONE GROUP that matches the integer part of the PR. That integer part is identical to the pull request ID in Gitea. - # @param app.configuration.namingPattern.template Valid Go format string. It MUST have one integer placeholder which will be replaced by the pull request ID. See: https://pkg.go.dev/fmt#hdr-Printing + ## Define pull request names from SonarScanner analysis. Default pattern matches the Jenkins Gitea plugin schema. + ## @param app.configuration.namingPattern.regex Regular expression that MUST HAVE exactly ONE GROUP that matches the integer part of the PR. That integer part is identical to the pull request ID in Gitea. + ## @param app.configuration.namingPattern.template Valid Go format string. It MUST have one integer placeholder which will be replaced by the pull request ID. See: https://pkg.go.dev/fmt#hdr-Printing namingPattern: regex: "^PR-(\\d+)$" template: "PR-%d" -# @param volumes If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly +## @param volumes If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly volumes: [] # - name: gitea-connection # secret: @@ -147,7 +147,7 @@ volumes: [] # secret: # secretName: sonarqube-secret-with-token-and-maybe-webhook-secret -# @param volumeMounts If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly +## @param volumeMounts If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly volumeMounts: [] # - name: gitea-connection # readOnly: true @@ -156,25 +156,25 @@ volumeMounts: [] # readOnly: true # mountPath: "/bot/secrets/sonarqube/" -# @section Security parameters +## @section Security parameters serviceAccount: - # @param serviceAccount.create Specifies whether a service account should be created + ## @param serviceAccount.create Specifies whether a service account should be created create: true - # @param serviceAccount.annotations Annotations to add to the service account + ## @param serviceAccount.annotations Annotations to add to the service account annotations: {} - # @param serviceAccount.name The name of the service account to use. If not set and create is true, a name is generated using the fullname template + ## @param serviceAccount.name The name of the service account to use. If not set and create is true, a name is generated using the fullname template name: "" -# ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod -# @param podSecurityContext.fsGroup Group ID for the container +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod +## @param podSecurityContext.fsGroup Group ID for the container podSecurityContext: fsGroup: 1000 -# ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container -# @param securityContext.readOnlyRootFilesystem Mounts the container's root filesystem as read-only -# @param securityContext.runAsNonRoot Avoid running as root user -# @param securityContext.runAsUser User ID for the container +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container +## @param securityContext.readOnlyRootFilesystem Mounts the container's root filesystem as read-only +## @param securityContext.runAsNonRoot Avoid running as root user +## @param securityContext.runAsUser User ID for the container securityContext: # capabilities: # drop: @@ -183,41 +183,41 @@ securityContext: runAsNonRoot: true runAsUser: 1000 -# @section Traffic exposure parameters +## @section Traffic exposure parameters -# @param service.type Service type -# @param service.port Service port +## @param service.type Service type +## @param service.port Service port service: type: ClusterIP port: 80 -# ref: https://kubernetes.io/docs/user-guide/ingress/ +## ref: https://kubernetes.io/docs/user-guide/ingress/ ingress: - # @param ingress.enabled Enable ingress controller resource + ## @param ingress.enabled Enable ingress controller resource enabled: false - # @param ingress.className IngressClass that will be be used to implement the Ingress (Kubernetes 1.18+) - # This is supported in Kubernetes 1.18+ and required if you have more than one IngressClass marked as the default for your cluster. - # ref: https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/ + ## @param ingress.className IngressClass that will be be used to implement the Ingress (Kubernetes 1.18+) + ## This is supported in Kubernetes 1.18+ and required if you have more than one IngressClass marked as the default for your cluster. + ## ref: https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/ className: "" - # @param ingress.annotations Additional annotations for the Ingress resource. + ## @param ingress.annotations Additional annotations for the Ingress resource. annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" - # @param ingress.hosts[0].host Host for the ingress resource - # @param ingress.hosts[0].paths[0].path The path to the bot endpoint - # @param ingress.hosts[0].paths[0].pathType Ingress path type + ## @param ingress.hosts[0].host Host for the ingress resource + ## @param ingress.hosts[0].paths[0].path The path to the bot endpoint + ## @param ingress.hosts[0].paths[0].pathType Ingress path type hosts: - host: sqbot.example.com paths: - path: / pathType: ImplementationSpecific - # @param ingress.tls The tls configuration for additional hostnames to be covered with configured ingress. - # see: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls + ## @param ingress.tls The tls configuration for additional hostnames to be covered with configured ingress. + ## see: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls tls: [] # - hosts: # - sqbot.example.com diff --git a/package-lock.json b/package-lock.json index decc27f..43e4f53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,8 +6,8 @@ "": { "name": "gitea-sonarqube-pr-bot", "license": "MIT", - "dependencies": { - "readme-generator-for-helm": "^1.3.1" + "devDependencies": { + "readme-generator-for-helm": "https://github.com/bitnami-labs/readme-generator-for-helm/tarball/main" }, "engines": { "node": ">=12.21.0", @@ -17,12 +17,14 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -32,6 +34,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, "engines": { "node": ">= 10" } @@ -39,12 +42,14 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/dot-object": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/dot-object/-/dot-object-2.1.4.tgz", "integrity": "sha512-7FXnyyCLFawNYJ+NhkqyP9Wd2yzuo+7n9pGiYpkmXCTYa8Ci2U0eUNDVg5OuO5Pm6aFXI2SWN8/N/w7SJWu1WA==", + "dev": true, "dependencies": { "commander": "^4.0.0", "glob": "^7.1.5" @@ -57,6 +62,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "engines": { "node": ">= 6" } @@ -64,12 +70,14 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/glob": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.2.tgz", - "integrity": "sha512-NzDgHDiJwKYByLrL5lONmQFpK/2G78SMMfo+E9CuGlX4IkvfKDsiQSNPwAYxEy+e6p7ZQ3uslSLlwlJcqezBmQ==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -80,12 +88,16 @@ }, "engines": { "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -94,25 +106,33 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/markdown-table": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "dev": true, "dependencies": { "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -123,7 +143,8 @@ "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "dependencies": { "wrappy": "1" } @@ -131,15 +152,18 @@ "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/readme-generator-for-helm": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/readme-generator-for-helm/-/readme-generator-for-helm-1.3.1.tgz", - "integrity": "sha512-3esincvKfR32K+xdxbYXcDG2bhiPSJdmie4dDFJxEu2Y93Dm8xPoDn7ieppyEpSiTBKiIYBBfE6BwHW2HE/j+A==", + "version": "2.4.0", + "resolved": "https://github.com/bitnami-labs/readme-generator-for-helm/tarball/main", + "integrity": "sha512-W5ziOuId0M00YQRDlA5le3oEguWe8hoINhivOAgEF+AZkk2bDoNxuFUaJIxqAUEvZRA8qlTfUlu+w90EOFbTLw==", + "dev": true, + "license": "ISC", "dependencies": { "commander": "^7.1.0", "dot-object": "^2.1.4", @@ -154,7 +178,8 @@ "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, "engines": { "node": ">=0.10" } @@ -162,12 +187,14 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "node_modules/yaml": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.1.tgz", - "integrity": "sha512-1NpAYQ3wjzIlMs0mgdBmYzLkFgWBIWrzYVDYfrixhoFNNgJ444/jT2kUT2sicRbJES3oQYRZugjB6Ro8SjKeFg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz", + "integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==", + "dev": true, "engines": { "node": ">= 14" } @@ -177,12 +204,14 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -191,17 +220,20 @@ "commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "dot-object": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/dot-object/-/dot-object-2.1.4.tgz", "integrity": "sha512-7FXnyyCLFawNYJ+NhkqyP9Wd2yzuo+7n9pGiYpkmXCTYa8Ci2U0eUNDVg5OuO5Pm6aFXI2SWN8/N/w7SJWu1WA==", + "dev": true, "requires": { "commander": "^4.0.0", "glob": "^7.1.5" @@ -210,19 +242,22 @@ "commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true } } }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "glob": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.2.tgz", - "integrity": "sha512-NzDgHDiJwKYByLrL5lONmQFpK/2G78SMMfo+E9CuGlX4IkvfKDsiQSNPwAYxEy+e6p7ZQ3uslSLlwlJcqezBmQ==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -235,7 +270,8 @@ "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -244,17 +280,20 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "markdown-table": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "dev": true, "requires": { "repeat-string": "^1.0.0" } @@ -263,6 +302,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -270,7 +310,8 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "requires": { "wrappy": "1" } @@ -278,12 +319,13 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true }, "readme-generator-for-helm": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/readme-generator-for-helm/-/readme-generator-for-helm-1.3.1.tgz", - "integrity": "sha512-3esincvKfR32K+xdxbYXcDG2bhiPSJdmie4dDFJxEu2Y93Dm8xPoDn7ieppyEpSiTBKiIYBBfE6BwHW2HE/j+A==", + "version": "https://github.com/bitnami-labs/readme-generator-for-helm/tarball/main", + "integrity": "sha512-W5ziOuId0M00YQRDlA5le3oEguWe8hoINhivOAgEF+AZkk2bDoNxuFUaJIxqAUEvZRA8qlTfUlu+w90EOFbTLw==", + "dev": true, "requires": { "commander": "^7.1.0", "dot-object": "^2.1.4", @@ -295,17 +337,20 @@ "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "yaml": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.1.tgz", - "integrity": "sha512-1NpAYQ3wjzIlMs0mgdBmYzLkFgWBIWrzYVDYfrixhoFNNgJ444/jT2kUT2sicRbJES3oQYRZugjB6Ro8SjKeFg==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz", + "integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==", + "dev": true } } } diff --git a/package.json b/package.json index 3652b97..98aa01f 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "npm": ">=8.0.0" }, "scripts": { - "helm-params": "cd node_modules/readme-generator-for-helm && node bin/index.js --config ./../../helm/readme-generator-config.json --readme ./../../helm/README.md --values ./../../helm/values.yaml" + "helm-params": "readme-generator --readme ./helm/README.md --values ./helm/values.yaml" }, - "dependencies": { - "readme-generator-for-helm": "^1.3.1" + "devDependencies": { + "readme-generator-for-helm": "https://github.com/bitnami-labs/readme-generator-for-helm/tarball/main" } } From 7e008773b0a34d2dfc9605bb60d5a360a8e61160 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Thu, 7 Jul 2022 19:56:26 +0200 Subject: [PATCH 115/128] Remove fvbock/endless as dependency Fixes: #31 Reviewed-on: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/pulls/32 Signed-off-by: Steven Kriegler --- cmd/gitea-sonarqube-bot/main.go | 44 +++++++++++++++++++++++++++++---- go.mod | 1 - go.sum | 2 -- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index 4cf2821..cf719be 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -1,19 +1,28 @@ package main import ( + "context" + "errors" "fmt" "log" + "net/http" "os" + "os/signal" + "syscall" + "time" "gitea-sonarqube-pr-bot/internal/api" giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" sonarQubeSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "gitea-sonarqube-pr-bot/internal/settings" - "github.com/fvbock/endless" "github.com/urfave/cli/v2" ) +var ( + HammerTime time.Duration = 15 * time.Second +) + func main() { app := &cli.App{ Name: "gitea-sonarqube-bot", @@ -46,15 +55,40 @@ func main() { } func serveApi(c *cli.Context) error { - fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") - config := c.Path("config") settings.Load(config) - fmt.Printf("Config file in use: %s\n", config) + + log.Println("Hi! I'm Gitea SonarQube Bot. At your service.") + log.Println("Config file in use:", config) giteaHandler := api.NewGiteaWebhookHandler(giteaSdk.New(), sonarQubeSdk.New()) sqHandler := api.NewSonarQubeWebhookHandler(giteaSdk.New(), sonarQubeSdk.New()) server := api.New(giteaHandler, sqHandler) - return endless.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), server.Engine) + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", c.Int("port")), + Handler: server.Engine, + } + + go func() { + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalln(err) + } + }() + + log.Println("Listen on", srv.Addr) + + quit := make(chan os.Signal) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), HammerTime) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Fatal("[STOP - Hammer Time] Forcefully shutting down\n", err) + } + + return nil } diff --git a/go.mod b/go.mod index c3a0701..08f4879 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.17 require ( code.gitea.io/sdk/gitea v0.15.1 - github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 github.com/gin-gonic/gin v1.7.7 github.com/spf13/viper v1.11.0 github.com/stretchr/testify v1.7.1 diff --git a/go.sum b/go.sum index 47defd0..b00e033 100644 --- a/go.sum +++ b/go.sum @@ -117,8 +117,6 @@ github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUork github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc= -github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= From 089523c56efa22c7b3e02771e855e23032a233da Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Thu, 7 Jul 2022 19:59:58 +0200 Subject: [PATCH 116/128] Update changelog with pending (unreleased) changes Signed-off-by: Steven Kriegler --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 543652d..70c5dbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Pending... + +### 👻 Maintenance + +- Remove `fvbock/endless` dependency + ## v0.2.1 ### 🤖 Application From 1b02303a4aa84572ceec063a1fe7f21c3d9163db Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Thu, 7 Jul 2022 20:11:55 +0200 Subject: [PATCH 117/128] Use buffered channel as per docs https://pkg.go.dev/os/signal#Notify Signed-off-by: Steven Kriegler --- cmd/gitea-sonarqube-bot/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index cf719be..ed949bc 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -78,7 +78,7 @@ func serveApi(c *cli.Context) error { log.Println("Listen on", srv.Addr) - quit := make(chan os.Signal) + quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down server...") From 576fc94517eef50c0e3c1389e5de212ef538e069 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Fri, 8 Jul 2022 13:38:53 +0200 Subject: [PATCH 118/128] Require go 1.18 for builds Signed-off-by: Steven Kriegler --- go.mod | 2 +- go.sum | 318 --------------------------------------------------------- 2 files changed, 1 insertion(+), 319 deletions(-) diff --git a/go.mod b/go.mod index 08f4879..8c09009 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module gitea-sonarqube-pr-bot -go 1.17 +go 1.18 require ( code.gitea.io/sdk/gitea v0.15.1 diff --git a/go.sum b/go.sum index b00e033..30b0ede 100644 --- a/go.sum +++ b/go.sum @@ -17,30 +17,14 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -56,44 +40,15 @@ code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M= code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -105,19 +60,10 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= @@ -125,10 +71,6 @@ github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -140,15 +82,10 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -156,8 +93,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -173,10 +108,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -187,17 +120,13 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -208,70 +137,28 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4= github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= -github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= -github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -284,82 +171,35 @@ github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= @@ -369,7 +209,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -382,9 +221,7 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= @@ -395,32 +232,20 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= -go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.2/go.mod h1:2D7ZejHVMIfog1221iLSYlQRzrtECw3kz4I4VAQm3qI= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c= golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -446,7 +271,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -457,10 +281,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -468,11 +290,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -489,20 +309,10 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -512,17 +322,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -533,32 +332,21 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -576,46 +364,22 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY= golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -635,7 +399,6 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -661,7 +424,6 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -670,20 +432,13 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -703,23 +458,6 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -750,7 +488,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -763,42 +500,7 @@ google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -812,22 +514,9 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -840,23 +529,16 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= From 81a7251081e7afbb77b77a64fd3739a5d3500c59 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Fri, 8 Jul 2022 13:49:05 +0200 Subject: [PATCH 119/128] Update base Docker images Signed-off-by: Steven Kriegler --- CHANGELOG.md | 2 ++ Dockerfile | 4 ++-- contrib/Dockerfile | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70c5dbb..49f4de2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### 👻 Maintenance - Remove `fvbock/endless` dependency +- Require Golang 1.18 for builds +- Update base Docker images ## v0.2.1 diff --git a/Dockerfile b/Dockerfile index c6f3eec..12f188d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ################################### # Build stages ################################### -FROM golang:1.18-alpine3.15 AS build-go +FROM golang:1.18-alpine3.16@sha256:7cc62574fcf9c5fb87ad42a9789d5539a6a085971d58ee75dd2ee146cb8a8695 AS build-go ARG GOPROXY ENV GOPROXY ${GOPROXY:-direct} @@ -17,7 +17,7 @@ RUN go build ./cmd/gitea-sonarqube-bot ################################### # Production image ################################### -FROM alpine:3.15 +FROM alpine:3.16@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c LABEL maintainer="justusbunsi " RUN apk update \ diff --git a/contrib/Dockerfile b/contrib/Dockerfile index c51970b..08ab23b 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.18-alpine3.15 +FROM golang:1.18-alpine3.16@sha256:7cc62574fcf9c5fb87ad42a9789d5539a6a085971d58ee75dd2ee146cb8a8695 RUN apk --no-cache add build-base git bash curl openssl npm From 477564bbab38065108f068dfb9fbf17b14f4baf8 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Fri, 8 Jul 2022 14:05:25 +0200 Subject: [PATCH 120/128] Bump dependencies Signed-off-by: Steven Kriegler --- go.mod | 26 ++++++++++++----------- go.sum | 67 ++++++++++++++++++++++++++-------------------------------- 2 files changed, 44 insertions(+), 49 deletions(-) diff --git a/go.mod b/go.mod index 8c09009..4588f2a 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,10 @@ go 1.18 require ( code.gitea.io/sdk/gitea v0.15.1 - github.com/gin-gonic/gin v1.7.7 - github.com/spf13/viper v1.11.0 - github.com/stretchr/testify v1.7.1 - github.com/urfave/cli/v2 v2.6.0 + github.com/gin-gonic/gin v1.8.1 + github.com/spf13/viper v1.12.0 + github.com/stretchr/testify v1.8.0 + github.com/urfave/cli/v2 v2.10.3 ) require ( @@ -18,8 +18,8 @@ require ( github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.11.0 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/hashicorp/go-version v1.4.0 // indirect + github.com/goccy/go-json v0.9.8 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect @@ -29,7 +29,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.1 // indirect + github.com/pelletier/go-toml/v2 v2.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/afero v1.8.2 // indirect @@ -37,13 +37,15 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.4.0 // indirect - github.com/subosito/gotenv v1.2.0 // indirect + github.com/subosito/gotenv v1.4.0 // indirect github.com/ugorji/go/codec v1.2.7 // indirect - golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 // indirect - golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/net v0.0.0-20220706163947-c90051bbdb60 // indirect + golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/protobuf v1.28.0 // indirect - gopkg.in/ini.v1 v1.66.4 // indirect + gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 30b0ede..0f1d22b 100644 --- a/go.sum +++ b/go.sum @@ -66,22 +66,21 @@ github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwV github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= -github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE= +github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -108,8 +107,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -122,7 +119,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -143,15 +140,14 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4= -github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -166,12 +162,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -179,13 +173,12 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw= +github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= @@ -206,8 +199,8 @@ github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmq github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= -github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -217,17 +210,19 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= +github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -github.com/urfave/cli/v2 v2.6.0 h1:yj2Drkflh8X/zUrkWlWlUjZYHyWN7WMmpVxyxXIUyv8= -github.com/urfave/cli/v2 v2.6.0/go.mod h1:oDzoM7pVwz6wHn5ogWgFUU1s4VJayeQS+aEZDqXIEJs= +github.com/urfave/cli/v2 v2.10.3 h1:oi571Fxz5aHugfBAJd5nkwSk3fzATXtMlpxdLylSCMo= +github.com/urfave/cli/v2 v2.10.3/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -246,8 +241,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c= -golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -313,6 +308,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220706163947-c90051bbdb60 h1:8NSylCMxLW4JvserAndSgFL7aPli6A68yf0bYFTcWCM= +golang.org/x/net v0.0.0-20220706163947-c90051bbdb60/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -345,7 +342,6 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -371,8 +367,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY= -golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d h1:/m5NbqQelATgoSPVC2Z23sR4kVNokFwDDyWh/3rGY+I= +golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -438,7 +434,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -528,7 +523,6 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -536,16 +530,15 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= -gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 h1:dbuHpmKjkDzSOMKAWl10QNlgaZUd3V1q99xc81tt2Kc= -gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 99fe6800b0aaa70748aff53b141d8c092bff42fe Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Fri, 8 Jul 2022 14:06:29 +0200 Subject: [PATCH 121/128] Remove all possible binaries on "make clean" Signed-off-by: Steven Kriegler --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 59be2ff..87bd590 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ run: clean: go clean - rm -f ${BINARY_NAME} + rm -f ${BINARY_NAME}* rm -f cover.out cover.html test-report.out test: From 525fa03065fcc83297d0cb5db0717b182ccb2e76 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Mon, 11 Jul 2022 15:24:43 +0200 Subject: [PATCH 122/128] Centralize API response handling Currently, all API handler functions take care of response code and message on their own. This leads to a huge injection chain for HTTP related objects. This refactors the API to consistently return response code and message to the API main entrypoint where the response is created and sent. Now, SonarQube and Gitea will get a response at the very end of any bot action for one request. SonarQube has a timeout of 10 seconds, which may be reached due to network latency. We'll see. Signed-off-by: Steven Kriegler --- internal/api/gitea.go | 60 +++++++++++----------------------- internal/api/gitea_test.go | 16 +++++++-- internal/api/main.go | 21 ++++++++---- internal/api/main_test.go | 31 +++++++++++------- internal/api/sonarqube.go | 34 +++++-------------- internal/api/sonarqube_test.go | 9 ++++- 6 files changed, 85 insertions(+), 86 deletions(-) diff --git a/internal/api/gitea.go b/internal/api/gitea.go index bbb65cd..d9b6d66 100644 --- a/internal/api/gitea.go +++ b/internal/api/gitea.go @@ -1,8 +1,6 @@ package api import ( - "fmt" - "io" "io/ioutil" "log" "net/http" @@ -14,8 +12,8 @@ import ( ) type GiteaWebhookHandlerInferface interface { - HandleSynchronize(rw http.ResponseWriter, r *http.Request) - HandleComment(rw http.ResponseWriter, r *http.Request) + HandleSynchronize(r *http.Request) (int, string) + HandleComment(r *http.Request) (int, string) } type GiteaWebhookHandler struct { @@ -23,7 +21,7 @@ type GiteaWebhookHandler struct { sqSdk sqSdk.SonarQubeSdkInterface } -func (h *GiteaWebhookHandler) parseBody(rw http.ResponseWriter, r *http.Request) ([]byte, error) { +func (h *GiteaWebhookHandler) parseBody(r *http.Request) ([]byte, error) { if r.Body != nil { defer r.Body.Close() } @@ -32,82 +30,62 @@ func (h *GiteaWebhookHandler) parseBody(rw http.ResponseWriter, r *http.Request) if err != nil { log.Printf("Error reading request body %s", err.Error()) - rw.WriteHeader(http.StatusInternalServerError) - io.WriteString(rw, fmt.Sprintf(`{"message": "%s"}`, err.Error())) return nil, err } return raw, nil } -func (h *GiteaWebhookHandler) HandleSynchronize(rw http.ResponseWriter, r *http.Request) { - rw.Header().Set("Content-Type", "application/json") - - raw, err := h.parseBody(rw, r) +func (h *GiteaWebhookHandler) HandleSynchronize(r *http.Request) (int, string) { + raw, err := h.parseBody(r) if err != nil { - return + return http.StatusInternalServerError, err.Error() } ok, err := isValidWebhook(raw, settings.Gitea.Webhook.Secret, r.Header.Get("X-Gitea-Signature"), "Gitea") if !ok { log.Print(err.Error()) - rw.WriteHeader(http.StatusPreconditionFailed) - io.WriteString(rw, fmt.Sprint(`{"message": "Webhook validation failed. Request rejected."}`)) - return + return http.StatusPreconditionFailed, "Webhook validation failed. Request rejected." } w, ok := webhook.NewPullWebhook(raw) if !ok { - rw.WriteHeader(http.StatusUnprocessableEntity) - io.WriteString(rw, `{"message": "Error parsing POST body."}`) - return + return http.StatusUnprocessableEntity, "Error parsing POST body." } if err := w.Validate(); err != nil { - rw.WriteHeader(http.StatusOK) - io.WriteString(rw, fmt.Sprintf(`{"message": "%s"}`, err.Error())) - return + return http.StatusOK, err.Error() } - rw.WriteHeader(http.StatusOK) - io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`) - w.ProcessData(h.giteaSdk, h.sqSdk) + + return http.StatusOK, "Processing data. See bot logs for details." } -func (h *GiteaWebhookHandler) HandleComment(rw http.ResponseWriter, r *http.Request) { - rw.Header().Set("Content-Type", "application/json") - - raw, err := h.parseBody(rw, r) +func (h *GiteaWebhookHandler) HandleComment(r *http.Request) (int, string) { + raw, err := h.parseBody(r) if err != nil { - return + return http.StatusInternalServerError, err.Error() } ok, err := isValidWebhook(raw, settings.Gitea.Webhook.Secret, r.Header.Get("X-Gitea-Signature"), "Gitea") if !ok { log.Print(err.Error()) - rw.WriteHeader(http.StatusPreconditionFailed) - io.WriteString(rw, `{"message": "Webhook validation failed. Request rejected."}`) - return + return http.StatusPreconditionFailed, "Webhook validation failed. Request rejected." } w, ok := webhook.NewCommentWebhook(raw) if !ok { - rw.WriteHeader(http.StatusUnprocessableEntity) - io.WriteString(rw, `{"message": "Error parsing POST body."}`) - return + return http.StatusUnprocessableEntity, "Error parsing POST body." } if err := w.Validate(); err != nil { - rw.WriteHeader(http.StatusOK) - io.WriteString(rw, fmt.Sprintf(`{"message": "%s"}`, err.Error())) - return + return http.StatusOK, err.Error() } - rw.WriteHeader(http.StatusOK) - io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`) - w.ProcessData(h.giteaSdk, h.sqSdk) + + return http.StatusOK, "Processing data. See bot logs for details." } func NewGiteaWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) GiteaWebhookHandlerInferface { diff --git a/internal/api/gitea_test.go b/internal/api/gitea_test.go index 74a407b..5eade86 100644 --- a/internal/api/gitea_test.go +++ b/internal/api/gitea_test.go @@ -2,6 +2,8 @@ package api import ( "bytes" + "fmt" + "io" "net/http" "net/http/httptest" "testing" @@ -20,7 +22,12 @@ func withValidGiteaCommentRequestData(t *testing.T, jsonBody []byte) (*http.Requ } rr := httptest.NewRecorder() - handler := http.HandlerFunc(webhookHandler.HandleComment) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + status, response := webhookHandler.HandleComment(r) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + io.WriteString(w, fmt.Sprintf(`{"message": "%s"}`, response)) + }) return req, rr, handler } @@ -34,7 +41,12 @@ func withValidGiteaSynchronizeRequestData(t *testing.T, jsonBody []byte) (*http. } rr := httptest.NewRecorder() - handler := http.HandlerFunc(webhookHandler.HandleSynchronize) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + status, response := webhookHandler.HandleSynchronize(r) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + io.WriteString(w, fmt.Sprintf(`{"message": "%s"}`, response)) + }) return req, rr, handler } diff --git a/internal/api/main.go b/internal/api/main.go index 7ae869f..1ff8c75 100644 --- a/internal/api/main.go +++ b/internal/api/main.go @@ -40,7 +40,10 @@ func (s *ApiServer) setup() { return } - s.sonarQubeWebhookHandler.Handle(c.Writer, c.Request) + status, response := s.sonarQubeWebhookHandler.Handle(c.Request) + c.JSON(status, gin.H{ + "message": response, + }) }).POST("/hooks/gitea", func(c *gin.Context) { h := validGiteaEndpointHeader{} @@ -49,16 +52,22 @@ func (s *ApiServer) setup() { return } + var status int + var response string + switch h.GiteaEvent { case "pull_request": - s.giteaWebhookHandler.HandleSynchronize(c.Writer, c.Request) + status, response = s.giteaWebhookHandler.HandleSynchronize(c.Request) case "issue_comment": - s.giteaWebhookHandler.HandleComment(c.Writer, c.Request) + status, response = s.giteaWebhookHandler.HandleComment(c.Request) default: - c.JSON(http.StatusOK, gin.H{ - "message": "ignore unknown event", - }) + status = http.StatusOK + response = "ignore unknown event" } + + c.JSON(status, gin.H{ + "message": response, + }) }) } diff --git a/internal/api/main_test.go b/internal/api/main_test.go index 046972a..9fad173 100644 --- a/internal/api/main_test.go +++ b/internal/api/main_test.go @@ -22,20 +22,23 @@ type SonarQubeHandlerMock struct { mock.Mock } -func (h *SonarQubeHandlerMock) Handle(rw http.ResponseWriter, r *http.Request) { - h.Called(rw, r) +func (h *SonarQubeHandlerMock) Handle(r *http.Request) (int, string) { + h.Called(r) + return http.StatusOK, "test-execution" } type GiteaHandlerMock struct { mock.Mock } -func (h *GiteaHandlerMock) HandleSynchronize(rw http.ResponseWriter, r *http.Request) { - h.Called(rw, r) +func (h *GiteaHandlerMock) HandleSynchronize(r *http.Request) (int, string) { + h.Called(r) + return http.StatusOK, "test-execution" } -func (h *GiteaHandlerMock) HandleComment(rw http.ResponseWriter, r *http.Request) { - h.Called(rw, r) +func (h *GiteaHandlerMock) HandleComment(r *http.Request) (int, string) { + h.Called(r) + return http.StatusOK, "test-execution" } type GiteaSdkMock struct { @@ -83,6 +86,8 @@ func (h *SQSdkMock) ComposeGiteaComment(data *sqSdk.CommentComposeData) (string, // SETUP: mute logs func TestMain(m *testing.M) { gin.SetMode(gin.TestMode) + gin.DefaultWriter = ioutil.Discard + gin.DefaultErrorWriter = ioutil.Discard log.SetOutput(ioutil.Discard) os.Exit(m.Run()) } @@ -113,7 +118,7 @@ func TestSonarQubeAPIRouteMissingProjectHeader(t *testing.T) { func TestSonarQubeAPIRouteProcessing(t *testing.T) { sonarQubeHandlerMock := new(SonarQubeHandlerMock) - sonarQubeHandlerMock.On("Handle", mock.Anything, mock.Anything).Return(nil) + sonarQubeHandlerMock.On("Handle", mock.IsType(&http.Request{})) router := New(new(GiteaHandlerMock), sonarQubeHandlerMock) @@ -124,6 +129,7 @@ func TestSonarQubeAPIRouteProcessing(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) sonarQubeHandlerMock.AssertNumberOfCalls(t, "Handle", 1) + sonarQubeHandlerMock.AssertExpectations(t) } func TestGiteaAPIRouteMissingEventHeader(t *testing.T) { @@ -139,7 +145,7 @@ func TestGiteaAPIRouteMissingEventHeader(t *testing.T) { func TestGiteaAPIRouteSynchronizeProcessing(t *testing.T) { giteaHandlerMock := new(GiteaHandlerMock) giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) - giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) + giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Maybe() router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) @@ -151,11 +157,12 @@ func TestGiteaAPIRouteSynchronizeProcessing(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 1) giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 0) + giteaHandlerMock.AssertExpectations(t) } func TestGiteaAPIRouteCommentProcessing(t *testing.T) { giteaHandlerMock := new(GiteaHandlerMock) - giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) + giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Maybe() giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) @@ -168,12 +175,13 @@ func TestGiteaAPIRouteCommentProcessing(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 0) giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 1) + giteaHandlerMock.AssertExpectations(t) } func TestGiteaAPIRouteUnknownEvent(t *testing.T) { giteaHandlerMock := new(GiteaHandlerMock) - giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) - giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) + giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Maybe() + giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Maybe() router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) @@ -185,4 +193,5 @@ func TestGiteaAPIRouteUnknownEvent(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 0) giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 0) + giteaHandlerMock.AssertExpectations(t) } diff --git a/internal/api/sonarqube.go b/internal/api/sonarqube.go index 9ed8b92..3b5bbad 100644 --- a/internal/api/sonarqube.go +++ b/internal/api/sonarqube.go @@ -2,7 +2,6 @@ package api import ( "fmt" - "io" "io/ioutil" "log" "net/http" @@ -15,7 +14,7 @@ import ( ) type SonarQubeWebhookHandlerInferface interface { - Handle(rw http.ResponseWriter, r *http.Request) + Handle(r *http.Request) (int, string) } type SonarQubeWebhookHandler struct { @@ -56,17 +55,12 @@ func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings. h.giteaSdk.PostComment(repo, w.PRIndex, comment) } -func (h *SonarQubeWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request) { - rw.Header().Set("Content-Type", "application/json") - +func (h *SonarQubeWebhookHandler) Handle(r *http.Request) (int, string) { projectName := r.Header.Get("X-SonarQube-Project") found, pIdx := h.inProjectsMapping(settings.Projects, projectName) if !found { log.Printf("Received hook for project '%s' which is not configured. Request ignored.", projectName) - - rw.WriteHeader(http.StatusOK) - io.WriteString(rw, fmt.Sprintf(`{"message": "Project '%s' not in configured list. Request ignored."}`, projectName)) - return + return http.StatusOK, fmt.Sprintf("Project '%s' not in configured list. Request ignored.", projectName) } log.Printf("Received hook for project '%s'. Processing data.", projectName) @@ -78,38 +72,28 @@ func (h *SonarQubeWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request raw, err := ioutil.ReadAll(r.Body) if err != nil { log.Printf("Error reading request body %s", err.Error()) - rw.WriteHeader(http.StatusInternalServerError) - io.WriteString(rw, fmt.Sprintf(`{"message": "%s"}`, err.Error())) - return + return http.StatusInternalServerError, err.Error() } ok, err := isValidWebhook(raw, settings.SonarQube.Webhook.Secret, r.Header.Get("X-Sonar-Webhook-HMAC-SHA256"), "SonarQube") if !ok { log.Print(err.Error()) - rw.WriteHeader(http.StatusPreconditionFailed) - io.WriteString(rw, `{"message": "Webhook validation failed. Request rejected."}`) - return + return http.StatusPreconditionFailed, "Webhook validation failed. Request rejected." } w, ok := webhook.New(raw) if !ok { - rw.WriteHeader(http.StatusUnprocessableEntity) - io.WriteString(rw, `{"message": "Error parsing POST body."}`) - return + return http.StatusUnprocessableEntity, "Error parsing POST body." } - // Send response to SonarQube at this point to ensure being within 10 seconds limit of webhook response timeout - rw.WriteHeader(http.StatusOK) - if strings.ToLower(w.Branch.Type) != "pull_request" { - io.WriteString(rw, `{"message": "Ignore Hook for non-PR analysis."}`) log.Println("Ignore Hook for non-PR analysis") - return + return http.StatusOK, "Ignore Hook for non-PR analysis." } - io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`) - h.processData(w, settings.Projects[pIdx].Gitea) + + return http.StatusOK, "Processing data. See bot logs for details." } func NewSonarQubeWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) SonarQubeWebhookHandlerInferface { diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index d4efd3b..fbd7cee 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -2,6 +2,8 @@ package api import ( "bytes" + "fmt" + "io" "net/http" "net/http/httptest" "regexp" @@ -22,7 +24,12 @@ func withValidSonarQubeRequestData(t *testing.T, jsonBody []byte) (*http.Request req.Header.Set("X-SonarQube-Project", "pr-bot") rr := httptest.NewRecorder() - handler := http.HandlerFunc(webhookHandler.Handle) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + status, response := webhookHandler.Handle(r) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + io.WriteString(w, fmt.Sprintf(`{"message": "%s"}`, response)) + }) return req, rr, handler } From d943a7f420cce1daaed92f02a47366fda099ad58 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Mon, 11 Jul 2022 15:53:28 +0200 Subject: [PATCH 123/128] Add tests for GetRevision Signed-off-by: Steven Kriegler --- internal/webhooks/sonarqube/webhook.go | 10 ++++--- internal/webhooks/sonarqube/webhook_test.go | 33 ++++++++++++++++++++- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index c5bfcce..40da644 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -7,6 +7,10 @@ import ( sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" ) +type properties struct { + OriginalCommit string `json:"sonar.analysis.sqbot,omitempty"` +} + type Webhook struct { ServerUrl string `json:"serverUrl"` Revision string `json:"revision"` @@ -27,10 +31,8 @@ type Webhook struct { Status string } `json:"conditions"` } `json:"qualityGate"` - Properties *struct { - OriginalCommit string `json:"sonar.analysis.sqbot,omitempty"` - } `json:"properties,omitempty"` - PRIndex int + Properties *properties `json:"properties,omitempty"` + PRIndex int } func (w *Webhook) GetRevision() string { diff --git a/internal/webhooks/sonarqube/webhook_test.go b/internal/webhooks/sonarqube/webhook_test.go index 4c8ac9d..02b2010 100644 --- a/internal/webhooks/sonarqube/webhook_test.go +++ b/internal/webhooks/sonarqube/webhook_test.go @@ -14,11 +14,12 @@ func TestNewWebhook(t *testing.T) { RegExp: regexp.MustCompile(`^PR-(\d+)$`), } - raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) + raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": { "sonar.analysis.sqbot": "a84442009c09b1adc278b6bb80a3853419f54007" } }`) response, ok := New(raw) assert.NotNil(t, response) assert.Equal(t, 1337, response.PRIndex) + assert.Equal(t, "a84442009c09b1adc278b6bb80a3853419f54007", response.Properties.OriginalCommit) assert.True(t, ok) t.Cleanup(func() { @@ -47,3 +48,33 @@ func TestNewWebhookInvalidBranchName(t *testing.T) { settings.Pattern = nil }) } + +func TestWebhookGetRevision(t *testing.T) { + t.Run("Default revision", func(t *testing.T) { + w := Webhook{ + Revision: "225fa0306c0ab83297d0cb5db0717b194ccb2e76", + } + + assert.Equal(t, w.Revision, w.GetRevision()) + }) + + t.Run("Default revision due to incomplete properties", func(t *testing.T) { + w := Webhook{ + Revision: "225fa0306c0ab83297d0cb5db0717b194ccb2e76", + Properties: &properties{}, + } + + assert.Equal(t, w.Revision, w.GetRevision()) + }) + + t.Run("Original commit from properties", func(t *testing.T) { + w := Webhook{ + Revision: "225fa0306c0ab83297d0cb5db0717b194ccb2e76", + Properties: &properties{ + OriginalCommit: "a9fe6800b0bbb70748aff53a011d8c09bbff42fe", + }, + } + + assert.Equal(t, w.Properties.OriginalCommit, w.GetRevision()) + }) +} From 51211d77cd58d0d12803434049a5a90ca42a6b3d Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Mon, 11 Jul 2022 17:39:22 +0200 Subject: [PATCH 124/128] Change internal name Signed-off-by: Steven Kriegler --- CONTRIBUTING.md | 6 +++--- cmd/gitea-sonarqube-bot/main.go | 8 ++++---- go.mod | 2 +- internal/api/gitea.go | 8 ++++---- internal/api/gitea_test.go | 2 +- internal/api/main_test.go | 6 +++--- internal/api/sonarqube.go | 8 ++++---- internal/api/sonarqube_test.go | 2 +- internal/clients/gitea/gitea.go | 4 ++-- internal/clients/sonarqube/sonarqube.go | 4 ++-- internal/clients/sonarqube/sonarqube_test.go | 2 +- internal/settings/settings_test.go | 10 +++++----- internal/webhooks/gitea/comment.go | 8 ++++---- internal/webhooks/gitea/pull.go | 6 +++--- internal/webhooks/sonarqube/webhook.go | 2 +- internal/webhooks/sonarqube/webhook_test.go | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 18 files changed, 43 insertions(+), 43 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c14df07..413699a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,10 +15,10 @@ ```bash # Build docker environment -docker build -t gitea-sonarqube-pr-bot/dev -f contrib/Dockerfile contrib +docker build -t gitea-sonarqube-bot/dev -f contrib/Dockerfile contrib # Start the environment -docker run --rm -it -p 49182:3000 -v "$(pwd):/projects" gitea-sonarqube-pr-bot/dev +docker run --rm -it -p 49182:3000 -v "$(pwd):/projects" gitea-sonarqube-bot/dev ``` ## Build and Run @@ -55,7 +55,7 @@ make helm-params For local purposes ```bash -docker build -t gitea-sonarqube-pr-bot/prod . +docker build -t gitea-sonarqube-bot/prod . ``` **Docker image** diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index ed949bc..ccbbcee 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -11,10 +11,10 @@ import ( "syscall" "time" - "gitea-sonarqube-pr-bot/internal/api" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sonarQubeSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" - "gitea-sonarqube-pr-bot/internal/settings" + "gitea-sonarqube-bot/internal/api" + giteaSdk "gitea-sonarqube-bot/internal/clients/gitea" + sonarQubeSdk "gitea-sonarqube-bot/internal/clients/sonarqube" + "gitea-sonarqube-bot/internal/settings" "github.com/urfave/cli/v2" ) diff --git a/go.mod b/go.mod index 4588f2a..ac4f1ae 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module gitea-sonarqube-pr-bot +module gitea-sonarqube-bot go 1.18 diff --git a/internal/api/gitea.go b/internal/api/gitea.go index d9b6d66..fbe2022 100644 --- a/internal/api/gitea.go +++ b/internal/api/gitea.go @@ -5,10 +5,10 @@ import ( "log" "net/http" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" - "gitea-sonarqube-pr-bot/internal/settings" - webhook "gitea-sonarqube-pr-bot/internal/webhooks/gitea" + giteaSdk "gitea-sonarqube-bot/internal/clients/gitea" + sqSdk "gitea-sonarqube-bot/internal/clients/sonarqube" + "gitea-sonarqube-bot/internal/settings" + webhook "gitea-sonarqube-bot/internal/webhooks/gitea" ) type GiteaWebhookHandlerInferface interface { diff --git a/internal/api/gitea_test.go b/internal/api/gitea_test.go index 5eade86..e82b05b 100644 --- a/internal/api/gitea_test.go +++ b/internal/api/gitea_test.go @@ -8,7 +8,7 @@ import ( "net/http/httptest" "testing" - "gitea-sonarqube-pr-bot/internal/settings" + "gitea-sonarqube-bot/internal/settings" "github.com/stretchr/testify/assert" ) diff --git a/internal/api/main_test.go b/internal/api/main_test.go index 9fad173..6fdd816 100644 --- a/internal/api/main_test.go +++ b/internal/api/main_test.go @@ -9,9 +9,9 @@ import ( "os" "testing" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" - "gitea-sonarqube-pr-bot/internal/settings" + giteaSdk "gitea-sonarqube-bot/internal/clients/gitea" + sqSdk "gitea-sonarqube-bot/internal/clients/sonarqube" + "gitea-sonarqube-bot/internal/settings" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" diff --git a/internal/api/sonarqube.go b/internal/api/sonarqube.go index 3b5bbad..48cf291 100644 --- a/internal/api/sonarqube.go +++ b/internal/api/sonarqube.go @@ -7,10 +7,10 @@ import ( "net/http" "strings" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" - "gitea-sonarqube-pr-bot/internal/settings" - webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" + giteaSdk "gitea-sonarqube-bot/internal/clients/gitea" + sqSdk "gitea-sonarqube-bot/internal/clients/sonarqube" + "gitea-sonarqube-bot/internal/settings" + webhook "gitea-sonarqube-bot/internal/webhooks/sonarqube" ) type SonarQubeWebhookHandlerInferface interface { diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index fbd7cee..8f71c6b 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -9,7 +9,7 @@ import ( "regexp" "testing" - "gitea-sonarqube-pr-bot/internal/settings" + "gitea-sonarqube-bot/internal/settings" "github.com/stretchr/testify/assert" ) diff --git a/internal/clients/gitea/gitea.go b/internal/clients/gitea/gitea.go index d48e66c..4aba30d 100644 --- a/internal/clients/gitea/gitea.go +++ b/internal/clients/gitea/gitea.go @@ -2,7 +2,7 @@ package gitea import ( "fmt" - "gitea-sonarqube-pr-bot/internal/settings" + "gitea-sonarqube-bot/internal/settings" "log" "code.gitea.io/sdk/gitea" @@ -31,7 +31,7 @@ func (sdk *GiteaSdk) PostComment(repo settings.GiteaRepository, idx int, msg str func (sdk *GiteaSdk) UpdateStatus(repo settings.GiteaRepository, ref string, details StatusDetails) error { opt := gitea.CreateStatusOption{ TargetURL: details.Url, - Context: "gitea-sonarqube-pr-bot", + Context: "gitea-sonarqube-bot", Description: details.Message, State: gitea.StatusState(details.State), } diff --git a/internal/clients/sonarqube/sonarqube.go b/internal/clients/sonarqube/sonarqube.go index aecfb5f..4403e08 100644 --- a/internal/clients/sonarqube/sonarqube.go +++ b/internal/clients/sonarqube/sonarqube.go @@ -10,8 +10,8 @@ import ( "strconv" "strings" - "gitea-sonarqube-pr-bot/internal/actions" - "gitea-sonarqube-pr-bot/internal/settings" + "gitea-sonarqube-bot/internal/actions" + "gitea-sonarqube-bot/internal/settings" ) func ParsePRIndex(name string) (int, error) { diff --git a/internal/clients/sonarqube/sonarqube_test.go b/internal/clients/sonarqube/sonarqube_test.go index d3ef651..928fce1 100644 --- a/internal/clients/sonarqube/sonarqube_test.go +++ b/internal/clients/sonarqube/sonarqube_test.go @@ -8,7 +8,7 @@ import ( "regexp" "testing" - "gitea-sonarqube-pr-bot/internal/settings" + "gitea-sonarqube-bot/internal/settings" "github.com/stretchr/testify/assert" ) diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 295ed36..9513e2e 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -26,7 +26,7 @@ sonarqube: additionalMetrics: [] projects: - sonarqube: - key: gitea-sonarqube-pr-bot + key: gitea-sonarqube-bot gitea: owner: example-organization name: pr-bot @@ -130,7 +130,7 @@ sonarqube: additionalMetrics: "new_security_hotspots" projects: - sonarqube: - key: gitea-sonarqube-pr-bot + key: gitea-sonarqube-bot gitea: owner: example-organization name: pr-bot @@ -203,7 +203,7 @@ sonarqube: value: fake-sonarqube-token projects: - sonarqube: - key: gitea-sonarqube-pr-bot + key: gitea-sonarqube-bot gitea: owner: example-organization name: pr-bot @@ -261,7 +261,7 @@ func TestLoadProjectsStructure(t *testing.T) { expectedProjects := []Project{ { SonarQube: struct{ Key string }{ - Key: "gitea-sonarqube-pr-bot", + Key: "gitea-sonarqube-bot", }, Gitea: GiteaRepository{ Owner: "example-organization", @@ -319,7 +319,7 @@ sonarqube: additionalMetrics: "new_security_hotspots" projects: - sonarqube: - key: gitea-sonarqube-pr-bot + key: gitea-sonarqube-bot gitea: owner: example-organization name: pr-bot diff --git a/internal/webhooks/gitea/comment.go b/internal/webhooks/gitea/comment.go index 4daf6c9..7aa121c 100644 --- a/internal/webhooks/gitea/comment.go +++ b/internal/webhooks/gitea/comment.go @@ -5,10 +5,10 @@ import ( "fmt" "log" - "gitea-sonarqube-pr-bot/internal/actions" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" - "gitea-sonarqube-pr-bot/internal/settings" + "gitea-sonarqube-bot/internal/actions" + giteaSdk "gitea-sonarqube-bot/internal/clients/gitea" + sqSdk "gitea-sonarqube-bot/internal/clients/sonarqube" + "gitea-sonarqube-bot/internal/settings" ) type issue struct { diff --git a/internal/webhooks/gitea/pull.go b/internal/webhooks/gitea/pull.go index 5a71ad1..9e6b6de 100644 --- a/internal/webhooks/gitea/pull.go +++ b/internal/webhooks/gitea/pull.go @@ -5,9 +5,9 @@ import ( "fmt" "log" - giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" - "gitea-sonarqube-pr-bot/internal/settings" + giteaSdk "gitea-sonarqube-bot/internal/clients/gitea" + sqSdk "gitea-sonarqube-bot/internal/clients/sonarqube" + "gitea-sonarqube-bot/internal/settings" ) type pullRequest struct { diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index 40da644..555cdf9 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -4,7 +4,7 @@ import ( "encoding/json" "log" - sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" + sqSdk "gitea-sonarqube-bot/internal/clients/sonarqube" ) type properties struct { diff --git a/internal/webhooks/sonarqube/webhook_test.go b/internal/webhooks/sonarqube/webhook_test.go index 02b2010..58c555d 100644 --- a/internal/webhooks/sonarqube/webhook_test.go +++ b/internal/webhooks/sonarqube/webhook_test.go @@ -4,7 +4,7 @@ import ( "regexp" "testing" - "gitea-sonarqube-pr-bot/internal/settings" + "gitea-sonarqube-bot/internal/settings" "github.com/stretchr/testify/assert" ) diff --git a/package-lock.json b/package-lock.json index 43e4f53..8cdf948 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,10 +1,10 @@ { - "name": "gitea-sonarqube-pr-bot", + "name": "gitea-sonarqube-bot", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "gitea-sonarqube-pr-bot", + "name": "gitea-sonarqube-bot", "license": "MIT", "devDependencies": { "readme-generator-for-helm": "https://github.com/bitnami-labs/readme-generator-for-helm/tarball/main" diff --git a/package.json b/package.json index 98aa01f..0f7b790 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "gitea-sonarqube-pr-bot", + "name": "gitea-sonarqube-bot", "description": "Integrate SonarQube analysis into Gitea Pull Requests", "author": "Steven Kriegler ", "license": "MIT", From 54beca9c25fd8798585196a583f400442a234d7a Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Tue, 12 Jul 2022 11:20:08 +0200 Subject: [PATCH 125/128] Introduce better test case structure Signed-off-by: Steven Kriegler --- internal/actions/actions_test.go | 16 +- internal/api/gitea_test.go | 402 ++++----- internal/api/main_test.go | 190 +++-- internal/api/request_validation_test.go | 57 +- internal/api/sonarqube_test.go | 260 +++--- internal/clients/sonarqube/sonarqube_test.go | 828 ++++++++++--------- internal/settings/settings_test.go | 485 +++++------ internal/webhooks/sonarqube/webhook_test.go | 58 +- 8 files changed, 1169 insertions(+), 1127 deletions(-) diff --git a/internal/actions/actions_test.go b/internal/actions/actions_test.go index 14e13c7..21eebf5 100644 --- a/internal/actions/actions_test.go +++ b/internal/actions/actions_test.go @@ -6,12 +6,14 @@ import ( "github.com/stretchr/testify/assert" ) -func TestIsValidBotCommentForInvalidComment(t *testing.T) { - assert.False(t, IsValidBotComment(""), "Undetected missing action prefix") - assert.False(t, IsValidBotComment("/sq-bot invalid-command"), "Undetected invalid bot command") - assert.False(t, IsValidBotComment("Some context with /sq-bot review within"), "Incorrect bot prefix detected inside random comment") -} +func TestIsValidBotComment(t *testing.T) { + t.Run("Valid", func(t *testing.T) { + assert.True(t, IsValidBotComment("/sq-bot review"), "Correct bot comment not recognized") + }) -func TestIsValidBotCommentForValidComment(t *testing.T) { - assert.True(t, IsValidBotComment("/sq-bot review"), "Correct bot comment not recognized") + t.Run("Invalid", func(t *testing.T) { + assert.False(t, IsValidBotComment(""), "Undetected missing action prefix") + assert.False(t, IsValidBotComment("/sq-bot invalid-command"), "Undetected invalid bot command") + assert.False(t, IsValidBotComment("Some context with /sq-bot review within"), "Incorrect bot prefix detected inside random comment") + }) } diff --git a/internal/api/gitea_test.go b/internal/api/gitea_test.go index e82b05b..9702ae0 100644 --- a/internal/api/gitea_test.go +++ b/internal/api/gitea_test.go @@ -13,226 +13,230 @@ import ( "github.com/stretchr/testify/assert" ) -func withValidGiteaCommentRequestData(t *testing.T, jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) { - webhookHandler := NewGiteaWebhookHandler(new(GiteaSdkMock), new(SQSdkMock)) +func TestHandleGiteaCommentWebhook(t *testing.T) { + withValidRequestData := func(t *testing.T, jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) { + webhookHandler := NewGiteaWebhookHandler(new(GiteaSdkMock), new(SQSdkMock)) - req, err := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer(jsonBody)) - if err != nil { - t.Fatal(err) + req, err := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer(jsonBody)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + status, response := webhookHandler.HandleComment(r) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + io.WriteString(w, fmt.Sprintf(`{"message": "%s"}`, response)) + }) + + return req, rr, handler } - rr := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - status, response := webhookHandler.HandleComment(r) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - io.WriteString(w, fmt.Sprintf(`{"message": "%s"}`, response)) + t.Run("On success", func(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + Gitea: settings.GiteaRepository{ + Owner: "test-user", + Name: "gitea-sonarqube-bot", + }, + }, + } + req, rr, handler := withValidRequestData(t, []byte(`{"action":"created","issue":{"id":1,"url":"http://localhost:3000/api/v1/repos/test-user/gitea-sonarqube-bot/issues/1","html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"title":"„README.md“ ändern","body":"","ref":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:57:29Z","closed_at":null,"due_date":null,"pull_request":{"merged":false,"merged_at":null},"repository":{"id":1,"name":"gitea-sonarqube-bot","owner":"test-user","full_name":"test-user/gitea-sonarqube-bot"}},"comment":{"id":2,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1#issuecomment-2","pull_request_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","issue_url":"","user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"body":"/sq-bot review","created_at":"2022-05-15T18:57:29Z","updated_at":"2022-05-15T18:57:29Z"},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":1,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"is_pull":true}`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) }) - return req, rr, handler -} + t.Run("With invalid JSON body", func(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + }, + } -func withValidGiteaSynchronizeRequestData(t *testing.T, jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) { - webhookHandler := NewGiteaWebhookHandler(new(GiteaSdkMock), new(SQSdkMock)) + req, rr, handler := withValidRequestData(t, []byte(`{ "action": ["non-string-value-for-action"] }`)) + handler.ServeHTTP(rr, req) - req, err := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer(jsonBody)) - if err != nil { - t.Fatal(err) - } + assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) + assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) - rr := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - status, response := webhookHandler.HandleSynchronize(r) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - io.WriteString(w, fmt.Sprintf(`{"message": "%s"}`, response)) + t.Cleanup(func() { + settings.Pattern = nil + }) }) - return req, rr, handler -} - -func TestHandleGiteaCommentWebhookSuccess(t *testing.T) { - settings.Pattern = &settings.PatternConfig{ - Template: "PR-%d", - } - settings.Gitea = settings.GiteaConfig{ - Webhook: &settings.Webhook{ - Secret: "", - }, - } - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "gitea-sonarqube-bot", + t.Run("With invalid signature", func(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "gitea-comment-test-webhook", }, - Gitea: settings.GiteaRepository{ - Owner: "test-user", - Name: "gitea-sonarqube-bot", + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "pr-bot", + }, }, - }, - } - req, rr, handler := withValidGiteaCommentRequestData(t, []byte(`{"action":"created","issue":{"id":1,"url":"http://localhost:3000/api/v1/repos/test-user/gitea-sonarqube-bot/issues/1","html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"title":"„README.md“ ändern","body":"","ref":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:57:29Z","closed_at":null,"due_date":null,"pull_request":{"merged":false,"merged_at":null},"repository":{"id":1,"name":"gitea-sonarqube-bot","owner":"test-user","full_name":"test-user/gitea-sonarqube-bot"}},"comment":{"id":2,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1#issuecomment-2","pull_request_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","issue_url":"","user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"body":"/sq-bot review","created_at":"2022-05-15T18:57:29Z","updated_at":"2022-05-15T18:57:29Z"},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":1,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"is_pull":true}`)) - handler.ServeHTTP(rr, req) + } + req, rr, handler := withValidRequestData(t, []byte(`{"action":"created","issue":{"id":1,"url":"http://localhost:3000/api/v1/repos/test-user/gitea-sonarqube-bot/issues/1","html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"title":"„README.md“ ändern","body":"","ref":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:57:29Z","closed_at":null,"due_date":null,"pull_request":{"merged":false,"merged_at":null},"repository":{"id":1,"name":"gitea-sonarqube-bot","owner":"test-user","full_name":"test-user/gitea-sonarqube-bot"}},"comment":{"id":2,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1#issuecomment-2","pull_request_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","issue_url":"","user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"body":"/sq-bot review","created_at":"2022-05-15T18:57:29Z","updated_at":"2022-05-15T18:57:29Z"},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":1,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"is_pull":true}`)) + req.Header.Set("X-Gitea-Signature", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467") + handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + assert.Equal(t, http.StatusPreconditionFailed, rr.Code) + assert.Equal(t, `{"message": "Webhook validation failed. Request rejected."}`, rr.Body.String()) + }) - t.Cleanup(func() { - settings.Pattern = nil + t.Run("With ignored project", func(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + }, + } + req, rr, handler := withValidRequestData(t, []byte(`{"action":"created","issue":{"id":1,"url":"http://localhost:3000/api/v1/repos/test-user/gitea-sonarqube-bot/issues/1","html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"title":"„README.md“ ändern","body":"","ref":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:57:29Z","closed_at":null,"due_date":null,"pull_request":{"merged":false,"merged_at":null},"repository":{"id":1,"name":"gitea-sonarqube-bot","owner":"test-user","full_name":"test-user/gitea-sonarqube-bot"}},"comment":{"id":2,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1#issuecomment-2","pull_request_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","issue_url":"","user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"body":"/sq-bot review","created_at":"2022-05-15T18:57:29Z","updated_at":"2022-05-15T18:57:29Z"},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":1,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"is_pull":true}`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "ignore hook for non-configured project 'test-user/gitea-sonarqube-bot'"}`, rr.Body.String()) }) } -func TestHandleGiteaCommentWebhookInvalidJSONBody(t *testing.T) { - settings.Pattern = &settings.PatternConfig{ - Template: "PR-%d", +func TestHandleGiteaSynchronizeWebhook(t *testing.T) { + withValidRequestData := func(t *testing.T, jsonBody []byte) (*http.Request, *httptest.ResponseRecorder, http.HandlerFunc) { + webhookHandler := NewGiteaWebhookHandler(new(GiteaSdkMock), new(SQSdkMock)) + + req, err := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer(jsonBody)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + status, response := webhookHandler.HandleSynchronize(r) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + io.WriteString(w, fmt.Sprintf(`{"message": "%s"}`, response)) + }) + + return req, rr, handler } - settings.Gitea = settings.GiteaConfig{ - Webhook: &settings.Webhook{ - Secret: "", - }, - } - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "gitea-sonarqube-bot", + + t.Run("On success", func(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "", }, - }, - } + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + Gitea: settings.GiteaRepository{ + Owner: "test-user", + Name: "gitea-sonarqube-bot", + }, + }, + } + req, rr, handler := withValidRequestData(t, []byte(`{"action":"opened","number":1,"pull_request":{"id":1,"url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"title":"„README.md“ ändern","body":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","diff_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.diff","patch_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"base":{"label":"main","ref":"main","sha":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"head":{"label":"test-user-patch-1","ref":"test-user-patch-1","sha":"4d3f126f7f6b76c01187a06ec704a8a3055591de","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"merge_base":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","due_date":null,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:46:19Z","closed_at":null},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"review":null}`)) + handler.ServeHTTP(rr, req) - req, rr, handler := withValidGiteaCommentRequestData(t, []byte(`{ "action": ["non-string-value-for-action"] }`)) - handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + }) - assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) - assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) + t.Run("With invalid JSON body", func(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + }, + } - t.Cleanup(func() { - settings.Pattern = nil + req, rr, handler := withValidRequestData(t, []byte(`{ "action": ["non-string-value-for-action"] }`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) + assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) + }) + + t.Run("With invalid signature", func(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "gitea-synchronize-test-webhook", + }, + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "pr-bot", + }, + }, + } + req, rr, handler := withValidRequestData(t, []byte(`{"action":"opened","number":1,"pull_request":{"id":1,"url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"title":"„README.md“ ändern","body":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","diff_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.diff","patch_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"base":{"label":"main","ref":"main","sha":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"head":{"label":"test-user-patch-1","ref":"test-user-patch-1","sha":"4d3f126f7f6b76c01187a06ec704a8a3055591de","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"merge_base":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","due_date":null,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:46:19Z","closed_at":null},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"review":null}`)) + req.Header.Set("X-Gitea-Signature", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467") + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusPreconditionFailed, rr.Code) + assert.Equal(t, `{"message": "Webhook validation failed. Request rejected."}`, rr.Body.String()) + }) + + t.Run("With ignored project", func(t *testing.T) { + settings.Gitea = settings.GiteaConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + }, + } + req, rr, handler := withValidRequestData(t, []byte(`{"action":"opened","number":1,"pull_request":{"id":1,"url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"title":"„README.md“ ändern","body":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","diff_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.diff","patch_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"base":{"label":"main","ref":"main","sha":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"head":{"label":"test-user-patch-1","ref":"test-user-patch-1","sha":"4d3f126f7f6b76c01187a06ec704a8a3055591de","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"merge_base":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","due_date":null,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:46:19Z","closed_at":null},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"review":null}`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "ignore hook for non-configured project 'test-user/gitea-sonarqube-bot'"}`, rr.Body.String()) }) } - -func TestHandleGiteaCommentInvalidWebhookSignature(t *testing.T) { - settings.Gitea = settings.GiteaConfig{ - Webhook: &settings.Webhook{ - Secret: "gitea-comment-test-webhook", - }, - } - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "pr-bot", - }, - }, - } - req, rr, handler := withValidGiteaCommentRequestData(t, []byte(`{"action":"created","issue":{"id":1,"url":"http://localhost:3000/api/v1/repos/test-user/gitea-sonarqube-bot/issues/1","html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"title":"„README.md“ ändern","body":"","ref":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:57:29Z","closed_at":null,"due_date":null,"pull_request":{"merged":false,"merged_at":null},"repository":{"id":1,"name":"gitea-sonarqube-bot","owner":"test-user","full_name":"test-user/gitea-sonarqube-bot"}},"comment":{"id":2,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1#issuecomment-2","pull_request_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","issue_url":"","user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"body":"/sq-bot review","created_at":"2022-05-15T18:57:29Z","updated_at":"2022-05-15T18:57:29Z"},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":1,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"is_pull":true}`)) - req.Header.Set("X-Gitea-Signature", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467") - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusPreconditionFailed, rr.Code) - assert.Equal(t, `{"message": "Webhook validation failed. Request rejected."}`, rr.Body.String()) -} - -func TestHandleGiteaCommentWebhookIgnoredProject(t *testing.T) { - settings.Gitea = settings.GiteaConfig{ - Webhook: &settings.Webhook{ - Secret: "", - }, - } - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "gitea-sonarqube-bot", - }, - }, - } - req, rr, handler := withValidGiteaCommentRequestData(t, []byte(`{"action":"created","issue":{"id":1,"url":"http://localhost:3000/api/v1/repos/test-user/gitea-sonarqube-bot/issues/1","html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"title":"„README.md“ ändern","body":"","ref":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:57:29Z","closed_at":null,"due_date":null,"pull_request":{"merged":false,"merged_at":null},"repository":{"id":1,"name":"gitea-sonarqube-bot","owner":"test-user","full_name":"test-user/gitea-sonarqube-bot"}},"comment":{"id":2,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1#issuecomment-2","pull_request_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","issue_url":"","user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"original_author":"","original_author_id":0,"body":"/sq-bot review","created_at":"2022-05-15T18:57:29Z","updated_at":"2022-05-15T18:57:29Z"},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":1,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"is_pull":true}`)) - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, `{"message": "ignore hook for non-configured project 'test-user/gitea-sonarqube-bot'"}`, rr.Body.String()) -} - -func TestHandleGiteaSynchronizeWebhookSuccess(t *testing.T) { - settings.Gitea = settings.GiteaConfig{ - Webhook: &settings.Webhook{ - Secret: "", - }, - } - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "gitea-sonarqube-bot", - }, - Gitea: settings.GiteaRepository{ - Owner: "test-user", - Name: "gitea-sonarqube-bot", - }, - }, - } - req, rr, handler := withValidGiteaSynchronizeRequestData(t, []byte(`{"action":"opened","number":1,"pull_request":{"id":1,"url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"title":"„README.md“ ändern","body":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","diff_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.diff","patch_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"base":{"label":"main","ref":"main","sha":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"head":{"label":"test-user-patch-1","ref":"test-user-patch-1","sha":"4d3f126f7f6b76c01187a06ec704a8a3055591de","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"merge_base":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","due_date":null,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:46:19Z","closed_at":null},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"review":null}`)) - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) -} - -func TestHandleGiteaSynchronizeWebhookInvalidJSONBody(t *testing.T) { - settings.Gitea = settings.GiteaConfig{ - Webhook: &settings.Webhook{ - Secret: "", - }, - } - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "gitea-sonarqube-bot", - }, - }, - } - - req, rr, handler := withValidGiteaSynchronizeRequestData(t, []byte(`{ "action": ["non-string-value-for-action"] }`)) - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) - assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) -} - -func TestHandleGiteaSynchronizeInvalidWebhookSignature(t *testing.T) { - settings.Gitea = settings.GiteaConfig{ - Webhook: &settings.Webhook{ - Secret: "gitea-synchronize-test-webhook", - }, - } - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "pr-bot", - }, - }, - } - req, rr, handler := withValidGiteaSynchronizeRequestData(t, []byte(`{"action":"opened","number":1,"pull_request":{"id":1,"url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"title":"„README.md“ ändern","body":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","diff_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.diff","patch_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"base":{"label":"main","ref":"main","sha":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"head":{"label":"test-user-patch-1","ref":"test-user-patch-1","sha":"4d3f126f7f6b76c01187a06ec704a8a3055591de","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"merge_base":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","due_date":null,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:46:19Z","closed_at":null},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"review":null}`)) - req.Header.Set("X-Gitea-Signature", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467") - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusPreconditionFailed, rr.Code) - assert.Equal(t, `{"message": "Webhook validation failed. Request rejected."}`, rr.Body.String()) -} - -func TestHandleGiteaSynchronizeWebhookIgnoredProject(t *testing.T) { - settings.Gitea = settings.GiteaConfig{ - Webhook: &settings.Webhook{ - Secret: "", - }, - } - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "gitea-sonarqube-bot", - }, - }, - } - req, rr, handler := withValidGiteaSynchronizeRequestData(t, []byte(`{"action":"opened","number":1,"pull_request":{"id":1,"url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","number":1,"user":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"title":"„README.md“ ändern","body":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1","diff_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.diff","patch_url":"http://localhost:3000/test-user/gitea-sonarqube-bot/pulls/1.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"base":{"label":"main","ref":"main","sha":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"head":{"label":"test-user-patch-1","ref":"test-user-patch-1","sha":"4d3f126f7f6b76c01187a06ec704a8a3055591de","repo_id":1,"repo":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":false,"push":false,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null}},"merge_base":"2e5c9f7fe85fd8fb6019b3dd299744e0afce076b","due_date":null,"created_at":"2022-05-15T18:46:19Z","updated_at":"2022-05-15T18:46:19Z","closed_at":null},"repository":{"id":1,"owner":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"name":"gitea-sonarqube-bot","full_name":"test-user/gitea-sonarqube-bot","description":"","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":110,"html_url":"http://localhost:3000/test-user/gitea-sonarqube-bot","ssh_url":"git@localhost:test-user/gitea-sonarqube-bot.git","clone_url":"http://localhost:3000/test-user/gitea-sonarqube-bot.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2022-05-15T18:45:46Z","updated_at":"2022-05-15T18:46:09Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null},"sender":{"id":1,"login":"test-user","full_name":"","email":"a@b.c","avatar_url":"http://localhost:3000/avatar/5d60d4e28066df254d5452f92c910092","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2022-05-15T18:42:54Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"test-user"},"review":null}`)) - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, `{"message": "ignore hook for non-configured project 'test-user/gitea-sonarqube-bot'"}`, rr.Body.String()) -} diff --git a/internal/api/main_test.go b/internal/api/main_test.go index 6fdd816..ecac2a7 100644 --- a/internal/api/main_test.go +++ b/internal/api/main_test.go @@ -93,105 +93,115 @@ func TestMain(m *testing.M) { } func TestNonAPIRoutes(t *testing.T) { - router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) + t.Run("favicon", func(t *testing.T) { + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/favicon.ico", nil) - router.Engine.ServeHTTP(w, req) - assert.Equal(t, http.StatusNoContent, w.Code) + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/favicon.ico", nil) + router.Engine.ServeHTTP(w, req) + assert.Equal(t, http.StatusNoContent, w.Code) + }) - w = httptest.NewRecorder() - req, _ = http.NewRequest("GET", "/ping", nil) - router.Engine.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code) + t.Run("ping", func(t *testing.T) { + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/ping", nil) + router.Engine.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + }) } -func TestSonarQubeAPIRouteMissingProjectHeader(t *testing.T) { - router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) +func TestSonarQubeAPIRoute(t *testing.T) { + t.Run("Missing project header", func(t *testing.T) { + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer([]byte(`{}`))) - router.Engine.ServeHTTP(w, req) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer([]byte(`{}`))) + router.Engine.ServeHTTP(w, req) - assert.Equal(t, http.StatusNotFound, w.Code) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("Processing", func(t *testing.T) { + sonarQubeHandlerMock := new(SonarQubeHandlerMock) + sonarQubeHandlerMock.On("Handle", mock.IsType(&http.Request{})) + + router := New(new(GiteaHandlerMock), sonarQubeHandlerMock) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer([]byte(`{}`))) + req.Header.Add("X-SonarQube-Project", "gitea-sonarqube-bot") + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + sonarQubeHandlerMock.AssertNumberOfCalls(t, "Handle", 1) + sonarQubeHandlerMock.AssertExpectations(t) + }) } -func TestSonarQubeAPIRouteProcessing(t *testing.T) { - sonarQubeHandlerMock := new(SonarQubeHandlerMock) - sonarQubeHandlerMock.On("Handle", mock.IsType(&http.Request{})) +func TestGiteaAPIRoute(t *testing.T) { + t.Run("Missing event header", func(t *testing.T) { + router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) - router := New(new(GiteaHandlerMock), sonarQubeHandlerMock) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) + router.Engine.ServeHTTP(w, req) - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/hooks/sonarqube", bytes.NewBuffer([]byte(`{}`))) - req.Header.Add("X-SonarQube-Project", "gitea-sonarqube-bot") - router.Engine.ServeHTTP(w, req) + assert.Equal(t, http.StatusNotFound, w.Code) + }) - assert.Equal(t, http.StatusOK, w.Code) - sonarQubeHandlerMock.AssertNumberOfCalls(t, "Handle", 1) - sonarQubeHandlerMock.AssertExpectations(t) -} - -func TestGiteaAPIRouteMissingEventHeader(t *testing.T) { - router := New(new(GiteaHandlerMock), new(SonarQubeHandlerMock)) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) - router.Engine.ServeHTTP(w, req) - - assert.Equal(t, http.StatusNotFound, w.Code) -} - -func TestGiteaAPIRouteSynchronizeProcessing(t *testing.T) { - giteaHandlerMock := new(GiteaHandlerMock) - giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) - giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Maybe() - - router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) - req.Header.Add("X-Gitea-Event", "pull_request") - router.Engine.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 1) - giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 0) - giteaHandlerMock.AssertExpectations(t) -} - -func TestGiteaAPIRouteCommentProcessing(t *testing.T) { - giteaHandlerMock := new(GiteaHandlerMock) - giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Maybe() - giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) - - router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) - req.Header.Add("X-Gitea-Event", "issue_comment") - router.Engine.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 0) - giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 1) - giteaHandlerMock.AssertExpectations(t) -} - -func TestGiteaAPIRouteUnknownEvent(t *testing.T) { - giteaHandlerMock := new(GiteaHandlerMock) - giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Maybe() - giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Maybe() - - router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) - req.Header.Add("X-Gitea-Event", "unknown") - router.Engine.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 0) - giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 0) - giteaHandlerMock.AssertExpectations(t) + t.Run("Processing synchronize", func(t *testing.T) { + giteaHandlerMock := new(GiteaHandlerMock) + giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Return(nil) + giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Maybe() + + router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) + req.Header.Add("X-Gitea-Event", "pull_request") + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 1) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 0) + giteaHandlerMock.AssertExpectations(t) + }) + + t.Run("Processing comment", func(t *testing.T) { + giteaHandlerMock := new(GiteaHandlerMock) + giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Maybe() + giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Return(nil) + + router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) + req.Header.Add("X-Gitea-Event", "issue_comment") + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 0) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 1) + giteaHandlerMock.AssertExpectations(t) + }) + + t.Run("Unknown event", func(t *testing.T) { + giteaHandlerMock := new(GiteaHandlerMock) + giteaHandlerMock.On("HandleSynchronize", mock.Anything, mock.Anything).Maybe() + giteaHandlerMock.On("HandleComment", mock.Anything, mock.Anything).Maybe() + + router := New(giteaHandlerMock, new(SonarQubeHandlerMock)) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/hooks/gitea", bytes.NewBuffer([]byte(`{}`))) + req.Header.Add("X-Gitea-Event", "unknown") + router.Engine.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleSynchronize", 0) + giteaHandlerMock.AssertNumberOfCalls(t, "HandleComment", 0) + giteaHandlerMock.AssertExpectations(t) + }) } diff --git a/internal/api/request_validation_test.go b/internal/api/request_validation_test.go index c5e241d..2d25d3a 100644 --- a/internal/api/request_validation_test.go +++ b/internal/api/request_validation_test.go @@ -10,37 +10,38 @@ func getRequestData() []byte { return []byte(`{"serverUrl":"https://example.com","status":"SUCCESS","analysedAt":"2022-05-15T16:45:31+0000","revision":"378080777919s07657a07f7a3e2d05dc75f64edd","changedAt":"2022-05-15T16:41:39+0000","project":{"key":"gitea-sonarqube-bot","name":"Gitea SonarQube Bot","url":"https://example.com/dashboard?id=gitea-sonarqube-bot"},"branch":{"name":"PR-1822","type":"PULL_REQUEST","isMain":false,"url":"https://example.com/dashboard?id=gitea-sonarqube-bot&pullRequest=PR-1822"},"qualityGate":{"name":"GiteaSonarQubeBot","status":"OK","conditions":[{"metric":"new_reliability_rating","operator":"GREATER_THAN","value":"1","status":"OK","errorThreshold":"1"},{"metric":"new_security_rating","operator":"GREATER_THAN","value":"1","status":"OK","errorThreshold":"1"},{"metric":"new_maintainability_rating","operator":"GREATER_THAN","value":"1","status":"OK","errorThreshold":"1"},{"metric":"new_security_hotspots_reviewed","operator":"LESS_THAN","status":"OK","errorThreshold":"100"}]},"properties":{"sonar.analysis.sqbot":"378080777919s07657a07f7a3e2d05dc75f64edd"}}`) } -func TestIsValidWebhookSuccess(t *testing.T) { - actual, _ := isValidWebhook(getRequestData(), "sonarqube-test-webhook-secret", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467", "test-component") - assert.True(t, actual, "Expected successful webhook signature validation") -} +func TestIsValidWebhook(t *testing.T) { + t.Run("Success", func(t *testing.T) { + actual, _ := isValidWebhook(getRequestData(), "sonarqube-test-webhook-secret", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467", "test-component") + assert.True(t, actual, "Expected successful webhook signature validation") + }) -func TestIsValidWebhookNothingConfiguredOrProvidedSuccess(t *testing.T) { - actual, _ := isValidWebhook(getRequestData(), "", "", "test-component") - assert.True(t, actual, "Webhook signature validation not skipped") -} + t.Run("Nothing configured or provided", func(t *testing.T) { + actual, _ := isValidWebhook(getRequestData(), "", "", "test-component") + assert.True(t, actual, "Webhook signature validation not skipped") + }) -func TestIsValidWebhookSignatureDecodingFailure(t *testing.T) { - actual, err := isValidWebhook(getRequestData(), "sonarqube-test-webhook-secret", "invalid-signature", "test-component") - assert.False(t, actual) - assert.EqualError(t, err, "Error decoding signature for test-component webhook.", "Undetected signature encoding error") -} + t.Run("Signature decoding error", func(t *testing.T) { + actual, err := isValidWebhook(getRequestData(), "sonarqube-test-webhook-secret", "invalid-signature", "test-component") + assert.False(t, actual) + assert.EqualError(t, err, "Error decoding signature for test-component webhook.", "Undetected signature encoding error") + }) -func TestIsValidWebhookSignatureMismatchFailure(t *testing.T) { - actual, err := isValidWebhook(getRequestData(), "sonarqube-test-webhook-secret", "fde6a666b7a1a46c27efb1961c17b46b6cf7aa13db5560e5ac95e801a18a92f3", "test-component") - assert.False(t, actual) - assert.EqualError(t, err, "Signature header does not match the received test-component webhook content. Request rejected.", "Undetected signature mismatch") - // assert.EqualError(t, err, "Signature header received but no test-component webhook secret configured. Request rejected due to possible configuration mismatch.", "Undetected configuration mismatch (1)") -} + t.Run("Signature mismatch", func(t *testing.T) { + actual, err := isValidWebhook(getRequestData(), "sonarqube-test-webhook-secret", "fde6a666b7a1a46c27efb1961c17b46b6cf7aa13db5560e5ac95e801a18a92f3", "test-component") + assert.False(t, actual) + assert.EqualError(t, err, "Signature header does not match the received test-component webhook content. Request rejected.", "Undetected signature mismatch") + }) -func TestIsValidWebhookEmptySecretConfigurationFailure(t *testing.T) { - actual, err := isValidWebhook(getRequestData(), "", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467", "test-component") - assert.False(t, actual) - assert.EqualError(t, err, "Signature header received but no test-component webhook secret configured. Request rejected due to possible configuration mismatch.", "Undetected configuration mismatch (1)") -} + t.Run("Empty secret configuration", func(t *testing.T) { + actual, err := isValidWebhook(getRequestData(), "", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467", "test-component") + assert.False(t, actual) + assert.EqualError(t, err, "Signature header received but no test-component webhook secret configured. Request rejected due to possible configuration mismatch.", "Undetected configuration mismatch (1)") + }) -func TestIsValidWebhookEmptySignatureConfigurationFailure(t *testing.T) { - actual, err := isValidWebhook(getRequestData(), "sonarqube-test-webhook-secret", "", "test-component") - assert.False(t, actual) - assert.EqualError(t, err, "test-component webhook secret configured but no signature header received. Request rejected due to possible configuration mismatch.", "Undetected configuration mismatch (2)") + t.Run("Empty signature configuration", func(t *testing.T) { + actual, err := isValidWebhook(getRequestData(), "sonarqube-test-webhook-secret", "", "test-component") + assert.False(t, actual) + assert.EqualError(t, err, "test-component webhook secret configured but no signature header received. Request rejected due to possible configuration mismatch.", "Undetected configuration mismatch (2)") + }) } diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index 8f71c6b..e79e517 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -34,137 +34,139 @@ func withValidSonarQubeRequestData(t *testing.T, jsonBody []byte) (*http.Request return req, rr, handler } -func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { - settings.Pattern = &settings.PatternConfig{ - RegExp: regexp.MustCompile(`^PR-(\d+)$`), - } - settings.SonarQube = settings.SonarQubeConfig{ - Webhook: &settings.Webhook{ - Secret: "", - }, - } - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "pr-bot", +func TestHandleSonarQubeWebhook(t *testing.T) { + t.Run("With mapped Project", func(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } + settings.SonarQube = settings.SonarQubeConfig{ + Webhook: &settings.Webhook{ + Secret: "", }, - }, - } - req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) - handler.ServeHTTP(rr, req) + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "pr-bot", + }, + }, + } + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) - t.Cleanup(func() { - settings.Pattern = nil - }) -} - -func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "another-project", - }, - }, - } - req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, `{"message": "Project 'pr-bot' not in configured list. Request ignored."}`, rr.Body.String()) -} - -func TestHandleSonarQubeWebhookInvalidJSONBody(t *testing.T) { - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "pr-bot", - }, - }, - } - - req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": ["invalid-server-url-content"] }`)) - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) - assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) -} - -func TestHandleSonarQubeWebhookInvalidWebhookSignature(t *testing.T) { - settings.SonarQube = settings.SonarQubeConfig{ - Webhook: &settings.Webhook{ - Secret: "sonarqube-test-webhook-secret", - }, - } - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "pr-bot", - }, - }, - } - req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) - req.Header.Set("X-Sonar-Webhook-HMAC-SHA256", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467") - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusPreconditionFailed, rr.Code) - assert.Equal(t, `{"message": "Webhook validation failed. Request rejected."}`, rr.Body.String()) -} - -func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { - settings.Pattern = &settings.PatternConfig{ - RegExp: regexp.MustCompile(`^PR-(\d+)$`), - } - settings.SonarQube = settings.SonarQubeConfig{ - Webhook: &settings.Webhook{ - Secret: "", - }, - } - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "pr-bot", - }, - }, - } - - req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) - - t.Cleanup(func() { - settings.Pattern = nil - }) -} - -func TestHandleSonarQubeWebhookForBranch(t *testing.T) { - settings.Pattern = &settings.PatternConfig{ - RegExp: regexp.MustCompile(`^PR-(\d+)$`), - } - settings.SonarQube = settings.SonarQubeConfig{ - Webhook: &settings.Webhook{ - Secret: "", - }, - } - settings.Projects = []settings.Project{ - { - SonarQube: struct{ Key string }{ - Key: "pr-bot", - }, - }, - } - - req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "BRANCH", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) - handler.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, `{"message": "Ignore Hook for non-PR analysis."}`, rr.Body.String()) - - t.Cleanup(func() { - settings.Pattern = nil + t.Cleanup(func() { + settings.Pattern = nil + }) + }) + + t.Run("Without mapped project", func(t *testing.T) { + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "another-project", + }, + }, + } + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "Project 'pr-bot' not in configured list. Request ignored."}`, rr.Body.String()) + }) + + t.Run("With invalid JSON body", func(t *testing.T) { + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "pr-bot", + }, + }, + } + + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": ["invalid-server-url-content"] }`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) + assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) + }) + + t.Run("With invalid webhook signature", func(t *testing.T) { + settings.SonarQube = settings.SonarQubeConfig{ + Webhook: &settings.Webhook{ + Secret: "sonarqube-test-webhook-secret", + }, + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "pr-bot", + }, + }, + } + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + req.Header.Set("X-Sonar-Webhook-HMAC-SHA256", "647f2395d30b1b7efcb58d9338be5b69c2addb54faf6bde6314a57ea28f45467") + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusPreconditionFailed, rr.Code) + assert.Equal(t, `{"message": "Webhook validation failed. Request rejected."}`, rr.Body.String()) + }) + + t.Run("Running for Pull Request", func(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } + settings.SonarQube = settings.SonarQubeConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "pr-bot", + }, + }, + } + + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) + }) + + t.Run("Running for branch", func(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } + settings.SonarQube = settings.SonarQubeConfig{ + Webhook: &settings.Webhook{ + Secret: "", + }, + } + settings.Projects = []settings.Project{ + { + SonarQube: struct{ Key string }{ + Key: "pr-bot", + }, + }, + } + + req, rr, handler := withValidSonarQubeRequestData(t, []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "BRANCH", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`)) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, `{"message": "Ignore Hook for non-PR analysis."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) }) } diff --git a/internal/clients/sonarqube/sonarqube_test.go b/internal/clients/sonarqube/sonarqube_test.go index 928fce1..28a0616 100644 --- a/internal/clients/sonarqube/sonarqube_test.go +++ b/internal/clients/sonarqube/sonarqube_test.go @@ -28,29 +28,31 @@ func (c *ClientMock) Do(req *http.Request) (*http.Response, error) { }, c.responseError } -func TestParsePRIndexSuccess(t *testing.T) { - settings.Pattern = &settings.PatternConfig{ - RegExp: regexp.MustCompile(`^PR-(\d+)$`), - } +func TestParsePRIndex(t *testing.T) { + t.Run("Success", func(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } - actual, _ := ParsePRIndex("PR-1337") - assert.Equal(t, 1337, actual, "PR index parsing is broken") + actual, _ := ParsePRIndex("PR-1337") + assert.Equal(t, 1337, actual, "PR index parsing is broken") - t.Cleanup(func() { - settings.Pattern = nil + t.Cleanup(func() { + settings.Pattern = nil + }) }) -} -func TestParsePRIndexNonIntegerFailure(t *testing.T) { - settings.Pattern = &settings.PatternConfig{ - RegExp: regexp.MustCompile(`^PR-(\d+)$`), - } + t.Run("No integer value", func(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } - _, err := ParsePRIndex("PR-invalid") - assert.EqualErrorf(t, err, "branch name 'PR-invalid' does not match regex '^PR-(\\d+)$'", "Integer parsing succeeds unexpectedly") + _, err := ParsePRIndex("PR-invalid") + assert.EqualErrorf(t, err, "branch name 'PR-invalid' does not match regex '^PR-(\\d+)$'", "Integer parsing succeeds unexpectedly") - t.Cleanup(func() { - settings.Pattern = nil + t.Cleanup(func() { + settings.Pattern = nil + }) }) } @@ -66,16 +68,14 @@ func TestPRNameFromIndex(t *testing.T) { }) } -func TestGetRenderedQualityGateSuccess(t *testing.T) { - actual := GetRenderedQualityGate("OK") +func TestGetRenderedQualityGate(t *testing.T) { + t.Run("Passed", func(t *testing.T) { + assert.Contains(t, GetRenderedQualityGate("OK"), ":white_check_mark:", "Undetected successful quality gate during status rendering") + }) - assert.Contains(t, actual, ":white_check_mark:", "Undetected successful quality gate during status rendering") -} - -func TestGetRenderedQualityGateFailure(t *testing.T) { - actual := GetRenderedQualityGate("ERROR") - - assert.Contains(t, actual, ":x:", "Undetected failed quality gate during status rendering") + t.Run("Failed", func(t *testing.T) { + assert.Contains(t, GetRenderedQualityGate("ERROR"), ":x:", "Undetected failed quality gate during status rendering") + }) } func TestGetPullRequestUrl(t *testing.T) { @@ -94,435 +94,445 @@ func TestGetPullRequestUrl(t *testing.T) { }) } -func TestRetrieveDataFromApiSuccess(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) +func TestRetrieveDataFromApi(t *testing.T) { + t.Run("Success", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + wrapper := &PullsResponse{} + err := retrieveDataFromApi(sdk, request, wrapper) + + assert.Nil(t, err, "Successful data retrieval broken and throws error") + assert.Equal(t, "Basic dGVzdC10b2tlbjo=", request.Header.Get("Authorization"), "Authorization header not set") + assert.Equal(t, "PR-1", wrapper.PullRequests[0].Key, "Unmarshallowing into wrapper broken") }) - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: io.ReadAll, - } - request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) - wrapper := &PullsResponse{} - err := retrieveDataFromApi(sdk, request, wrapper) + t.Run("Internal error", func(t *testing.T) { + expected := fmt.Errorf("This error indicates an error while performing the request") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}), + recoder: httptest.NewRecorder(), + responseError: expected, + }, + bodyReader: io.ReadAll, + } - assert.Nil(t, err, "Successful data retrieval broken and throws error") - assert.Equal(t, "Basic dGVzdC10b2tlbjo=", request.Header.Get("Authorization"), "Authorization header not set") - assert.Equal(t, "PR-1", wrapper.PullRequests[0].Key, "Unmarshallowing into wrapper broken") -} + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + err := retrieveDataFromApi(sdk, request, &PullsResponse{}) -func TestRetrieveDataFromApiRequestError(t *testing.T) { - expected := fmt.Errorf("This error indicates an error while performing the request") - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}), - recoder: httptest.NewRecorder(), - responseError: expected, - }, - bodyReader: io.ReadAll, - } - - request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) - err := retrieveDataFromApi(sdk, request, &PullsResponse{}) - - assert.ErrorIs(t, err, expected, "Undetected request performing error") -} - -func TestRetrieveDataFromApiUnauthorized(t *testing.T) { - recorder := httptest.NewRecorder() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - recorder.Code = http.StatusUnauthorized + assert.ErrorIs(t, err, expected, "Undetected request performing error") }) - sdk := &SonarQubeSdk{ - token: "simulated-invalid-token", - client: &ClientMock{ - handler: handler, - recoder: recorder, - responseError: nil, - }, - bodyReader: io.ReadAll, - } - request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) - err := retrieveDataFromApi(sdk, request, &PullsResponse{}) + t.Run("Unauthorized", func(t *testing.T) { + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + recorder.Code = http.StatusUnauthorized + }) + sdk := &SonarQubeSdk{ + token: "simulated-invalid-token", + client: &ClientMock{ + handler: handler, + recoder: recorder, + responseError: nil, + }, + bodyReader: io.ReadAll, + } - assert.Errorf(t, err, "missing or invalid API token", "Undetected unauthorized error") -} + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + err := retrieveDataFromApi(sdk, request, &PullsResponse{}) -func TestRetrieveDataFromApiBodyReadError(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + assert.Errorf(t, err, "missing or invalid API token", "Undetected unauthorized error") }) - expected := fmt.Errorf("Error reading body content") - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: func(r io.Reader) ([]byte, error) { - return []byte(``), expected - }, - } - request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) - err := retrieveDataFromApi(sdk, request, &PullsResponse{}) + t.Run("Body read error", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + expected := fmt.Errorf("Error reading body content") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: func(r io.Reader) ([]byte, error) { + return []byte(``), expected + }, + } - assert.ErrorIs(t, err, expected, "Undetected body processing error") -} + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + err := retrieveDataFromApi(sdk, request, &PullsResponse{}) -func TestRetrieveDataFromApiBodyUnmarshalError(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"pullReq`)) + assert.ErrorIs(t, err, expected, "Undetected body processing error") }) - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: io.ReadAll, - } - request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) - err := retrieveDataFromApi(sdk, request, &PullsResponse{}) + t.Run("Unmarshal error", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullReq`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + } - assert.Errorf(t, err, "unexpected end of JSON input", "Undetected body unmarshal error") -} + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + err := retrieveDataFromApi(sdk, request, &PullsResponse{}) -func TestFetchPullRequestsSuccess(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) - }) - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return httptest.NewRequest(method, target, body), nil - }, - } - - actual, err := sdk.fetchPullRequests("test-project") - - assert.Nil(t, err, "Successful data retrieval broken and throws error") - assert.IsType(t, &PullsResponse{}, actual, "Happy path broken") -} - -func TestFetchPullRequestsRequestBuildingFailure(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) - }) - expected := fmt.Errorf("Some simulated error") - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return nil, expected - }, - } - - _, err := sdk.fetchPullRequests("test-project") - - assert.Equal(t, expected, err, "Unexpected error instance returned") -} - -func TestFetchPullRequestsRequestError(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) - }) - expected := fmt.Errorf("Some simulated error") - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: expected, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return httptest.NewRequest(method, target, body), nil - }, - } - - _, err := sdk.fetchPullRequests("test-project") - - assert.Equal(t, expected, err) -} - -func TestFetchPullRequestsErrorsInResponse(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"errors":[{"msg":"Project 'test-project' not found"}]}`)) - }) - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return httptest.NewRequest(method, target, body), nil - }, - } - - _, err := sdk.fetchPullRequests("test-project") - - assert.Errorf(t, err, "Project 'test-project' not found", "Response error parsing broken") -} - -func TestGetPullRequestSuccess(t *testing.T) { - settings.Pattern = &settings.PatternConfig{ - Template: "PR-%d", - } - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) - }) - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return httptest.NewRequest(method, target, body), nil - }, - } - - actual, err := sdk.GetPullRequest("test-project", 1) - - assert.Nil(t, err, "Successful data retrieval broken and throws error") - assert.IsType(t, &PullRequest{}, actual, "Happy path broken") - - t.Cleanup(func() { - settings.Pattern = nil + assert.Errorf(t, err, "unexpected end of JSON input", "Undetected body unmarshal error") }) } -func TestGetPullRequestFetchError(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) +func TestFetchPullRequests(t *testing.T) { + t.Run("Success", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + actual, err := sdk.fetchPullRequests("test-project") + + assert.Nil(t, err, "Successful data retrieval broken and throws error") + assert.IsType(t, &PullsResponse{}, actual, "Happy path broken") }) - expected := fmt.Errorf("Some simulated error") - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: expected, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return httptest.NewRequest(method, target, body), nil - }, - } - _, err := sdk.GetPullRequest("test-project", 1) + t.Run("Building failure", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + expected := fmt.Errorf("Some simulated error") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return nil, expected + }, + } - assert.Errorf(t, err, "fetching pull requests failed", "Incorrect edge case is throwing errors") - assert.Errorf(t, err, "Some simulated error", "Unexpected error cause") -} + _, err := sdk.fetchPullRequests("test-project") -func TestGetPullRequestUnknownPR(t *testing.T) { - settings.Pattern = &settings.PatternConfig{ - Template: "PR-%d", - } - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + assert.Equal(t, expected, err, "Unexpected error instance returned") }) - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return httptest.NewRequest(method, target, body), nil - }, - } - _, err := sdk.GetPullRequest("test-project", 1337) + t.Run("Internal error", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + expected := fmt.Errorf("Some simulated error") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: expected, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } - assert.Errorf(t, err, "no pull request found with name 'PR-1337'") + _, err := sdk.fetchPullRequests("test-project") - t.Cleanup(func() { - settings.Pattern = nil + assert.Equal(t, expected, err) + }) + + t.Run("Errors in response", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"errors":[{"msg":"Project 'test-project' not found"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + _, err := sdk.fetchPullRequests("test-project") + + assert.Errorf(t, err, "Project 'test-project' not found", "Response error parsing broken") }) } -func TestGetMeasuresSuccess(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"0","bestValue":true}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) +func TestGetPullRequest(t *testing.T) { + t.Run("Success", func(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + actual, err := sdk.GetPullRequest("test-project", 1) + + assert.Nil(t, err, "Successful data retrieval broken and throws error") + assert.IsType(t, &PullRequest{}, actual, "Happy path broken") + + t.Cleanup(func() { + settings.Pattern = nil + }) }) - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return httptest.NewRequest(method, target, body), nil - }, - } - actual, err := sdk.GetMeasures("test-project", "PR-1") + t.Run("Fetch error", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + expected := fmt.Errorf("Some simulated error") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: expected, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } - assert.Nil(t, err, "Successful data retrieval broken and throws error") - assert.IsType(t, &MeasuresResponse{}, actual, "Happy path broken") + _, err := sdk.GetPullRequest("test-project", 1) + + assert.Errorf(t, err, "fetching pull requests failed", "Incorrect edge case is throwing errors") + assert.Errorf(t, err, "Some simulated error", "Unexpected error cause") + }) + + t.Run("Unknown PR", func(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + _, err := sdk.GetPullRequest("test-project", 1337) + + assert.Errorf(t, err, "no pull request found with name 'PR-1337'") + + t.Cleanup(func() { + settings.Pattern = nil + }) + }) } -func TestGetMeasuresRequestBuildingFailure(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"0","bestValue":true}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) +func TestGetMeasures(t *testing.T) { + t.Run("Success", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"0","bestValue":true}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + actual, err := sdk.GetMeasures("test-project", "PR-1") + + assert.Nil(t, err, "Successful data retrieval broken and throws error") + assert.IsType(t, &MeasuresResponse{}, actual, "Happy path broken") }) - expected := fmt.Errorf("Some simulated error") - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return nil, expected - }, - } - _, err := sdk.GetMeasures("test-project", "PR-1") + t.Run("Building failure", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"0","bestValue":true}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) + }) + expected := fmt.Errorf("Some simulated error") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return nil, expected + }, + } - assert.Equal(t, expected, err, "Unexpected error instance returned") + _, err := sdk.GetMeasures("test-project", "PR-1") + + assert.Equal(t, expected, err, "Unexpected error instance returned") + }) + + t.Run("Request error", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"0","bestValue":true}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) + }) + expected := fmt.Errorf("Some simulated error") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: expected, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + _, err := sdk.GetMeasures("test-project", "PR-1") + + assert.Equal(t, expected, err) + }) + + t.Run("Errors in response", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"errors":[{"msg":"Component 'non-existing-project' of pull request 'PR-1' not found"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } + + _, err := sdk.GetMeasures("non-existing-project", "PR-1") + + assert.Errorf(t, err, "Component 'non-existing-project' of pull request 'PR-1' not found", "Response error parsing broken") + }) } -func TestGetMeasuresRequestError(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"0","bestValue":true}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) - }) - expected := fmt.Errorf("Some simulated error") - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: expected, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return httptest.NewRequest(method, target, body), nil - }, - } +func TestComposeGiteaComment(t *testing.T) { + t.Run("Success", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"10","bestValue":false}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) + }) + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return httptest.NewRequest(method, target, body), nil + }, + } - _, err := sdk.GetMeasures("test-project", "PR-1") + actual, err := sdk.ComposeGiteaComment(&CommentComposeData{ + Key: "test-project", + PRName: "PR-1", + Url: "https://sonarqube.example.com", + QualityGate: "OK", + }) - assert.Equal(t, expected, err) -} - -func TestGetMeasuresErrorsInResponse(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"errors":[{"msg":"Component 'non-existing-project' of pull request 'PR-1' not found"}]}`)) - }) - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return httptest.NewRequest(method, target, body), nil - }, - } - - _, err := sdk.GetMeasures("non-existing-project", "PR-1") - - assert.Errorf(t, err, "Component 'non-existing-project' of pull request 'PR-1' not found", "Response error parsing broken") -} - -func TestComposeGiteaCommentSuccess(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"10","bestValue":false}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) - }) - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return httptest.NewRequest(method, target, body), nil - }, - } - - actual, err := sdk.ComposeGiteaComment(&CommentComposeData{ - Key: "test-project", - PRName: "PR-1", - Url: "https://sonarqube.example.com", - QualityGate: "OK", + assert.Nil(t, err, "Successful comment composing throwing errors") + assert.Contains(t, actual, ":white_check_mark:", "Happy path [Quality Gate] broken") + assert.Contains(t, actual, "| Metric | Current |", "Happy path [Metrics Header] broken") + assert.Contains(t, actual, "| Bugs | 10 |", "Happy path [Metrics Values] broken") + assert.Contains(t, actual, "https://sonarqube.example.com", "Happy path [Link] broken") + assert.Contains(t, actual, "/sq-bot review", "Happy path [Command] broken") }) - assert.Nil(t, err, "Successful comment composing throwing errors") - assert.Contains(t, actual, ":white_check_mark:", "Happy path [Quality Gate] broken") - assert.Contains(t, actual, "| Metric | Current |", "Happy path [Metrics Header] broken") - assert.Contains(t, actual, "| Bugs | 10 |", "Happy path [Metrics Values] broken") - assert.Contains(t, actual, "https://sonarqube.example.com", "Happy path [Link] broken") - assert.Contains(t, actual, "/sq-bot review", "Happy path [Command] broken") -} + t.Run("Error", func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"10","bestValue":false}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) + }) + expected := fmt.Errorf("Expected error from GetMeasures") + sdk := &SonarQubeSdk{ + token: "test-token", + client: &ClientMock{ + handler: handler, + recoder: httptest.NewRecorder(), + responseError: nil, + }, + bodyReader: io.ReadAll, + httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { + return nil, expected + }, + } -func TestComposeGiteaCommentError(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"10","bestValue":false}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) + _, err := sdk.ComposeGiteaComment(&CommentComposeData{ + Key: "test-project", + PRName: "PR-1", + Url: "https://sonarqube.example.com", + QualityGate: "OK", + }) + + assert.Errorf(t, err, expected.Error(), "Undetected error while composing comment") }) - expected := fmt.Errorf("Expected error from GetMeasures") - sdk := &SonarQubeSdk{ - token: "test-token", - client: &ClientMock{ - handler: handler, - recoder: httptest.NewRecorder(), - responseError: nil, - }, - bodyReader: io.ReadAll, - httpRequest: func(method, target string, body io.Reader) (*http.Request, error) { - return nil, expected - }, - } - - _, err := sdk.ComposeGiteaComment(&CommentComposeData{ - Key: "test-project", - PRName: "PR-1", - Url: "https://sonarqube.example.com", - QualityGate: "OK", - }) - - assert.Errorf(t, err, expected.Error(), "Undetected error while composing comment") } func TestNew(t *testing.T) { diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 9513e2e..187731d 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -10,8 +10,9 @@ import ( "github.com/stretchr/testify/assert" ) -var defaultConfig []byte = []byte( - `gitea: +func defaultConfig() []byte { + return []byte( + `gitea: url: https://example.com/gitea token: value: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 @@ -34,6 +35,7 @@ namingPattern: regex: "^PR-(\\d+)$" template: "PR-%d" `) +} func WriteConfigFile(t *testing.T, content []byte) string { dir := os.TempDir() @@ -48,78 +50,157 @@ func WriteConfigFile(t *testing.T, content []byte) string { return config } -func TestLoadWithMissingFile(t *testing.T) { - assert.Panics(t, func() { Load(path.Join(os.TempDir(), "config.yaml")) }, "No panic while reading missing file") -} +func TestLoad(t *testing.T) { + t.Run("Missing file", func(t *testing.T) { + assert.Panics(t, func() { Load(path.Join(os.TempDir(), "config.yaml")) }, "No panic while reading missing file") + }) -func TestLoadWithExistingFile(t *testing.T) { - c := WriteConfigFile(t, defaultConfig) + t.Run("Existing file", func(t *testing.T) { + c := WriteConfigFile(t, defaultConfig()) + assert.NotPanics(t, func() { Load(c) }, "Unexpected panic while reading existing file") + }) - assert.NotPanics(t, func() { Load(c) }, "Unexpected panic while reading existing file") -} + t.Run("File references", func(t *testing.T) { + giteaWebhookSecretFile := path.Join(os.TempDir(), "webhook-secret-gitea") + _ = ioutil.WriteFile(giteaWebhookSecretFile, []byte(`gitea-totally-secret`), 0444) -func TestLoadGiteaStructure(t *testing.T) { - c := WriteConfigFile(t, defaultConfig) - Load(c) + giteaTokenFile := path.Join(os.TempDir(), "token-secret-gitea") + _ = ioutil.WriteFile(giteaTokenFile, []byte(`d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565`), 0444) - expected := GiteaConfig{ - Url: "https://example.com/gitea", - Token: &Token{ - Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", - }, - Webhook: &Webhook{ - Secret: "haxxor-gitea-secret", - }, - } + sonarqubeWebhookSecretFile := path.Join(os.TempDir(), "webhook-secret-sonarqube") + _ = ioutil.WriteFile(sonarqubeWebhookSecretFile, []byte(`sonarqube-totally-secret`), 0444) - assert.EqualValues(t, expected, Gitea) -} + sonarqubeTokenFile := path.Join(os.TempDir(), "token-secret-sonarqube") + _ = ioutil.WriteFile(sonarqubeTokenFile, []byte(`a09eb5785b25bb2cbacf48808a677a0709f02d8e`), 0444) -func TestLoadGiteaStructureInjectedEnvs(t *testing.T) { - os.Setenv("PRBOT_GITEA_WEBHOOK_SECRET", "injected-webhook-secret") - os.Setenv("PRBOT_GITEA_TOKEN_VALUE", "injected-token") - c := WriteConfigFile(t, defaultConfig) - Load(c) + c := WriteConfigFile(t, []byte( + `gitea: + url: https://example.com/gitea + token: + value: fake-gitea-token +sonarqube: + url: https://example.com/sonarqube + token: + value: fake-sonarqube-token +projects: + - sonarqube: + key: gitea-sonarqube-bot + gitea: + owner: example-organization + name: pr-bot +`)) + os.Setenv("PRBOT_GITEA_WEBHOOK_SECRETFILE", giteaWebhookSecretFile) + os.Setenv("PRBOT_GITEA_TOKEN_FILE", giteaTokenFile) + os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRETFILE", sonarqubeWebhookSecretFile) + os.Setenv("PRBOT_SONARQUBE_TOKEN_FILE", sonarqubeTokenFile) - expected := GiteaConfig{ - Url: "https://example.com/gitea", - Token: &Token{ - Value: "injected-token", - }, - Webhook: &Webhook{ - Secret: "injected-webhook-secret", - }, - } + expectedGitea := GiteaConfig{ + Url: "https://example.com/gitea", + Token: &Token{ + Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", + file: giteaTokenFile, + }, + Webhook: &Webhook{ + Secret: "gitea-totally-secret", + secretFile: giteaWebhookSecretFile, + }, + } - assert.EqualValues(t, expected, Gitea) + expectedSonarQube := SonarQubeConfig{ + Url: "https://example.com/sonarqube", + Token: &Token{ + Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", + file: sonarqubeTokenFile, + }, + Webhook: &Webhook{ + Secret: "sonarqube-totally-secret", + secretFile: sonarqubeWebhookSecretFile, + }, + AdditionalMetrics: []string{}, + } - t.Cleanup(func() { - os.Unsetenv("PRBOT_GITEA_WEBHOOK_SECRET") - os.Unsetenv("PRBOT_GITEA_TOKEN_VALUE") + Load(c) + assert.EqualValues(t, expectedGitea, Gitea) + assert.EqualValues(t, expectedSonarQube, SonarQube) + + t.Cleanup(func() { + os.Remove(giteaWebhookSecretFile) + os.Remove(giteaTokenFile) + os.Remove(sonarqubeWebhookSecretFile) + os.Remove(sonarqubeTokenFile) + os.Unsetenv("PRBOT_GITEA_WEBHOOK_SECRETFILE") + os.Unsetenv("PRBOT_GITEA_TOKEN_FILE") + os.Unsetenv("PRBOT_SONARQUBE_WEBHOOK_SECRETFILE") + os.Unsetenv("PRBOT_SONARQUBE_TOKEN_FILE") + }) }) } -func TestLoadSonarQubeStructure(t *testing.T) { - c := WriteConfigFile(t, defaultConfig) - Load(c) +func TestLoadGitea(t *testing.T) { + t.Run("Default", func(t *testing.T) { + c := WriteConfigFile(t, defaultConfig()) + Load(c) - expected := SonarQubeConfig{ - Url: "https://example.com/sonarqube", - Token: &Token{ - Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", - }, - Webhook: &Webhook{ - Secret: "haxxor-sonarqube-secret", - }, - } + expected := GiteaConfig{ + Url: "https://example.com/gitea", + Token: &Token{ + Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", + }, + Webhook: &Webhook{ + Secret: "haxxor-gitea-secret", + }, + } - assert.EqualValues(t, expected, SonarQube) - assert.EqualValues(t, expected.GetMetricsList(), "bugs,vulnerabilities,code_smells") + assert.EqualValues(t, expected, Gitea) + }) + + t.Run("Injected envs", func(t *testing.T) { + os.Setenv("PRBOT_GITEA_WEBHOOK_SECRET", "injected-webhook-secret") + os.Setenv("PRBOT_GITEA_TOKEN_VALUE", "injected-token") + c := WriteConfigFile(t, defaultConfig()) + Load(c) + + expected := GiteaConfig{ + Url: "https://example.com/gitea", + Token: &Token{ + Value: "injected-token", + }, + Webhook: &Webhook{ + Secret: "injected-webhook-secret", + }, + } + + assert.EqualValues(t, expected, Gitea) + + t.Cleanup(func() { + os.Unsetenv("PRBOT_GITEA_WEBHOOK_SECRET") + os.Unsetenv("PRBOT_GITEA_TOKEN_VALUE") + }) + }) } -func TestLoadSonarQubeStructureWithAdditionalMetrics(t *testing.T) { - c := WriteConfigFile(t, []byte( - `gitea: +func TestLoadSonarQube(t *testing.T) { + t.Run("Default", func(t *testing.T) { + c := WriteConfigFile(t, defaultConfig()) + Load(c) + + expected := SonarQubeConfig{ + Url: "https://example.com/sonarqube", + Token: &Token{ + Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", + }, + Webhook: &Webhook{ + Secret: "haxxor-sonarqube-secret", + }, + } + + assert.EqualValues(t, expected, SonarQube) + assert.EqualValues(t, expected.GetMetricsList(), "bugs,vulnerabilities,code_smells") + }) + + t.Run("Additional metrics", func(t *testing.T) { + c := WriteConfigFile(t, []byte( + `gitea: url: https://example.com/gitea token: value: fake-gitea-token @@ -135,147 +216,74 @@ projects: owner: example-organization name: pr-bot `)) - Load(c) + Load(c) - expected := SonarQubeConfig{ - Url: "https://example.com/sonarqube", - Token: &Token{ - Value: "fake-sonarqube-token", - }, - Webhook: &Webhook{ - Secret: "", - }, - AdditionalMetrics: []string{ - "new_security_hotspots", - }, - } + expected := SonarQubeConfig{ + Url: "https://example.com/sonarqube", + Token: &Token{ + Value: "fake-sonarqube-token", + }, + Webhook: &Webhook{ + Secret: "", + }, + AdditionalMetrics: []string{ + "new_security_hotspots", + }, + } - assert.EqualValues(t, expected, SonarQube) - assert.EqualValues(t, expected.AdditionalMetrics, []string{"new_security_hotspots"}) - assert.EqualValues(t, "bugs,vulnerabilities,code_smells,new_security_hotspots", SonarQube.GetMetricsList()) -} + assert.EqualValues(t, expected, SonarQube) + assert.EqualValues(t, expected.AdditionalMetrics, []string{"new_security_hotspots"}) + assert.EqualValues(t, "bugs,vulnerabilities,code_smells,new_security_hotspots", SonarQube.GetMetricsList()) + }) -func TestLoadSonarQubeStructureInjectedEnvs(t *testing.T) { - os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRET", "injected-webhook-secret") - os.Setenv("PRBOT_SONARQUBE_TOKEN_VALUE", "injected-token") - c := WriteConfigFile(t, defaultConfig) - Load(c) + t.Run("Injected envs", func(t *testing.T) { + os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRET", "injected-webhook-secret") + os.Setenv("PRBOT_SONARQUBE_TOKEN_VALUE", "injected-token") + c := WriteConfigFile(t, defaultConfig()) + Load(c) - expected := SonarQubeConfig{ - Url: "https://example.com/sonarqube", - Token: &Token{ - Value: "injected-token", - }, - Webhook: &Webhook{ - Secret: "injected-webhook-secret", - }, - } + expected := SonarQubeConfig{ + Url: "https://example.com/sonarqube", + Token: &Token{ + Value: "injected-token", + }, + Webhook: &Webhook{ + Secret: "injected-webhook-secret", + }, + } - assert.EqualValues(t, expected, SonarQube) + assert.EqualValues(t, expected, SonarQube) - t.Cleanup(func() { - os.Unsetenv("PRBOT_SONARQUBE_WEBHOOK_SECRET") - os.Unsetenv("PRBOT_SONARQUBE_TOKEN_VALUE") + t.Cleanup(func() { + os.Unsetenv("PRBOT_SONARQUBE_WEBHOOK_SECRET") + os.Unsetenv("PRBOT_SONARQUBE_TOKEN_VALUE") + }) }) } -func TestLoadStructureWithFileReferenceResolving(t *testing.T) { - giteaWebhookSecretFile := path.Join(os.TempDir(), "webhook-secret-gitea") - _ = ioutil.WriteFile(giteaWebhookSecretFile, []byte(`gitea-totally-secret`), 0444) +func TestLoadProjects(t *testing.T) { + t.Run("Default", func(t *testing.T) { + c := WriteConfigFile(t, defaultConfig()) + Load(c) - giteaTokenFile := path.Join(os.TempDir(), "token-secret-gitea") - _ = ioutil.WriteFile(giteaTokenFile, []byte(`d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565`), 0444) + expectedProjects := []Project{ + { + SonarQube: struct{ Key string }{ + Key: "gitea-sonarqube-bot", + }, + Gitea: GiteaRepository{ + Owner: "example-organization", + Name: "pr-bot", + }, + }, + } - sonarqubeWebhookSecretFile := path.Join(os.TempDir(), "webhook-secret-sonarqube") - _ = ioutil.WriteFile(sonarqubeWebhookSecretFile, []byte(`sonarqube-totally-secret`), 0444) - - sonarqubeTokenFile := path.Join(os.TempDir(), "token-secret-sonarqube") - _ = ioutil.WriteFile(sonarqubeTokenFile, []byte(`a09eb5785b25bb2cbacf48808a677a0709f02d8e`), 0444) - - c := WriteConfigFile(t, []byte( - `gitea: - url: https://example.com/gitea - token: - value: fake-gitea-token -sonarqube: - url: https://example.com/sonarqube - token: - value: fake-sonarqube-token -projects: - - sonarqube: - key: gitea-sonarqube-bot - gitea: - owner: example-organization - name: pr-bot -`)) - os.Setenv("PRBOT_GITEA_WEBHOOK_SECRETFILE", giteaWebhookSecretFile) - os.Setenv("PRBOT_GITEA_TOKEN_FILE", giteaTokenFile) - os.Setenv("PRBOT_SONARQUBE_WEBHOOK_SECRETFILE", sonarqubeWebhookSecretFile) - os.Setenv("PRBOT_SONARQUBE_TOKEN_FILE", sonarqubeTokenFile) - - expectedGitea := GiteaConfig{ - Url: "https://example.com/gitea", - Token: &Token{ - Value: "d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565", - file: giteaTokenFile, - }, - Webhook: &Webhook{ - Secret: "gitea-totally-secret", - secretFile: giteaWebhookSecretFile, - }, - } - - expectedSonarQube := SonarQubeConfig{ - Url: "https://example.com/sonarqube", - Token: &Token{ - Value: "a09eb5785b25bb2cbacf48808a677a0709f02d8e", - file: sonarqubeTokenFile, - }, - Webhook: &Webhook{ - Secret: "sonarqube-totally-secret", - secretFile: sonarqubeWebhookSecretFile, - }, - AdditionalMetrics: []string{}, - } - - Load(c) - assert.EqualValues(t, expectedGitea, Gitea) - assert.EqualValues(t, expectedSonarQube, SonarQube) - - t.Cleanup(func() { - os.Remove(giteaWebhookSecretFile) - os.Remove(giteaTokenFile) - os.Remove(sonarqubeWebhookSecretFile) - os.Remove(sonarqubeTokenFile) - os.Unsetenv("PRBOT_GITEA_WEBHOOK_SECRETFILE") - os.Unsetenv("PRBOT_GITEA_TOKEN_FILE") - os.Unsetenv("PRBOT_SONARQUBE_WEBHOOK_SECRETFILE") - os.Unsetenv("PRBOT_SONARQUBE_TOKEN_FILE") + assert.EqualValues(t, expectedProjects, Projects) }) -} -func TestLoadProjectsStructure(t *testing.T) { - c := WriteConfigFile(t, defaultConfig) - Load(c) - - expectedProjects := []Project{ - { - SonarQube: struct{ Key string }{ - Key: "gitea-sonarqube-bot", - }, - Gitea: GiteaRepository{ - Owner: "example-organization", - Name: "pr-bot", - }, - }, - } - - assert.EqualValues(t, expectedProjects, Projects) -} - -func TestLoadProjectsStructureWithNoMapping(t *testing.T) { - invalidConfig := []byte( - `gitea: + t.Run("Empty mapping", func(t *testing.T) { + invalidConfig := []byte( + `gitea: url: https://example.com/gitea token: value: d0fcdeb5eaa99c506831f9eb4e63fc7cc484a565 @@ -289,26 +297,28 @@ sonarqube: secret: haxxor-sonarqube-secret projects: [] `) - c := WriteConfigFile(t, invalidConfig) + c := WriteConfigFile(t, invalidConfig) - assert.Panics(t, func() { Load(c) }, "No panic for empty project mapping that is required") + assert.Panics(t, func() { Load(c) }, "No panic for empty project mapping that is required") + }) } -func TestLoadNamingPatternStructure(t *testing.T) { - c := WriteConfigFile(t, defaultConfig) - Load(c) +func TestLoadNamingPattern(t *testing.T) { + t.Run("Default", func(t *testing.T) { + c := WriteConfigFile(t, defaultConfig()) + Load(c) - expected := &PatternConfig{ - RegExp: regexp.MustCompile(`^PR-(\d+)$`), - Template: "PR-%d", - } + expected := &PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + Template: "PR-%d", + } - assert.EqualValues(t, expected, Pattern) -} + assert.EqualValues(t, expected, Pattern) + }) -func TestLoadNamingPatternStructureWithInternalDefaults(t *testing.T) { - c := WriteConfigFile(t, []byte( - `gitea: + t.Run("Internal defaults", func(t *testing.T) { + c := WriteConfigFile(t, []byte( + `gitea: url: https://example.com/gitea token: value: fake-gitea-token @@ -324,48 +334,49 @@ projects: owner: example-organization name: pr-bot `)) - Load(c) + Load(c) - expected := &PatternConfig{ - RegExp: regexp.MustCompile(`^PR-(\d+)$`), - Template: "PR-%d", - } + expected := &PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + Template: "PR-%d", + } - assert.EqualValues(t, expected, Pattern) -} + assert.EqualValues(t, expected, Pattern) + }) -func TestLoadNamingPatternStructureInjectedEnvs(t *testing.T) { - os.Setenv("PRBOT_NAMINGPATTERN_REGEX", "test-(\\d+)-pullrequest") - os.Setenv("PRBOT_NAMINGPATTERN_TEMPLATE", "test-%d-pullrequest") - c := WriteConfigFile(t, defaultConfig) - Load(c) + t.Run("Injected envs", func(t *testing.T) { + os.Setenv("PRBOT_NAMINGPATTERN_REGEX", "test-(\\d+)-pullrequest") + os.Setenv("PRBOT_NAMINGPATTERN_TEMPLATE", "test-%d-pullrequest") + c := WriteConfigFile(t, defaultConfig()) + Load(c) - expected := &PatternConfig{ - RegExp: regexp.MustCompile(`test-(\d+)-pullrequest`), - Template: "test-%d-pullrequest", - } + expected := &PatternConfig{ + RegExp: regexp.MustCompile(`test-(\d+)-pullrequest`), + Template: "test-%d-pullrequest", + } - assert.EqualValues(t, expected, Pattern) + assert.EqualValues(t, expected, Pattern) - t.Cleanup(func() { - os.Unsetenv("PRBOT_NAMINGPATTERN_REGEX") - os.Unsetenv("PRBOT_NAMINGPATTERN_TEMPLATE") - }) -} - -func TestLoadNamingPatternStructureMixedInput(t *testing.T) { - os.Setenv("PRBOT_NAMINGPATTERN_REGEX", "test-(\\d+)-pullrequest") - c := WriteConfigFile(t, defaultConfig) - Load(c) - - expected := &PatternConfig{ - RegExp: regexp.MustCompile(`test-(\d+)-pullrequest`), - Template: "PR-%d", - } - - assert.EqualValues(t, expected, Pattern) - - t.Cleanup(func() { - os.Unsetenv("PRBOT_NAMINGPATTERN_REGEX") + t.Cleanup(func() { + os.Unsetenv("PRBOT_NAMINGPATTERN_REGEX") + os.Unsetenv("PRBOT_NAMINGPATTERN_TEMPLATE") + }) + }) + + t.Run("Mixed input", func(t *testing.T) { + os.Setenv("PRBOT_NAMINGPATTERN_REGEX", "test-(\\d+)-pullrequest") + c := WriteConfigFile(t, defaultConfig()) + Load(c) + + expected := &PatternConfig{ + RegExp: regexp.MustCompile(`test-(\d+)-pullrequest`), + Template: "PR-%d", + } + + assert.EqualValues(t, expected, Pattern) + + t.Cleanup(func() { + os.Unsetenv("PRBOT_NAMINGPATTERN_REGEX") + }) }) } diff --git a/internal/webhooks/sonarqube/webhook_test.go b/internal/webhooks/sonarqube/webhook_test.go index 58c555d..ae84321 100644 --- a/internal/webhooks/sonarqube/webhook_test.go +++ b/internal/webhooks/sonarqube/webhook_test.go @@ -10,47 +10,49 @@ import ( ) func TestNewWebhook(t *testing.T) { - settings.Pattern = &settings.PatternConfig{ - RegExp: regexp.MustCompile(`^PR-(\d+)$`), - } + t.Run("Success", func(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } - raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": { "sonar.analysis.sqbot": "a84442009c09b1adc278b6bb80a3853419f54007" } }`) - response, ok := New(raw) + raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": { "sonar.analysis.sqbot": "a84442009c09b1adc278b6bb80a3853419f54007" } }`) + response, ok := New(raw) - assert.NotNil(t, response) - assert.Equal(t, 1337, response.PRIndex) - assert.Equal(t, "a84442009c09b1adc278b6bb80a3853419f54007", response.Properties.OriginalCommit) - assert.True(t, ok) + assert.NotNil(t, response) + assert.Equal(t, 1337, response.PRIndex) + assert.Equal(t, "a84442009c09b1adc278b6bb80a3853419f54007", response.Properties.OriginalCommit) + assert.True(t, ok) - t.Cleanup(func() { - settings.Pattern = nil + t.Cleanup(func() { + settings.Pattern = nil + }) }) -} -func TestNewWebhookInvalidJSON(t *testing.T) { - raw := []byte(`{ "serverUrl": ["invalid-server-url-content"] }`) - _, ok := New(raw) + t.Run("Invalid JSON", func(t *testing.T) { + raw := []byte(`{ "serverUrl": ["invalid-server-url-content"] }`) + _, ok := New(raw) - assert.False(t, ok) -} + assert.False(t, ok) + }) -func TestNewWebhookInvalidBranchName(t *testing.T) { - settings.Pattern = &settings.PatternConfig{ - RegExp: regexp.MustCompile(`^PR-(\d+)$`), - } + t.Run("Invalid branch name", func(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } - raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "invalid", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) - _, ok := New(raw) + raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "invalid", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) + _, ok := New(raw) - assert.False(t, ok) + assert.False(t, ok) - t.Cleanup(func() { - settings.Pattern = nil + t.Cleanup(func() { + settings.Pattern = nil + }) }) } func TestWebhookGetRevision(t *testing.T) { - t.Run("Default revision", func(t *testing.T) { + t.Run("Default", func(t *testing.T) { w := Webhook{ Revision: "225fa0306c0ab83297d0cb5db0717b194ccb2e76", } @@ -58,7 +60,7 @@ func TestWebhookGetRevision(t *testing.T) { assert.Equal(t, w.Revision, w.GetRevision()) }) - t.Run("Default revision due to incomplete properties", func(t *testing.T) { + t.Run("Incomplete properties", func(t *testing.T) { w := Webhook{ Revision: "225fa0306c0ab83297d0cb5db0717b194ccb2e76", Properties: &properties{}, From b6b90876515c77575c0faea84a17dc3fc1f570c5 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Tue, 12 Jul 2022 12:26:10 +0200 Subject: [PATCH 126/128] Initialize SonarQube SDK with its settings Instead of using global settings. This is important for validated configuration reloads. Signed-off-by: Steven Kriegler --- cmd/gitea-sonarqube-bot/main.go | 4 +- internal/clients/sonarqube/sonarqube.go | 16 ++- internal/clients/sonarqube/sonarqube_test.go | 118 +++++++++++++++---- 3 files changed, 106 insertions(+), 32 deletions(-) diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index ccbbcee..28392bd 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -61,8 +61,8 @@ func serveApi(c *cli.Context) error { log.Println("Hi! I'm Gitea SonarQube Bot. At your service.") log.Println("Config file in use:", config) - giteaHandler := api.NewGiteaWebhookHandler(giteaSdk.New(), sonarQubeSdk.New()) - sqHandler := api.NewSonarQubeWebhookHandler(giteaSdk.New(), sonarQubeSdk.New()) + giteaHandler := api.NewGiteaWebhookHandler(giteaSdk.New(), sonarQubeSdk.New(&settings.SonarQube)) + sqHandler := api.NewSonarQubeWebhookHandler(giteaSdk.New(), sonarQubeSdk.New(&settings.SonarQube)) server := api.New(giteaHandler, sqHandler) srv := &http.Server{ diff --git a/internal/clients/sonarqube/sonarqube.go b/internal/clients/sonarqube/sonarqube.go index 4403e08..a615155 100644 --- a/internal/clients/sonarqube/sonarqube.go +++ b/internal/clients/sonarqube/sonarqube.go @@ -93,16 +93,15 @@ type SonarQubeSdk struct { client ClientInterface bodyReader BodyReader httpRequest HttpRequest - baseUrl string - token string + settings *settings.SonarQubeConfig } func (sdk *SonarQubeSdk) GetPullRequestUrl(project string, index int64) string { - return fmt.Sprintf("%s/dashboard?id=%s&pullRequest=%s", sdk.baseUrl, project, PRNameFromIndex(index)) + return fmt.Sprintf("%s/dashboard?id=%s&pullRequest=%s", sdk.settings.Url, project, PRNameFromIndex(index)) } func (sdk *SonarQubeSdk) fetchPullRequests(project string) (*PullsResponse, error) { - url := fmt.Sprintf("%s/api/project_pull_requests/list?project=%s", sdk.baseUrl, project) + url := fmt.Sprintf("%s/api/project_pull_requests/list?project=%s", sdk.settings.Url, project) request, err := sdk.httpRequest(http.MethodGet, url, nil) if err != nil { return nil, err @@ -137,7 +136,7 @@ func (sdk *SonarQubeSdk) GetPullRequest(project string, index int64) (*PullReque } func (sdk *SonarQubeSdk) GetMeasures(project string, branch string) (*MeasuresResponse, error) { - url := fmt.Sprintf("%s/api/measures/component?additionalFields=metrics&metricKeys=%s&component=%s&pullRequest=%s", sdk.baseUrl, settings.SonarQube.GetMetricsList(), project, branch) + url := fmt.Sprintf("%s/api/measures/component?additionalFields=metrics&metricKeys=%s&component=%s&pullRequest=%s", sdk.settings.Url, settings.SonarQube.GetMetricsList(), project, branch) request, err := sdk.httpRequest(http.MethodGet, url, nil) if err != nil { return nil, err @@ -174,16 +173,15 @@ func (sdk *SonarQubeSdk) ComposeGiteaComment(data *CommentComposeData) (string, } func (sdk *SonarQubeSdk) basicAuth() string { - auth := []byte(fmt.Sprintf("%s:", sdk.token)) + auth := []byte(fmt.Sprintf("%s:", sdk.settings.Token.Value)) return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(auth)) } -func New() *SonarQubeSdk { +func New(configuration *settings.SonarQubeConfig) *SonarQubeSdk { return &SonarQubeSdk{ client: &http.Client{}, bodyReader: io.ReadAll, httpRequest: http.NewRequest, - baseUrl: settings.SonarQube.Url, - token: settings.SonarQube.Token.Value, + settings: configuration, } } diff --git a/internal/clients/sonarqube/sonarqube_test.go b/internal/clients/sonarqube/sonarqube_test.go index 28a0616..6f88278 100644 --- a/internal/clients/sonarqube/sonarqube_test.go +++ b/internal/clients/sonarqube/sonarqube_test.go @@ -80,7 +80,9 @@ func TestGetRenderedQualityGate(t *testing.T) { func TestGetPullRequestUrl(t *testing.T) { sdk := &SonarQubeSdk{ - baseUrl: "https://sonarqube.example.com", + settings: &settings.SonarQubeConfig{ + Url: "https://sonarqube.example.com", + }, } settings.Pattern = &settings.PatternConfig{ Template: "PR-%d", @@ -100,7 +102,11 @@ func TestRetrieveDataFromApi(t *testing.T) { w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) }) sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -121,7 +127,11 @@ func TestRetrieveDataFromApi(t *testing.T) { t.Run("Internal error", func(t *testing.T) { expected := fmt.Errorf("This error indicates an error while performing the request") sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}), recoder: httptest.NewRecorder(), @@ -142,7 +152,11 @@ func TestRetrieveDataFromApi(t *testing.T) { recorder.Code = http.StatusUnauthorized }) sdk := &SonarQubeSdk{ - token: "simulated-invalid-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "simulated-invalid-token", + }, + }, client: &ClientMock{ handler: handler, recoder: recorder, @@ -163,7 +177,11 @@ func TestRetrieveDataFromApi(t *testing.T) { }) expected := fmt.Errorf("Error reading body content") sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -185,7 +203,11 @@ func TestRetrieveDataFromApi(t *testing.T) { w.Write([]byte(`{"pullReq`)) }) sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -207,7 +229,11 @@ func TestFetchPullRequests(t *testing.T) { w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) }) sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -231,7 +257,11 @@ func TestFetchPullRequests(t *testing.T) { }) expected := fmt.Errorf("Some simulated error") sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -254,7 +284,11 @@ func TestFetchPullRequests(t *testing.T) { }) expected := fmt.Errorf("Some simulated error") sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -276,7 +310,11 @@ func TestFetchPullRequests(t *testing.T) { w.Write([]byte(`{"errors":[{"msg":"Project 'test-project' not found"}]}`)) }) sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -303,7 +341,11 @@ func TestGetPullRequest(t *testing.T) { w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) }) sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -331,7 +373,11 @@ func TestGetPullRequest(t *testing.T) { }) expected := fmt.Errorf("Some simulated error") sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -358,7 +404,11 @@ func TestGetPullRequest(t *testing.T) { w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) }) sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -386,7 +436,11 @@ func TestGetMeasures(t *testing.T) { w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"0","bestValue":true}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) }) sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -410,7 +464,11 @@ func TestGetMeasures(t *testing.T) { }) expected := fmt.Errorf("Some simulated error") sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -433,7 +491,11 @@ func TestGetMeasures(t *testing.T) { }) expected := fmt.Errorf("Some simulated error") sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -455,7 +517,11 @@ func TestGetMeasures(t *testing.T) { w.Write([]byte(`{"errors":[{"msg":"Component 'non-existing-project' of pull request 'PR-1' not found"}]}`)) }) sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -479,7 +545,11 @@ func TestComposeGiteaComment(t *testing.T) { w.Write([]byte(`{"component":{"key":"test-project","name":"Test Project","qualifier":"TRK","measures":[{"metric":"bugs","value":"10","bestValue":false}],"pullRequest":"PR-1"},"metrics":[{"key":"bugs","name":"Bugs","description":"Bugs","domain":"Reliability","type":"INT","higherValuesAreBetter":false,"qualitative":false,"hidden":false,"custom":false,"bestValue":"0"}]}`)) }) sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -512,7 +582,11 @@ func TestComposeGiteaComment(t *testing.T) { }) expected := fmt.Errorf("Expected error from GetMeasures") sdk := &SonarQubeSdk{ - token: "test-token", + settings: &settings.SonarQubeConfig{ + Token: &settings.Token{ + Value: "test-token", + }, + }, client: &ClientMock{ handler: handler, recoder: httptest.NewRecorder(), @@ -536,11 +610,13 @@ func TestComposeGiteaComment(t *testing.T) { } func TestNew(t *testing.T) { - settings.SonarQube = settings.SonarQubeConfig{ + config := &settings.SonarQubeConfig{ Url: "http://example.com", Token: &settings.Token{ Value: "test-token", }, } - assert.IsType(t, &SonarQubeSdk{}, New(), "") + actual := New(config) + assert.IsType(t, &SonarQubeSdk{}, actual, "Unexpected return type") + assert.Equal(t, config, actual.settings) } From f808a581777469c1307320b25e678480c623a3e5 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Tue, 12 Jul 2022 16:57:12 +0200 Subject: [PATCH 127/128] Refactor internal name to url-like one Signed-off-by: Steven Kriegler --- cmd/gitea-sonarqube-bot/main.go | 8 ++++---- go.mod | 2 +- internal/api/gitea.go | 8 ++++---- internal/api/gitea_test.go | 3 +-- internal/api/main_test.go | 6 +++--- internal/api/sonarqube.go | 8 ++++---- internal/api/sonarqube_test.go | 3 +-- internal/clients/gitea/gitea.go | 2 +- internal/clients/sonarqube/sonarqube.go | 4 ++-- internal/clients/sonarqube/sonarqube_test.go | 3 +-- internal/webhooks/gitea/comment.go | 8 ++++---- internal/webhooks/gitea/pull.go | 6 +++--- internal/webhooks/sonarqube/webhook.go | 2 +- internal/webhooks/sonarqube/webhook_test.go | 3 +-- 14 files changed, 31 insertions(+), 35 deletions(-) diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index 28392bd..49739e9 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -11,10 +11,10 @@ import ( "syscall" "time" - "gitea-sonarqube-bot/internal/api" - giteaSdk "gitea-sonarqube-bot/internal/clients/gitea" - sonarQubeSdk "gitea-sonarqube-bot/internal/clients/sonarqube" - "gitea-sonarqube-bot/internal/settings" + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/api" + giteaSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/gitea" + sonarQubeSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/sonarqube" + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" "github.com/urfave/cli/v2" ) diff --git a/go.mod b/go.mod index ac4f1ae..1edf702 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module gitea-sonarqube-bot +module codeberg.org/justusbunsi/gitea-sonarqube-bot go 1.18 diff --git a/internal/api/gitea.go b/internal/api/gitea.go index fbe2022..971960d 100644 --- a/internal/api/gitea.go +++ b/internal/api/gitea.go @@ -5,10 +5,10 @@ import ( "log" "net/http" - giteaSdk "gitea-sonarqube-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-bot/internal/clients/sonarqube" - "gitea-sonarqube-bot/internal/settings" - webhook "gitea-sonarqube-bot/internal/webhooks/gitea" + giteaSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/gitea" + sqSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/sonarqube" + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" + webhook "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/webhooks/gitea" ) type GiteaWebhookHandlerInferface interface { diff --git a/internal/api/gitea_test.go b/internal/api/gitea_test.go index 9702ae0..a614e1e 100644 --- a/internal/api/gitea_test.go +++ b/internal/api/gitea_test.go @@ -8,8 +8,7 @@ import ( "net/http/httptest" "testing" - "gitea-sonarqube-bot/internal/settings" - + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" "github.com/stretchr/testify/assert" ) diff --git a/internal/api/main_test.go b/internal/api/main_test.go index ecac2a7..1eb95fe 100644 --- a/internal/api/main_test.go +++ b/internal/api/main_test.go @@ -9,9 +9,9 @@ import ( "os" "testing" - giteaSdk "gitea-sonarqube-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-bot/internal/clients/sonarqube" - "gitea-sonarqube-bot/internal/settings" + giteaSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/gitea" + sqSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/sonarqube" + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" diff --git a/internal/api/sonarqube.go b/internal/api/sonarqube.go index 48cf291..c215df5 100644 --- a/internal/api/sonarqube.go +++ b/internal/api/sonarqube.go @@ -7,10 +7,10 @@ import ( "net/http" "strings" - giteaSdk "gitea-sonarqube-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-bot/internal/clients/sonarqube" - "gitea-sonarqube-bot/internal/settings" - webhook "gitea-sonarqube-bot/internal/webhooks/sonarqube" + giteaSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/gitea" + sqSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/sonarqube" + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" + webhook "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/webhooks/sonarqube" ) type SonarQubeWebhookHandlerInferface interface { diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index e79e517..d93ac05 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -9,8 +9,7 @@ import ( "regexp" "testing" - "gitea-sonarqube-bot/internal/settings" - + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" "github.com/stretchr/testify/assert" ) diff --git a/internal/clients/gitea/gitea.go b/internal/clients/gitea/gitea.go index 4aba30d..92f837a 100644 --- a/internal/clients/gitea/gitea.go +++ b/internal/clients/gitea/gitea.go @@ -2,10 +2,10 @@ package gitea import ( "fmt" - "gitea-sonarqube-bot/internal/settings" "log" "code.gitea.io/sdk/gitea" + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" ) type GiteaSdkInterface interface { diff --git a/internal/clients/sonarqube/sonarqube.go b/internal/clients/sonarqube/sonarqube.go index a615155..708d3e2 100644 --- a/internal/clients/sonarqube/sonarqube.go +++ b/internal/clients/sonarqube/sonarqube.go @@ -10,8 +10,8 @@ import ( "strconv" "strings" - "gitea-sonarqube-bot/internal/actions" - "gitea-sonarqube-bot/internal/settings" + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/actions" + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" ) func ParsePRIndex(name string) (int, error) { diff --git a/internal/clients/sonarqube/sonarqube_test.go b/internal/clients/sonarqube/sonarqube_test.go index 6f88278..6ac73bf 100644 --- a/internal/clients/sonarqube/sonarqube_test.go +++ b/internal/clients/sonarqube/sonarqube_test.go @@ -8,8 +8,7 @@ import ( "regexp" "testing" - "gitea-sonarqube-bot/internal/settings" - + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" "github.com/stretchr/testify/assert" ) diff --git a/internal/webhooks/gitea/comment.go b/internal/webhooks/gitea/comment.go index 7aa121c..4d63dbb 100644 --- a/internal/webhooks/gitea/comment.go +++ b/internal/webhooks/gitea/comment.go @@ -5,10 +5,10 @@ import ( "fmt" "log" - "gitea-sonarqube-bot/internal/actions" - giteaSdk "gitea-sonarqube-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-bot/internal/clients/sonarqube" - "gitea-sonarqube-bot/internal/settings" + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/actions" + giteaSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/gitea" + sqSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/sonarqube" + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" ) type issue struct { diff --git a/internal/webhooks/gitea/pull.go b/internal/webhooks/gitea/pull.go index 9e6b6de..49d3d71 100644 --- a/internal/webhooks/gitea/pull.go +++ b/internal/webhooks/gitea/pull.go @@ -5,9 +5,9 @@ import ( "fmt" "log" - giteaSdk "gitea-sonarqube-bot/internal/clients/gitea" - sqSdk "gitea-sonarqube-bot/internal/clients/sonarqube" - "gitea-sonarqube-bot/internal/settings" + giteaSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/gitea" + sqSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/sonarqube" + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" ) type pullRequest struct { diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index 555cdf9..1ab5412 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -4,7 +4,7 @@ import ( "encoding/json" "log" - sqSdk "gitea-sonarqube-bot/internal/clients/sonarqube" + sqSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/sonarqube" ) type properties struct { diff --git a/internal/webhooks/sonarqube/webhook_test.go b/internal/webhooks/sonarqube/webhook_test.go index ae84321..1ca327f 100644 --- a/internal/webhooks/sonarqube/webhook_test.go +++ b/internal/webhooks/sonarqube/webhook_test.go @@ -4,8 +4,7 @@ import ( "regexp" "testing" - "gitea-sonarqube-bot/internal/settings" - + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" "github.com/stretchr/testify/assert" ) From b632381c908a178791354ab5ef5f4d5a8a7f11c2 Mon Sep 17 00:00:00 2001 From: justusbunsi Date: Tue, 12 Jul 2022 18:31:25 +0200 Subject: [PATCH 128/128] Write tests for Gitea client Signed-off-by: Steven Kriegler Reviewed-on: https://codeberg.org/justusbunsi/gitea-sonarqube-bot/pulls/36 --- Makefile | 8 +- cmd/gitea-sonarqube-bot/main.go | 5 +- internal/clients/gitea/gitea.go | 12 +- internal/clients/gitea/gitea_test.go | 198 +++++++++++++++++++++++++++ internal/clients/gitea/main_test.go | 14 ++ 5 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 internal/clients/gitea/gitea_test.go create mode 100644 internal/clients/gitea/main_test.go diff --git a/Makefile b/Makefile index 87bd590..98cee96 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,8 @@ help: @echo " - test Run full test suite" @echo " - test p=./path/to/package Run test suite for specific package" @echo " - test\#SpecificTestName Run a specific" - @echo " - coverage Run full test suite and generates coverage report as HTML file" + @echo " - coverage Run full test suite and generate coverage report as HTML file" + @echo " - coverage p=./path/to/package Run test suite for specific package and generate coverage report as HTML file" @echo " - helm-params Auto-generates 'Parameters' section of 'helm/README.md' based on comments in values.yaml" @echo " - helm-pack Prepares Helm Chart release artifacts for pushing to 'charts' branch" @echo " - dep Dependency maintenance (tidy, vendor, verify)" @@ -44,8 +45,13 @@ test-ci: go test -mod=vendor -coverprofile=cover.out -json ./... > test-report.out coverage: +ifdef p + go test -coverprofile=cover.out $(p) + go tool cover -html=cover.out -o cover.html +else go test -coverprofile=cover.out ./... go tool cover -html=cover.out -o cover.html +endif helm-params: npm install diff --git a/cmd/gitea-sonarqube-bot/main.go b/cmd/gitea-sonarqube-bot/main.go index 49739e9..371f966 100644 --- a/cmd/gitea-sonarqube-bot/main.go +++ b/cmd/gitea-sonarqube-bot/main.go @@ -16,6 +16,7 @@ import ( sonarQubeSdk "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/clients/sonarqube" "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" + "code.gitea.io/sdk/gitea" "github.com/urfave/cli/v2" ) @@ -61,8 +62,8 @@ func serveApi(c *cli.Context) error { log.Println("Hi! I'm Gitea SonarQube Bot. At your service.") log.Println("Config file in use:", config) - giteaHandler := api.NewGiteaWebhookHandler(giteaSdk.New(), sonarQubeSdk.New(&settings.SonarQube)) - sqHandler := api.NewSonarQubeWebhookHandler(giteaSdk.New(), sonarQubeSdk.New(&settings.SonarQube)) + giteaHandler := api.NewGiteaWebhookHandler(giteaSdk.New(&settings.Gitea, gitea.NewClient), sonarQubeSdk.New(&settings.SonarQube)) + sqHandler := api.NewSonarQubeWebhookHandler(giteaSdk.New(&settings.Gitea, gitea.NewClient), sonarQubeSdk.New(&settings.SonarQube)) server := api.New(giteaHandler, sqHandler) srv := &http.Server{ diff --git a/internal/clients/gitea/gitea.go b/internal/clients/gitea/gitea.go index 92f837a..0bda95f 100644 --- a/internal/clients/gitea/gitea.go +++ b/internal/clients/gitea/gitea.go @@ -14,8 +14,14 @@ type GiteaSdkInterface interface { DetermineHEAD(settings.GiteaRepository, int64) (string, error) } +type ClientInterface interface { + CreateIssueComment(owner, repo string, index int64, opt gitea.CreateIssueCommentOption) (*gitea.Comment, *gitea.Response, error) + CreateStatus(owner, repo, sha string, opts gitea.CreateStatusOption) (*gitea.Status, *gitea.Response, error) + GetPullRequest(owner, repo string, index int64) (*gitea.PullRequest, *gitea.Response, error) +} + type GiteaSdk struct { - client *gitea.Client + client ClientInterface } func (sdk *GiteaSdk) PostComment(repo settings.GiteaRepository, idx int, msg string) error { @@ -53,8 +59,8 @@ func (sdk *GiteaSdk) DetermineHEAD(repo settings.GiteaRepository, idx int64) (st return pr.Head.Sha, nil } -func New() *GiteaSdk { - client, err := gitea.NewClient(settings.Gitea.Url, gitea.SetToken(settings.Gitea.Token.Value)) +func New[T ClientInterface](configuration *settings.GiteaConfig, newClient func(url string, options ...gitea.ClientOption) (T, error)) *GiteaSdk { + client, err := newClient(configuration.Url, gitea.SetToken(configuration.Token.Value)) if err != nil { panic(fmt.Errorf("cannot initialize Gitea client: %w", err)) } diff --git a/internal/clients/gitea/gitea_test.go b/internal/clients/gitea/gitea_test.go new file mode 100644 index 0000000..a094245 --- /dev/null +++ b/internal/clients/gitea/gitea_test.go @@ -0,0 +1,198 @@ +package gitea + +import ( + "errors" + "net/http" + "testing" + + "code.gitea.io/sdk/gitea" + "codeberg.org/justusbunsi/gitea-sonarqube-bot/internal/settings" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type SdkMock struct { + simulatedError error + mock.Mock +} + +func (m *SdkMock) CreateIssueComment(owner, repo string, index int64, opt gitea.CreateIssueCommentOption) (*gitea.Comment, *gitea.Response, error) { + m.Called(owner, repo, index, opt) + return nil, nil, m.simulatedError +} +func (m *SdkMock) CreateStatus(owner, repo, sha string, opts gitea.CreateStatusOption) (*gitea.Status, *gitea.Response, error) { + m.Called(owner, repo, sha, opts) + r := &gitea.Response{ + Response: &http.Response{ + StatusCode: http.StatusOK, + }, + } + if m.simulatedError != nil { + r.StatusCode = http.StatusInternalServerError + } + return nil, r, m.simulatedError +} +func (m *SdkMock) GetPullRequest(owner, repo string, index int64) (*gitea.PullRequest, *gitea.Response, error) { + m.Called(owner, repo, index) + return &gitea.PullRequest{ + Head: &gitea.PRBranchInfo{ + Sha: "a1aada0b7b19e58ae539b4812d960bca35ev78cb", + }, + }, nil, m.simulatedError +} + +func TestNew(t *testing.T) { + t.Run("Success", func(t *testing.T) { + config := &settings.GiteaConfig{ + Url: "http://example.com", + Token: &settings.Token{ + Value: "test-token", + }, + } + + callback := func(url string, options ...gitea.ClientOption) (*SdkMock, error) { + return &SdkMock{}, nil + } + assert.IsType(t, &GiteaSdk{}, New(config, callback), "") + }) + + t.Run("Initialization errors", func(t *testing.T) { + config := &settings.GiteaConfig{ + Url: "http://example.com", + Token: &settings.Token{ + Value: "test-token", + }, + } + + callback := func(url string, options ...gitea.ClientOption) (*SdkMock, error) { + return nil, errors.New("Simulated initialization error") + } + assert.PanicsWithError(t, "cannot initialize Gitea client: Simulated initialization error", func() { New(config, callback) }) + }) +} + +func TestDetermineHEAD(t *testing.T) { + t.Run("Success", func(t *testing.T) { + clientMock := &SdkMock{} + clientMock.On("GetPullRequest", "test-owner", "test-repo", int64(1)).Once() + + sdk := GiteaSdk{ + client: clientMock, + } + sha, err := sdk.DetermineHEAD(settings.GiteaRepository{ + Owner: "test-owner", + Name: "test-repo", + }, 1) + + assert.Nil(t, err) + assert.Equal(t, "a1aada0b7b19e58ae539b4812d960bca35ev78cb", sha) + clientMock.AssertExpectations(t) + }) + + t.Run("API error", func(t *testing.T) { + clientMock := &SdkMock{ + simulatedError: errors.New("Simulated error"), + } + clientMock.On("GetPullRequest", "test-owner", "test-repo", int64(1)).Once() + + sdk := GiteaSdk{ + client: clientMock, + } + + _, err := sdk.DetermineHEAD(settings.GiteaRepository{ + Owner: "test-owner", + Name: "test-repo", + }, 1) + + assert.Errorf(t, err, "Simulated error") + clientMock.AssertExpectations(t) + }) +} + +func TestUpdateStatus(t *testing.T) { + t.Run("Success", func(t *testing.T) { + clientMock := &SdkMock{} + clientMock.On("CreateStatus", "test-owner", "test-repo", "a1aada0b7b19e58ae539b4812d960bca35ev78cb", mock.Anything).Once() + sdk := GiteaSdk{ + client: clientMock, + } + + err := sdk.UpdateStatus(settings.GiteaRepository{ + Owner: "test-owner", + Name: "test-repo", + }, "a1aada0b7b19e58ae539b4812d960bca35ev78cb", StatusDetails{ + Url: "http://example.com", + Message: "expected message", + State: StatusOK, + }) + + assert.Nil(t, err) + clientMock.AssertExpectations(t) + + actualStatusOption := clientMock.Calls[0].Arguments[3].(gitea.CreateStatusOption) + assert.Equal(t, "http://example.com", actualStatusOption.TargetURL) + assert.Equal(t, "expected message", actualStatusOption.Description) + assert.Equal(t, gitea.StatusSuccess, actualStatusOption.State) + }) + + t.Run("API error", func(t *testing.T) { + clientMock := &SdkMock{ + simulatedError: errors.New("Simulated error"), + } + clientMock.On("CreateStatus", "test-owner", "test-repo", "a1aada0b7b19e58ae539b4812d960bca35ev78cb", mock.Anything).Once() + sdk := GiteaSdk{ + client: clientMock, + } + + err := sdk.UpdateStatus(settings.GiteaRepository{ + Owner: "test-owner", + Name: "test-repo", + }, "a1aada0b7b19e58ae539b4812d960bca35ev78cb", StatusDetails{ + Url: "http://example.com", + Message: "expected message", + State: StatusOK, + }) + + assert.Errorf(t, err, "Simulated error") + clientMock.AssertExpectations(t) + }) +} + +func TestPostComment(t *testing.T) { + t.Run("Success", func(t *testing.T) { + clientMock := &SdkMock{} + clientMock.On("CreateIssueComment", "test-owner", "test-repo", int64(1), mock.Anything).Once() + sdk := GiteaSdk{ + client: clientMock, + } + + err := sdk.PostComment(settings.GiteaRepository{ + Owner: "test-owner", + Name: "test-repo", + }, 1, "test post comment") + + assert.Nil(t, err) + clientMock.AssertExpectations(t) + + actualCommentOption := clientMock.Calls[0].Arguments[3].(gitea.CreateIssueCommentOption) + assert.Equal(t, "test post comment", actualCommentOption.Body) + }) + + t.Run("API error", func(t *testing.T) { + clientMock := &SdkMock{ + simulatedError: errors.New("Simulated error"), + } + clientMock.On("CreateIssueComment", "test-owner", "test-repo", int64(1), mock.Anything).Once() + sdk := GiteaSdk{ + client: clientMock, + } + + err := sdk.PostComment(settings.GiteaRepository{ + Owner: "test-owner", + Name: "test-repo", + }, 1, "test post comment") + + assert.Errorf(t, err, "Simulated error") + clientMock.AssertExpectations(t) + }) +} diff --git a/internal/clients/gitea/main_test.go b/internal/clients/gitea/main_test.go new file mode 100644 index 0000000..1355cbb --- /dev/null +++ b/internal/clients/gitea/main_test.go @@ -0,0 +1,14 @@ +package gitea + +import ( + "io/ioutil" + "log" + "os" + "testing" +) + +// SETUP: mute logs +func TestMain(m *testing.M) { + log.SetOutput(ioutil.Discard) + os.Exit(m.Run()) +}