1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00

feat(sonar): add output of measurements (#2218)

* add sonarqube measurements

* fetch measurements from API

* add api for fetching issue counts

* add debug outputs

* add further severities

* log number of issues

* report failure

* expose method to send request

* Fixed what was broken.

* add debug output

* wip

* correct opaque property

* push client handling to apiClient.go

* use correct API URL

* correct log outputs

* remove logging

* remove option validation

* extend search options

* restructure

* rename api client file

* simplify client usage

* simplify issue client

* write sonar values to influx

* extract issue service

* reorder imports

* add sonar integration test

* allow unknown fields

* add test case

* add test case

* remove

* fix

* Update http.go

* Apply suggestions from code review

* Update cmd/sonarExecuteScan.go

* rework test cases

* use explicit returns

* add task service

* add waitfortask

* fix typo

* remove fixme

* expose poll interval

* rename test cases

* add test cases

* use newAPIClient method

* use waitForTask

* rename services

* finalize code

* handle error

* move defer

* move types

* add test case

* use http.status...

* add test case

* expose api endpoint names

* extract api client

* adjust test cases

* Update integration-tests-pr.yaml

* Update integration-tests.yaml

* improve require message

* Update integration-tests-pr.yaml

* Update integration-tests-pr.yaml
This commit is contained in:
Christopher Fenner 2021-02-24 15:44:23 +01:00 committed by GitHub
parent fbbb55471d
commit cb3fa7c293
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 933 additions and 29 deletions

View File

@ -10,6 +10,9 @@ import (
"strings"
"time"
"github.com/bmatcuk/doublestar"
"github.com/pkg/errors"
"github.com/SAP/jenkins-library/pkg/command"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
@ -18,8 +21,6 @@ import (
StepResults "github.com/SAP/jenkins-library/pkg/piperutils"
SonarUtils "github.com/SAP/jenkins-library/pkg/sonar"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/bmatcuk/doublestar"
"github.com/pkg/errors"
)
type sonarSettings struct {
@ -76,9 +77,13 @@ func sonarExecuteScan(config sonarExecuteScanOptions, _ *telemetry.CustomData, i
// reroute command output to logging framework
runner.Stdout(log.Writer())
runner.Stderr(log.Writer())
client := piperhttp.Client{}
client.SetOptions(piperhttp.ClientOptions{TransportTimeout: 20 * time.Second})
// client for downloading the sonar-scanner
downloadClient := &piperhttp.Client{}
downloadClient.SetOptions(piperhttp.ClientOptions{TransportTimeout: 20 * time.Second})
// client for talking to the SonarQube API
apiClient := &piperhttp.Client{}
//TODO: implement certificate handling
apiClient.SetOptions(piperhttp.ClientOptions{TransportSkipVerification: true})
sonar = sonarSettings{
workingDir: "./",
@ -88,7 +93,7 @@ func sonarExecuteScan(config sonarExecuteScanOptions, _ *telemetry.CustomData, i
}
influx.step_data.fields.sonar = false
if err := runSonar(config, &client, &runner); err != nil {
if err := runSonar(config, downloadClient, &runner, apiClient, influx); err != nil {
if log.GetErrorCategory() == log.ErrorUndefined && runner.GetExitCode() == 2 {
// see https://github.com/SonarSource/sonar-scanner-cli/blob/adb67d645c3bcb9b46f29dea06ba082ebec9ba7a/src/main/java/org/sonarsource/scanner/cli/Exit.java#L25
log.SetErrorCategory(log.ErrorConfiguration)
@ -98,7 +103,7 @@ func sonarExecuteScan(config sonarExecuteScanOptions, _ *telemetry.CustomData, i
influx.step_data.fields.sonar = true
}
func runSonar(config sonarExecuteScanOptions, client piperhttp.Downloader, runner command.ExecRunner) error {
func runSonar(config sonarExecuteScanOptions, client piperhttp.Downloader, runner command.ExecRunner, apiClient SonarUtils.Sender, influx *sonarExecuteScanInflux) error {
if len(config.ServerURL) > 0 {
sonar.addEnvironment("SONAR_HOST_URL=" + config.ServerURL)
}
@ -168,6 +173,36 @@ func runSonar(config sonarExecuteScanOptions, client piperhttp.Downloader, runne
}
StepResults.PersistReportsAndLinks("sonarExecuteScan", sonar.workingDir, nil, links)
}
taskService := SonarUtils.NewTaskService(taskReport.ServerURL, config.Token, taskReport.TaskID, apiClient)
// wait for analysis task to complete
err = taskService.WaitForTask()
if err != nil {
return err
}
// fetch number of issues by severity
issueService := SonarUtils.NewIssuesService(taskReport.ServerURL, config.Token, taskReport.ProjectKey, config.Organization, config.BranchName, config.ChangeID, apiClient)
influx.sonarqube_data.fields.blocker_issues, err = issueService.GetNumberOfBlockerIssues()
if err != nil {
return err
}
influx.sonarqube_data.fields.critical_issues, err = issueService.GetNumberOfCriticalIssues()
if err != nil {
return err
}
influx.sonarqube_data.fields.major_issues, err = issueService.GetNumberOfMajorIssues()
if err != nil {
return err
}
influx.sonarqube_data.fields.minor_issues, err = issueService.GetNumberOfMinorIssues()
if err != nil {
return err
}
influx.sonarqube_data.fields.info_issues, err = issueService.GetNumberOfInfoIssues()
if err != nil {
return err
}
log.Entry().Debugf("Influx values: %v", influx.sonarqube_data.fields)
return nil
}

View File

@ -51,6 +51,17 @@ type sonarExecuteScanInflux struct {
tags struct {
}
}
sonarqube_data struct {
fields struct {
blocker_issues int
critical_issues int
major_issues int
minor_issues int
info_issues int
}
tags struct {
}
}
}
func (i *sonarExecuteScanInflux) persist(path, resourceName string) {
@ -61,6 +72,11 @@ func (i *sonarExecuteScanInflux) persist(path, resourceName string) {
value interface{}
}{
{valType: config.InfluxField, measurement: "step_data", name: "sonar", value: i.step_data.fields.sonar},
{valType: config.InfluxField, measurement: "sonarqube_data", name: "blocker_issues", value: i.sonarqube_data.fields.blocker_issues},
{valType: config.InfluxField, measurement: "sonarqube_data", name: "critical_issues", value: i.sonarqube_data.fields.critical_issues},
{valType: config.InfluxField, measurement: "sonarqube_data", name: "major_issues", value: i.sonarqube_data.fields.major_issues},
{valType: config.InfluxField, measurement: "sonarqube_data", name: "minor_issues", value: i.sonarqube_data.fields.minor_issues},
{valType: config.InfluxField, measurement: "sonarqube_data", name: "info_issues", value: i.sonarqube_data.fields.info_issues},
}
errCount := 0
@ -425,6 +441,7 @@ func sonarExecuteScanMetadata() config.StepData {
Type: "influx",
Parameters: []map[string]interface{}{
{"Name": "step_data"}, {"fields": []map[string]string{{"name": "sonar"}}},
{"Name": "sonarqube_data"}, {"fields": []map[string]string{{"name": "blocker_issues"}, {"name": "critical_issues"}, {"name": "major_issues"}, {"name": "minor_issues"}, {"name": "info_issues"}}},
},
},
},

View File

@ -2,7 +2,6 @@ package cmd
import (
"fmt"
"github.com/bmatcuk/doublestar"
"io/ioutil"
"net/http"
"os"
@ -10,12 +9,16 @@ import (
"path/filepath"
"testing"
piperHttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/mock"
FileUtils "github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/bmatcuk/doublestar"
"github.com/jarcoal/httpmock"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
piperHttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/mock"
FileUtils "github.com/SAP/jenkins-library/pkg/piperutils"
SonarUtils "github.com/SAP/jenkins-library/pkg/sonar"
)
//TODO: extract to mock package
@ -90,13 +93,32 @@ func mockGlob(matchesForPatterns map[string][]string) func(pattern string) ([]st
func createTaskReportFile(t *testing.T, workingDir string) {
require.NoError(t, os.MkdirAll(filepath.Join(workingDir, ".scannerwork"), 0755))
require.NoError(t, ioutil.WriteFile(filepath.Join(workingDir, ".scannerwork", "report-task.txt"), []byte("projectKey=piper-test\nserverUrl=https://sonarcloud.io\nserverVersion=8.0.0.12345\ndashboardUrl=https://sonarcloud.io/dashboard/index/piper-test\nceTaskId=AXERR2JBbm9IiM5TEST\nceTaskUrl=https://sonarcloud.io/api/ce/task?id=AXERR2JBbm9IiMTEST"), 0755))
require.NoError(t, ioutil.WriteFile(filepath.Join(workingDir, ".scannerwork", "report-task.txt"), []byte(taskReportContent), 0755))
require.FileExists(t, filepath.Join(workingDir, ".scannerwork", "report-task.txt"))
}
const sonarServerURL = "https://sonarcloud.io"
const taskReportContent = `
projectKey=piper-test
serverUrl=` + sonarServerURL + `
serverVersion=8.0.0.12345
dashboardUrl=` + sonarServerURL + `/dashboard/index/piper-test
ceTaskId=AXERR2JBbm9IiM5TEST
ceTaskUrl=` + sonarServerURL + `/api/ce/task?id=AXERR2JBbm9IiMTEST
`
func TestRunSonar(t *testing.T) {
mockRunner := mock.ExecMockRunner{}
mockClient := mockDownloader{shouldFail: false}
mockDownloadClient := mockDownloader{shouldFail: false}
apiClient := &piperHttp.Client{}
apiClient.SetOptions(piperHttp.ClientOptions{UseDefaultTransport: true})
// mock SonarQube API calls
httpmock.Activate()
defer httpmock.DeactivateAndReset()
// add response handler
httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointCeTask+"", httpmock.NewStringResponder(http.StatusOK, `{ "task": { "componentId": "AXERR2JBbm9IiM5TEST", "status": "SUCCESS" }}`))
httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointIssuesSearch+"", httpmock.NewStringResponder(http.StatusOK, `{ "total": 0 }`))
t.Run("default", func(t *testing.T) {
// init
@ -114,7 +136,7 @@ func TestRunSonar(t *testing.T) {
options := sonarExecuteScanOptions{
CustomTLSCertificateLinks: []string{},
Token: "secret-ABC",
ServerURL: "https://sonar.sap.com",
ServerURL: sonarServerURL,
Organization: "SAP",
ProjectVersion: "1.2.3",
}
@ -126,12 +148,12 @@ func TestRunSonar(t *testing.T) {
os.Unsetenv("PIPER_SONAR_LOAD_CERTIFICATES")
}()
// test
err = runSonar(options, &mockClient, &mockRunner)
err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{})
// assert
assert.NoError(t, err)
assert.Contains(t, sonar.options, "-Dsonar.projectVersion=1.2.3")
assert.Contains(t, sonar.options, "-Dsonar.organization=SAP")
assert.Contains(t, sonar.environment, "SONAR_HOST_URL=https://sonar.sap.com")
assert.Contains(t, sonar.environment, "SONAR_HOST_URL="+sonarServerURL)
assert.Contains(t, sonar.environment, "SONAR_TOKEN=secret-ABC")
assert.Contains(t, sonar.environment, "SONAR_SCANNER_OPTS=-Djavax.net.ssl.trustStore="+filepath.Join(getWorkingDir(), ".certificates", "cacerts")+" -Djavax.net.ssl.trustStorePassword=changeit")
assert.FileExists(t, filepath.Join(sonar.workingDir, "sonarExecuteScan_reports.json"))
@ -158,7 +180,7 @@ func TestRunSonar(t *testing.T) {
fileUtilsExists = FileUtils.FileExists
}()
// test
err = runSonar(options, &mockClient, &mockRunner)
err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{})
// assert
assert.NoError(t, err)
assert.Contains(t, sonar.options, "-Dsonar.projectKey=piper")
@ -197,7 +219,7 @@ func TestRunSonar(t *testing.T) {
InferJavaBinaries: true,
}
// test
err = runSonar(options, &mockClient, &mockRunner)
err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{})
// assert
assert.NoError(t, err)
assert.Contains(t, sonar.options, fmt.Sprintf("-Dsonar.java.binaries=%s,%s,%s",
@ -238,7 +260,7 @@ func TestRunSonar(t *testing.T) {
InferJavaBinaries: true,
}
// test
err = runSonar(options, &mockClient, &mockRunner)
err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{})
// assert
assert.NoError(t, err)
assert.NotContains(t, sonar.options, fmt.Sprintf("-Dsonar.java.binaries=%s",
@ -269,7 +291,7 @@ func TestRunSonar(t *testing.T) {
fileUtilsExists = FileUtils.FileExists
}()
// test
err = runSonar(options, &mockClient, &mockRunner)
err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{})
// assert
assert.NoError(t, err)
assert.Contains(t, sonar.options, "-Dsonar.projectKey=mock-project-key")

4
go.mod
View File

@ -38,8 +38,10 @@ require (
github.com/hashicorp/vault/api v1.0.4
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/jarcoal/httpmock v1.0.6
github.com/jarcoal/httpmock v1.0.8
github.com/magiconair/properties v1.8.4
github.com/magicsong/color-glog v0.0.1 // indirect
github.com/magicsong/sonargo v0.0.1
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/mitchellh/reflectwalk v1.0.1 // indirect

12
go.sum
View File

@ -97,7 +97,9 @@ github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4Rq
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
@ -536,8 +538,8 @@ github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbk
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI=
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
github.com/jarcoal/httpmock v1.0.6 h1:e81vOSexXU3mJuJ4l//geOmKIt+Vkxerk1feQBC8D0g=
github.com/jarcoal/httpmock v1.0.6/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@ -596,6 +598,10 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY=
github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magicsong/color-glog v0.0.1 h1:oNcPsLimp32VzXxzaAz9XJwaKozMZlH/ey+SeFnMQ78=
github.com/magicsong/color-glog v0.0.1/go.mod h1:deWCmaVwA0vkOmXxEmQKwSEDo5toQkmboS07mAjQ4mE=
github.com/magicsong/sonargo v0.0.1 h1:BLEUJZP2gDoVcf6dxp6aX7J63q7iSdoWc64TWhYqsy4=
github.com/magicsong/sonargo v0.0.1/go.mod h1:YTbxs7Tlp8ACfSRljHZgJKX3/C2Dgno+hC0oRoNx6hs=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -721,6 +727,7 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@ -1202,6 +1209,7 @@ google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEG
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -10,11 +10,49 @@ import (
"testing"
"time"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/piperenv"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SAP/jenkins-library/pkg/command"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/piperenv"
"github.com/SAP/jenkins-library/pkg/sonar"
)
func TestSonarIssueSearch(t *testing.T) {
t.Parallel()
// init
token := os.Getenv("PIPER_INTEGRATION_SONAR_TOKEN")
require.NotEmpty(t, token, "SonarQube API Token is missing")
host := os.Getenv("PIPER_INTEGRATION_SONAR_HOST")
if len(host) == 0 {
host = "https://sonarcloud.io"
}
organization := os.Getenv("PIPER_INTEGRATION_SONAR_ORGANIZATION")
if len(organization) == 0 {
organization = "sap-1"
}
componentKey := os.Getenv("PIPER_INTEGRATION_SONAR_PROJECT")
if len(componentKey) == 0 {
componentKey = "SAP_jenkins-library"
}
options := &sonar.IssuesSearchOption{
ComponentKeys: componentKey,
Severities: "INFO",
Resolved: "false",
Ps: "1",
Organization: organization,
}
issueService := sonar.NewIssuesService(host, token, componentKey, organization, "", "", &piperhttp.Client{})
// test
result, _, err := issueService.SearchIssues(options)
// assert
assert.NoError(t, err)
assert.NotEmpty(t, result.Components)
//FIXME: include once implememnted
// assert.NotEmpty(t, result.Organizations)
}
func TestPiperGithubPublishRelease(t *testing.T) {
t.Parallel()
token := os.Getenv("PIPER_INTEGRATION_GITHUB_TOKEN")

View File

@ -6,17 +6,17 @@ package main
import (
"bytes"
"fmt"
"github.com/SAP/jenkins-library/pkg/piperutils"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"testing"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/stretchr/testify/assert"
"io/ioutil"
"path/filepath"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/piperutils"
)
func TestPiperHelp(t *testing.T) {

73
pkg/sonar/client.go Normal file
View File

@ -0,0 +1,73 @@
package sonar
import (
"encoding/json"
"net/http"
"strings"
"github.com/SAP/jenkins-library/pkg/log"
sonargo "github.com/magicsong/sonargo/sonar"
)
// Requester ...
type Requester struct {
Client Sender
Host string
Username string
Password string
// TODO: implement certificate handling
// Certificates [][]byte
}
// Sender provides an interface to the piper http client for uid/pwd and token authenticated requests
type Sender interface {
Send(*http.Request) (*http.Response, error)
}
func (requester *Requester) create(method, path string, options interface{}) (request *http.Request, err error) {
sonarGoClient, err := sonargo.NewClient(requester.Host, requester.Username, requester.Password)
if err != nil {
return
}
// reuse request creation from sonargo
request, err = sonarGoClient.NewRequest(method, path, options)
if err != nil {
return
}
// request created by sonarGO uses .Opaque without the host parameter leading to a request against https://api/issues/search
// https://github.com/magicsong/sonargo/blob/103eda7abc20bd192a064b6eb94ba26329e339f1/sonar/sonarqube.go#L55
request.URL.Opaque = ""
request.URL.Path = sonarGoClient.BaseURL().Path + path
return
}
func (requester *Requester) send(request *http.Request) (*http.Response, error) {
return requester.Client.Send(request)
}
func (requester *Requester) decode(response *http.Response, result interface{}) error {
decoder := json.NewDecoder(response.Body)
defer response.Body.Close()
// sonargo.IssuesSearchObject does not imlement "internal" field organization and thus decoding fails
// anyway the field is currently not needed so we simply allow (and drop) unknown fields to avoid extending the type
// decoder.DisallowUnknownFields()
return decoder.Decode(result)
}
// NewAPIClient ...
func NewAPIClient(host, token string, client Sender) *Requester {
// Make sure the given URL end with a slash
if !strings.HasSuffix(host, "/") {
host += "/"
}
// Make sure the given URL end with a api/
if !strings.HasSuffix(host, "api/") {
host += "api/"
}
log.Entry().Debugf("using api client for '%s'", host)
return &Requester{
Client: client,
Host: host,
Username: token,
}
}

49
pkg/sonar/client_test.go Normal file
View File

@ -0,0 +1,49 @@
package sonar
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestCreate(t *testing.T) {
testURL := "https://example.org/api/"
t.Run("", func(t *testing.T) {
// init
requester := Requester{
Host: testURL,
Username: mock.Anything,
}
// test
request, err := requester.create(http.MethodGet, mock.Anything, &IssuesSearchOption{P: "42"})
// assert
assert.NoError(t, err)
assert.Empty(t, request.URL.Opaque)
assert.Equal(t, http.MethodGet, request.Method)
assert.Equal(t, "https", request.URL.Scheme)
assert.Equal(t, "example.org", request.URL.Host)
assert.Equal(t, "/api/"+mock.Anything, request.URL.Path)
assert.Contains(t, request.Header.Get("Authorization"), "Basic ")
})
}
func TestNewAPIClient(t *testing.T) {
tests := []struct {
name string
host string
want string
}{
{name: mock.Anything, want: "https://example.org/api/", host: "https://example.org"},
{name: mock.Anything, want: "https://example.org/api/", host: "https://example.org/"},
{name: mock.Anything, want: "https://example.org/api/", host: "https://example.org/api"},
{name: mock.Anything, want: "https://example.org/api/", host: "https://example.org/api/"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewAPIClient(tt.host, mock.Anything, nil)
assert.Equal(t, tt.want, got.Host)
})
}
}

104
pkg/sonar/issueService.go Normal file
View File

@ -0,0 +1,104 @@
package sonar
import (
"net/http"
sonargo "github.com/magicsong/sonargo/sonar"
"github.com/pkg/errors"
)
// EndpointIssuesSearch API endpoint for https://sonarcloud.io/web_api/api/issues/search
const EndpointIssuesSearch = "issues/search"
// IssueService ...
type IssueService struct {
Organization string
Project string
Branch string
PullRequest string
apiClient *Requester
}
// SearchIssues ...
func (service *IssueService) SearchIssues(options *IssuesSearchOption) (*sonargo.IssuesSearchObject, *http.Response, error) {
request, err := service.apiClient.create("GET", EndpointIssuesSearch, options)
if err != nil {
return nil, nil, err
}
// use custom HTTP client to send request
response, err := service.apiClient.send(request)
if err != nil {
return nil, nil, err
}
// reuse response verrification from sonargo
err = sonargo.CheckResponse(response)
if err != nil {
return nil, response, err
}
// decode JSON response
result := new(sonargo.IssuesSearchObject)
err = service.apiClient.decode(response, result)
if err != nil {
return nil, response, err
}
return result, response, nil
}
func (service *IssueService) getIssueCount(severity issueSeverity) (int, error) {
options := &IssuesSearchOption{
ComponentKeys: service.Project,
Severities: severity.ToString(),
Resolved: "false",
Ps: "1",
}
if len(service.Branch) > 0 {
options.Branch = service.Branch
}
if len(service.Organization) > 0 {
options.Organization = service.Organization
}
if len(service.PullRequest) > 0 {
options.PullRequest = service.PullRequest
}
result, _, err := service.SearchIssues(options)
if err != nil {
return -1, errors.Wrapf(err, "failed to fetch the numer of '%s' issues", severity)
}
return result.Total, nil
}
// GetNumberOfBlockerIssues returns the number of issue with BLOCKER severity.
func (service *IssueService) GetNumberOfBlockerIssues() (int, error) {
return service.getIssueCount(blocker)
}
// GetNumberOfCriticalIssues returns the number of issue with CRITICAL severity.
func (service *IssueService) GetNumberOfCriticalIssues() (int, error) {
return service.getIssueCount(critical)
}
// GetNumberOfMajorIssues returns the number of issue with MAJOR severity.
func (service *IssueService) GetNumberOfMajorIssues() (int, error) {
return service.getIssueCount(major)
}
// GetNumberOfMinorIssues returns the number of issue with MINOR severity.
func (service *IssueService) GetNumberOfMinorIssues() (int, error) {
return service.getIssueCount(minor)
}
// GetNumberOfInfoIssues returns the number of issue with INFO severity.
func (service *IssueService) GetNumberOfInfoIssues() (int, error) {
return service.getIssueCount(info)
}
// NewIssuesService returns a new instance of a service for the issues API endpoint.
func NewIssuesService(host, token, project, organization, branch, pullRequest string, client Sender) *IssueService {
return &IssueService{
Organization: organization,
Project: project,
Branch: branch,
PullRequest: pullRequest,
apiClient: NewAPIClient(host, token, client),
}
}

View File

@ -0,0 +1,164 @@
package sonar
import (
"net/http"
"testing"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
)
func TestIssueService(t *testing.T) {
testURL := "https://example.org"
t.Run("success", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
sender := &piperhttp.Client{}
sender.SetOptions(piperhttp.ClientOptions{UseDefaultTransport: true})
// add response handler
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointIssuesSearch+"", httpmock.NewStringResponder(http.StatusOK, responseIssueSearchCritical))
// create service instance
serviceUnderTest := NewIssuesService(testURL, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, sender)
// test
count, err := serviceUnderTest.GetNumberOfBlockerIssues()
// assert
assert.NoError(t, err)
assert.Equal(t, 111, count)
assert.Equal(t, 1, httpmock.GetTotalCallCount(), "unexpected number of requests")
})
t.Run("error", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
sender := &piperhttp.Client{}
sender.SetOptions(piperhttp.ClientOptions{UseDefaultTransport: true})
// add response handler
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointIssuesSearch+"", httpmock.NewStringResponder(http.StatusNotFound, responseIssueSearchError))
// create service instance
serviceUnderTest := NewIssuesService(testURL, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, sender)
// test
count, err := serviceUnderTest.GetNumberOfCriticalIssues()
// assert
assert.Error(t, err)
assert.Equal(t, -1, count)
assert.Equal(t, 1, httpmock.GetTotalCallCount(), "unexpected number of requests")
})
t.Run("", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
sender := &piperhttp.Client{}
sender.SetOptions(piperhttp.ClientOptions{UseDefaultTransport: true})
// add response handler
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointIssuesSearch+"", httpmock.NewStringResponder(http.StatusOK, responseIssueSearchCritical))
// create service instance
serviceUnderTest := NewIssuesService(testURL, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, sender)
// test
countMajor, err := serviceUnderTest.GetNumberOfMajorIssues()
countMinor, err := serviceUnderTest.GetNumberOfMinorIssues()
countInfo, err := serviceUnderTest.GetNumberOfInfoIssues()
// assert
assert.NoError(t, err)
assert.Equal(t, 111, countMajor)
assert.Equal(t, 111, countMinor)
assert.Equal(t, 111, countInfo)
assert.Equal(t, 3, httpmock.GetTotalCallCount(), "unexpected number of requests")
})
}
const responseIssueSearchError = `{
"errors": [
{
"msg": "At least one of the following parameters must be specified: organization, projects, projectKeys (deprecated), componentKeys, componentUuids (deprecated), assignees, issues"
}
]
}`
const responseIssueSearchCritical = `{
"total": 111,
"p": 1,
"ps": 1,
"paging": {
"pageIndex": 1,
"pageSize": 1,
"total": 111
},
"effortTotal": 1176,
"debtTotal": 1176,
"issues": [
{
"key": "AXW3MmCVOYWf3_DBLGvL",
"rule": "go:S3776",
"severity": "CRITICAL",
"component": "SAP_jenkins-library:cmd/fortifyExecuteScan.go",
"project": "SAP_jenkins-library",
"line": 647,
"hash": "a154a51bdb1502a2ac057a348d08e7f6",
"textRange": {
"startLine": 647,
"endLine": 647,
"startOffset": 5,
"endOffset": 23
},
"flows": [
{
"locations": [
{
"component": "SAP_jenkins-library:cmd/fortifyExecuteScan.go",
"textRange": {
"startLine": 651,
"endLine": 651,
"startOffset": 1,
"endOffset": 3
},
"msg": "+1"
}
]
}
],
"status": "OPEN",
"message": "Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.",
"effort": "6min",
"debt": "6min",
"assignee": "CCFenner@github",
"author": "33484802+olivernocon@users.noreply.github.com",
"tags": [],
"creationDate": "2020-11-11T11:06:04+0100",
"updateDate": "2020-11-11T11:06:04+0100",
"type": "CODE_SMELL",
"organization": "sap-1"
}
],
"components": [
{
"organization": "sap-1",
"key": "SAP_jenkins-library:cmd/fortifyExecuteScan.go",
"uuid": "AXVKXJIlrkwsFznOfAie",
"enabled": true,
"qualifier": "FIL",
"name": "fortifyExecuteScan.go",
"longName": "cmd/fortifyExecuteScan.go",
"path": "cmd/fortifyExecuteScan.go"
},
{
"organization": "sap-1",
"key": "SAP_jenkins-library",
"uuid": "AXVFg_8dh6o1O3pu_MCx",
"enabled": true,
"qualifier": "TRK",
"name": "jenkins-library",
"longName": "jenkins-library"
}
],
"organizations": [
{
"key": "sap-1",
"name": "SAP"
}
],
"facets": []
}`

99
pkg/sonar/taskService.go Normal file
View File

@ -0,0 +1,99 @@
package sonar
import (
"net/http"
"time"
sonargo "github.com/magicsong/sonargo/sonar"
"github.com/SAP/jenkins-library/pkg/log"
)
// EndpointCeTask API endpoint for https://sonarcloud.io/web_api/api/ce/task
const EndpointCeTask = "ce/task"
const (
taskStatusSuccess = "SUCCESS"
taskStatusFailed = "FAILED"
taskStatusCanceled = "CANCELED"
taskStatusPending = "PENDING"
taskStatusProcessing = "IN_PROGRESS"
)
// TaskService ...
type TaskService struct {
TaskID string
PollInterval time.Duration
apiClient *Requester
}
// GetTask ...
func (service *TaskService) GetTask(options *sonargo.CeTaskOption) (*sonargo.CeTaskObject, *http.Response, error) {
request, err := service.apiClient.create("GET", EndpointCeTask, options)
if err != nil {
return nil, nil, err
}
// use custom HTTP client to send request
response, err := service.apiClient.send(request)
if response == nil && err != nil {
return nil, nil, err
}
// reuse response verrification from sonargo
err = sonargo.CheckResponse(response)
if err != nil {
return nil, response, err
}
// decode JSON response
result := new(sonargo.CeTaskObject)
err = service.apiClient.decode(response, result)
if err != nil {
return nil, response, err
}
return result, response, nil
}
// HasFinished ...
func (service *TaskService) HasFinished() (bool, error) {
options := &sonargo.CeTaskOption{
Id: service.TaskID,
AdditionalFields: "warnings",
}
result, _, err := service.GetTask(options)
if err != nil {
return false, err
}
if result.Task.Status == taskStatusPending || result.Task.Status == taskStatusProcessing {
return false, nil
}
for _, warning := range result.Task.Warnings {
log.Entry().Warnf("Warnings during analysis: %s", warning)
}
return true, nil
}
// WaitForTask ..
func (service *TaskService) WaitForTask() error {
log.Entry().Info("waiting for SonarQube task to complete..")
finished, err := service.HasFinished()
if err != nil {
return err
}
for !finished {
time.Sleep(service.PollInterval)
finished, err = service.HasFinished()
if err != nil {
return err
}
}
log.Entry().Info("finished.")
return nil
}
// NewTaskService returns a new instance of a service for the task API endpoint.
func NewTaskService(host, token, task string, client Sender) *TaskService {
return &TaskService{
TaskID: task,
PollInterval: 15 * time.Second,
apiClient: NewAPIClient(host, token, client),
}
}

View File

@ -0,0 +1,231 @@
package sonar
import (
"errors"
"net/http"
"testing"
"time"
"github.com/jarcoal/httpmock"
sonargo "github.com/magicsong/sonargo/sonar"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
)
func TestGetTask(t *testing.T) {
testURL := "https://example.org"
t.Run("success", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
sender := &piperhttp.Client{}
sender.SetOptions(piperhttp.ClientOptions{UseDefaultTransport: true})
// add response handler
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointCeTask+"", httpmock.NewStringResponder(http.StatusOK, responseCeTaskSuccess))
// create service instance
serviceUnderTest := NewTaskService(testURL, mock.Anything, mock.Anything, sender)
// test
result, response, err := serviceUnderTest.GetTask(&sonargo.CeTaskOption{Id: mock.Anything})
// assert
assert.NoError(t, err)
assert.NotEmpty(t, result)
assert.NotEmpty(t, response)
assert.Equal(t, 1, httpmock.GetTotalCallCount(), "unexpected number of requests")
})
t.Run("request error", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
sender := &piperhttp.Client{}
sender.SetOptions(piperhttp.ClientOptions{UseDefaultTransport: true})
// add response handler
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointCeTask+"", httpmock.NewErrorResponder(errors.New("internal server error")))
// create service instance
serviceUnderTest := NewTaskService(testURL, mock.Anything, mock.Anything, sender)
// test
result, response, err := serviceUnderTest.GetTask(&sonargo.CeTaskOption{Id: mock.Anything})
// assert
assert.Error(t, err)
assert.Contains(t, err.Error(), "internal server error")
assert.Empty(t, result)
assert.Empty(t, response)
assert.Equal(t, 1, httpmock.GetTotalCallCount(), "unexpected number of requests")
})
t.Run("server error", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
sender := &piperhttp.Client{}
sender.SetOptions(piperhttp.ClientOptions{UseDefaultTransport: true})
// add response handler
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointCeTask+"", httpmock.NewStringResponder(http.StatusNotFound, responseCeTaskError))
// create service instance
serviceUnderTest := NewTaskService(testURL, mock.Anything, mock.Anything, sender)
// test
result, response, err := serviceUnderTest.GetTask(&sonargo.CeTaskOption{Id: mock.Anything})
// assert
assert.Error(t, err)
assert.Contains(t, err.Error(), "No activity found for task ")
assert.Empty(t, result)
assert.NotEmpty(t, response)
assert.Equal(t, 1, httpmock.GetTotalCallCount(), "unexpected number of requests")
})
}
func TestWaitForTask(t *testing.T) {
testURL := "https://example.org"
t.Run("success", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
sender := &piperhttp.Client{}
sender.SetOptions(piperhttp.ClientOptions{UseDefaultTransport: true})
// add response handler
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointCeTask+"", httpmock.ResponderFromMultipleResponses(
[]*http.Response{
httpmock.NewStringResponse(http.StatusOK, responseCeTaskPending),
httpmock.NewStringResponse(http.StatusOK, responseCeTaskProcessing),
httpmock.NewStringResponse(http.StatusOK, responseCeTaskSuccess),
},
))
// create service instance
serviceUnderTest := NewTaskService(testURL, mock.Anything, mock.Anything, sender)
serviceUnderTest.PollInterval = time.Millisecond
// test
err := serviceUnderTest.WaitForTask()
// assert
assert.NoError(t, err)
assert.Equal(t, 3, httpmock.GetTotalCallCount(), "unexpected number of requests")
})
t.Run("failure", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
sender := &piperhttp.Client{}
sender.SetOptions(piperhttp.ClientOptions{UseDefaultTransport: true})
// add response handler
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointCeTask+"", httpmock.ResponderFromMultipleResponses(
[]*http.Response{
httpmock.NewStringResponse(http.StatusOK, responseCeTaskPending),
httpmock.NewStringResponse(http.StatusOK, responseCeTaskProcessing),
httpmock.NewStringResponse(http.StatusNotFound, responseCeTaskFailure),
},
))
// create service instance
serviceUnderTest := NewTaskService(testURL, mock.Anything, mock.Anything, sender)
serviceUnderTest.PollInterval = time.Millisecond
// test
err := serviceUnderTest.WaitForTask()
// assert
assert.Error(t, err)
assert.Contains(t, err.Error(), "status: FAILED")
assert.Equal(t, 3, httpmock.GetTotalCallCount(), "unexpected number of requests")
})
}
const responseCeTaskError = `{
"errors": [
{
"msg": "No activity found for task 'AXDj5ZWQ_ZJrW2xGuBWl'"
}
]
}`
const responseCeTaskPending = `{
"task": {
"id": "AXe5y_ZMPMpzvP5DRxw_",
"type": "REPORT",
"componentId": "AW8jANn5v4pDRYwyZIiM",
"componentKey": "Piper-Validation/Golang",
"componentName": "Piper-Validation: Golang",
"componentQualifier": "TRK",
"analysisId": "AXe5y_mgcqEbAZBpFc0V",
"status": "PENDING",
"submittedAt": "2021-02-19T10:18:07+0000",
"submitterLogin": "CCFenner",
"startedAt": "2021-02-19T10:18:08+0000",
"executedAt": "2021-02-19T10:18:09+0000",
"executionTimeMs": 551,
"logs": false,
"hasScannerContext": true,
"organization": "default-organization",
"warningCount": 1,
"warnings": []
}
}`
const responseCeTaskProcessing = `{
"task": {
"id": "AXe5y_ZMPMpzvP5DRxw_",
"type": "REPORT",
"componentId": "AW8jANn5v4pDRYwyZIiM",
"componentKey": "Piper-Validation/Golang",
"componentName": "Piper-Validation: Golang",
"componentQualifier": "TRK",
"analysisId": "AXe5y_mgcqEbAZBpFc0V",
"status": "IN_PROGRESS",
"submittedAt": "2021-02-19T10:18:07+0000",
"submitterLogin": "CCFenner",
"startedAt": "2021-02-19T10:18:08+0000",
"executedAt": "2021-02-19T10:18:09+0000",
"executionTimeMs": 551,
"logs": false,
"hasScannerContext": true,
"organization": "default-organization",
"warningCount": 1,
"warnings": []
}
}`
const responseCeTaskSuccess = `{
"task": {
"id": "AXe5y_ZMPMpzvP5DRxw_",
"type": "REPORT",
"componentId": "AW8jANn5v4pDRYwyZIiM",
"componentKey": "Piper-Validation/Golang",
"componentName": "Piper-Validation: Golang",
"componentQualifier": "TRK",
"analysisId": "AXe5y_mgcqEbAZBpFc0V",
"status": "SUCCESS",
"submittedAt": "2021-02-19T10:18:07+0000",
"submitterLogin": "CCFenner",
"startedAt": "2021-02-19T10:18:08+0000",
"executedAt": "2021-02-19T10:18:09+0000",
"executionTimeMs": 551,
"logs": false,
"hasScannerContext": true,
"organization": "default-organization",
"warningCount": 1,
"warnings": [
"The project key ‘Piper-Validation/Golang’ contains invalid characters. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit. You should update the project key with the expected format."
]
}
}`
const responseCeTaskFailure = `{
"task": {
"organization": "my-org-1",
"id": "AVAn5RKqYwETbXvgas-I",
"type": "REPORT",
"componentId": "AVAn5RJmYwETbXvgas-H",
"componentKey": "project_1",
"componentName": "Project One",
"componentQualifier": "TRK",
"analysisId": "123456",
"status": "FAILED",
"submittedAt": "2015-10-02T11:32:15+0200",
"startedAt": "2015-10-02T11:32:16+0200",
"executedAt": "2015-10-02T11:32:22+0200",
"executionTimeMs": 5286,
"errorMessage": "Fail to extract report AVaXuGAi_te3Ldc_YItm from database",
"logs": false,
"hasErrorStacktrace": true,
"errorStacktrace": "java.lang.IllegalStateException: Fail to extract report AVaXuGAi_te3Ldc_YItm from database\n\tat org.sonar.server.computation.task.projectanalysis.step.ExtractReportStep.execute(ExtractReportStep.java:50)",
"scannerContext": "SonarQube plugins:\n\t- Git 1.0 (scmgit)\n\t- Java 3.13.1 (java)",
"hasScannerContext": true
}
}`

