Add SonarQube endpoint
Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com>
This commit is contained in:
parent
118c08090a
commit
c575542118
|
@ -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))
|
||||
}
|
||||
|
|
14
internal/webhooks/sonarqube/main_test.go
Normal file
14
internal/webhooks/sonarqube/main_test.go
Normal 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())
|
||||
}
|
61
internal/webhooks/sonarqube/sonarqube.go
Normal file
61
internal/webhooks/sonarqube/sonarqube.go
Normal 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."}`)
|
||||
}
|
79
internal/webhooks/sonarqube/sonarqube_test.go
Normal file
79
internal/webhooks/sonarqube/sonarqube_test.go
Normal 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())
|
||||
}
|
47
internal/webhooks/sonarqube/webhook.go
Normal file
47
internal/webhooks/sonarqube/webhook.go
Normal 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
|
||||
}
|
23
internal/webhooks/sonarqube/webhook_test.go
Normal file
23
internal/webhooks/sonarqube/webhook_test.go
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue