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:
parent
fbbb55471d
commit
cb3fa7c293
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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
4
go.mod
@ -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
12
go.sum
@ -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=
|
||||
|
@ -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")
|
||||
|
@ -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
73
pkg/sonar/client.go
Normal 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
49
pkg/sonar/client_test.go
Normal 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
104
pkg/sonar/issueService.go
Normal 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),
|
||||
}
|
||||
}
|
164
pkg/sonar/issueService_test.go
Normal file
164
pkg/sonar/issueService_test.go
Normal 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
99
pkg/sonar/taskService.go
Normal 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),
|
||||
}
|
||||
}
|
231
pkg/sonar/taskService_test.go
Normal file
231
pkg/sonar/taskService_test.go
Normal 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
50
pkg/sonar/types.go
Normal 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 <repository>:<rule>",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"
|
||||
)
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user