50
pkg/sonar/types.go Normal file
View File

@ -0,0 +1,50 @@
package sonar
// IssuesSearchOption is a copy from magicsong/sonargo plus the "internal" fields organization, branch and pullrequest.
type IssuesSearchOption struct {
Branch string `url:"branch,omitempty"` // Description:"Branch key"
Organization string `url:"organization,omitempty"` // Description:"Organization key"
PullRequest string `url:"pullRequest,omitempty"` // Description:"Pull request id"
// copied from https://github.com/magicsong/sonargo/blob/103eda7abc20bd192a064b6eb94ba26329e339f1/sonar/issues_service.go#L311
AdditionalFields string `url:"additionalFields,omitempty"` // Description:"Comma-separated list of the optional fields to be returned in response. Action plans are dropped in 5.5, it is not returned in the response.",ExampleValue:""
Asc string `url:"asc,omitempty"` // Description:"Ascending sort",ExampleValue:""
Assigned string `url:"assigned,omitempty"` // Description:"To retrieve assigned or unassigned issues",ExampleValue:""
Assignees string `url:"assignees,omitempty"` // Description:"Comma-separated list of assignee logins. The value '__me__' can be used as a placeholder for user who performs the request",ExampleValue:"admin,usera,__me__"
Authors string `url:"authors,omitempty"` // Description:"Comma-separated list of SCM accounts",ExampleValue:"torvalds@linux-foundation.org"
ComponentKeys string `url:"componentKeys,omitempty"` // Description:"Comma-separated list of component keys. Retrieve issues associated to a specific list of components (and all its descendants). A component can be a portfolio, project, module, directory or file.",ExampleValue:"my_project"
ComponentRootUuids string `url:"componentRootUuids,omitempty"` // Description:"If used, will have the same meaning as componentUuids AND onComponentOnly=false.",ExampleValue:""
ComponentRoots string `url:"componentRoots,omitempty"` // Description:"If used, will have the same meaning as componentKeys AND onComponentOnly=false.",ExampleValue:""
ComponentUuids string `url:"componentUuids,omitempty"` // Description:"To retrieve issues associated to a specific list of components their sub-components (comma-separated list of component IDs). This parameter is mostly used by the Issues page, please prefer usage of the componentKeys parameter. A component can be a project, module, directory or file.",ExampleValue:"584a89f2-8037-4f7b-b82c-8b45d2d63fb2"
Components string `url:"components,omitempty"` // Description:"If used, will have the same meaning as componentKeys AND onComponentOnly=true.",ExampleValue:""
CreatedAfter string `url:"createdAfter,omitempty"` // Description:"To retrieve issues created after the given date (inclusive). <br>Either a date (server timezone) or datetime can be provided. <br>If this parameter is set, createdSince must not be set",ExampleValue:"2017-10-19 or 2017-10-19T13:00:00+0200"
CreatedAt string `url:"createdAt,omitempty"` // Description:"Datetime to retrieve issues created during a specific analysis",ExampleValue:"2017-10-19T13:00:00+0200"
CreatedBefore string `url:"createdBefore,omitempty"` // Description:"To retrieve issues created before the given date (inclusive). <br>Either a date (server timezone) or datetime can be provided.",ExampleValue:"2017-10-19 or 2017-10-19T13:00:00+0200"
CreatedInLast string `url:"createdInLast,omitempty"` // Description:"To retrieve issues created during a time span before the current time (exclusive). Accepted units are 'y' for year, 'm' for month, 'w' for week and 'd' for day. If this parameter is set, createdAfter must not be set",ExampleValue:"1m2w (1 month 2 weeks)"
Issues string `url:"issues,omitempty"` // Description:"Comma-separated list of issue keys",ExampleValue:"5bccd6e8-f525-43a2-8d76-fcb13dde79ef"
Languages string `url:"languages,omitempty"` // Description:"Comma-separated list of languages. Available since 4.4",ExampleValue:"java,js"
P string `url:"p,omitempty"` // Description:"1-based page number",ExampleValue:"42"
Ps string `url:"ps,omitempty"` // Description:"Page size. Must be greater than 0 and less or equal than 500",ExampleValue:"20"
Resolutions string `url:"resolutions,omitempty"` // Description:"Comma-separated list of resolutions",ExampleValue:"FIXED,REMOVED"
Resolved string `url:"resolved,omitempty"` // Description:"To match resolved or unresolved issues",ExampleValue:""
Rules string `url:"rules,omitempty"` // Description:"Comma-separated list of coding rule keys. Format is &lt;repository&gt;:&lt;rule&gt;",ExampleValue:"squid:AvoidCycles"
S string `url:"s,omitempty"` // Description:"Sort field",ExampleValue:""
Severities string `url:"severities,omitempty"` // Description:"Comma-separated list of severities",ExampleValue:"BLOCKER,CRITICAL"
SinceLeakPeriod string `url:"sinceLeakPeriod,omitempty"` // Description:"To retrieve issues created since the leak period.<br>If this parameter is set to a truthy value, createdAfter must not be set and one component id or key must be provided.",ExampleValue:""
Statuses string `url:"statuses,omitempty"` // Description:"Comma-separated list of statuses",ExampleValue:"OPEN,REOPENED"
Tags string `url:"tags,omitempty"` // Description:"Comma-separated list of tags.",ExampleValue:"security,convention"
Types string `url:"types,omitempty"` // Description:"Comma-separated list of types.",ExampleValue:"CODE_SMELL,BUG"
}
type issueSeverity string
func (s issueSeverity) ToString() string {
return string(s)
}
const (
blocker issueSeverity = "BLOCKER"
critical issueSeverity = "CRITICAL"
major issueSeverity = "MAJOR"
minor issueSeverity = "MINOR"
info issueSeverity = "INFO"
)

View File

@ -259,6 +259,18 @@ spec:
fields:
- name: sonar
type: bool
- name: sonarqube_data
fields:
- name: blocker_issues
type: int
- name: critical_issues
type: int
- name: major_issues
type: int
- name: minor_issues
type: int
- name: info_issues
type: int
containers:
- name: sonar
image: sonarsource/sonar-scanner-cli:4.5