Add SonarQube endpoint

Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com>
This commit is contained in:
justusbunsi 2021-06-24 14:38:04 +02:00
parent 118c08090a
commit c575542118
No known key found for this signature in database
GPG key ID: 990B348ECAC9C7DB
6 changed files with 253 additions and 31 deletions

View file

@ -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))
}

View file

@ -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())
}

View file

@ -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."}`)
}

View file

@ -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())
}

View file

@ -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
}

View file

@ -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)
}