From dbb4f6bdf8974a7887e8f2f14067bd1586cef8cb Mon Sep 17 00:00:00 2001 From: Michael Sauter Date: Thu, 7 Oct 2021 08:42:17 +0200 Subject: [PATCH] Harden SonarQube integration * Do not generate reports when pull request exists for scanned branch. For more information, see https://github.com/opendevstack/ods-jenkins-shared-library/issues/663 and https://github.com/cnescatlab/sonar-cnes-report/issues/159. * Ensure background task on server finishes before generating a report. For more information, see https://github.com/opendevstack/ods-jenkins-shared-library/issues/732. * Unify logging approach: instead of printing to STDOUT directly for some messages, funnel everything through the logger instance. Other tasks should adopt this as well. Closes #227. --- cmd/sonar/main.go | 134 +++++++++++---- cmd/sonar/main_test.go | 152 ++++++++++++++++++ .../task-ods-build-go-with-sidecar.yaml | 6 +- .../templates/task-ods-build-go.yaml | 6 +- .../task-ods-build-gradle-with-sidecar.yaml | 9 +- .../templates/task-ods-build-gradle.yaml | 7 +- .../task-ods-build-python-with-sidecar.yaml | 10 +- .../templates/task-ods-build-python.yaml | 10 +- ...ask-ods-build-typescript-with-sidecar.yaml | 10 +- .../templates/task-ods-build-typescript.yaml | 10 +- .../design/software-design-specification.adoc | 2 +- .../software-requirements-specification.adoc | 1 + docs/tasks/ods-build-go-with-sidecar.adoc | 6 +- docs/tasks/ods-build-go.adoc | 6 +- docs/tasks/ods-build-gradle-with-sidecar.adoc | 7 +- docs/tasks/ods-build-gradle.adoc | 7 +- docs/tasks/ods-build-python-with-sidecar.adoc | 10 +- docs/tasks/ods-build-python.adoc | 10 +- .../ods-build-typescript-with-sidecar.adoc | 10 +- docs/tasks/ods-build-typescript.adoc | 10 +- pkg/sonar/client.go | 8 + pkg/sonar/compute_engine.go | 61 +++++++ pkg/sonar/compute_engine_test.go | 57 +++++++ pkg/sonar/scan.go | 57 +++++++ pkg/sonar/scan_test.go | 24 +++ test/tasks/ods-build-go_test.go | 19 +++ test/testdata/fixtures/sonar/report-task.txt | 7 + test/testdata/fixtures/sonar/task_failed.json | 23 +++ .../testdata/fixtures/sonar/task_success.json | 23 +++ 29 files changed, 631 insertions(+), 71 deletions(-) create mode 100644 cmd/sonar/main_test.go create mode 100644 pkg/sonar/compute_engine.go create mode 100644 pkg/sonar/compute_engine_test.go create mode 100644 pkg/sonar/scan_test.go create mode 100644 test/testdata/fixtures/sonar/report-task.txt create mode 100644 test/testdata/fixtures/sonar/task_failed.json create mode 100644 test/testdata/fixtures/sonar/task_success.json diff --git a/cmd/sonar/main.go b/cmd/sonar/main.go index 9671ce4e..96b6f12f 100644 --- a/cmd/sonar/main.go +++ b/cmd/sonar/main.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/opendevstack/pipeline/pkg/logging" "github.com/opendevstack/pipeline/pkg/pipelinectxt" @@ -18,12 +19,18 @@ type options struct { sonarURL string sonarEdition string workingDir string + rootPath string qualityGate bool debug bool } func main() { - opts := options{} + rootPath, err := filepath.Abs(".") + if err != nil { + log.Fatal(err) + } + + opts := options{rootPath: rootPath} flag.StringVar(&opts.sonarAuthToken, "sonar-auth-token", os.Getenv("SONAR_AUTH_TOKEN"), "sonar-auth-token") flag.StringVar(&opts.sonarURL, "sonar-url", os.Getenv("SONAR_URL"), "sonar-url") flag.StringVar(&opts.sonarEdition, "sonar-edition", os.Getenv("SONAR_EDITION"), "sonar-edition") @@ -38,22 +45,15 @@ func main() { } ctxt := &pipelinectxt.ODSContext{} - err := ctxt.ReadCache(".") - if err != nil { - log.Fatal(err) - } - rootPath, err := filepath.Abs(".") + err = ctxt.ReadCache(".") if err != nil { log.Fatal(err) } + err = os.Chdir(opts.workingDir) if err != nil { log.Fatal(err) } - artifactPrefix := "" - if opts.workingDir != "." { - artifactPrefix = strings.Replace(opts.workingDir, "/", "-", -1) + "-" - } sonarClient := sonar.NewClient(&sonar.ClientConfig{ APIToken: opts.sonarAuthToken, @@ -63,61 +63,129 @@ func main() { Logger: logger, }) + err = sonarScan(logger, opts, ctxt, sonarClient) + if err != nil { + log.Fatal(err) + } +} + +func sonarScan( + logger logging.LeveledLoggerInterface, + opts options, + ctxt *pipelinectxt.ODSContext, + sonarClient sonar.ClientInterface) error { + artifactPrefix := "" + if opts.workingDir != "." { + artifactPrefix = strings.Replace(opts.workingDir, "/", "-", -1) + "-" + } + sonarProject := sonar.ProjectKey(ctxt, artifactPrefix) - fmt.Println("Scanning with sonar-scanner ...") + logger.Infof("Scanning with sonar-scanner ...\n") var prInfo *sonar.PullRequest if len(ctxt.PullRequestKey) > 0 && ctxt.PullRequestKey != "0" && len(ctxt.PullRequestBase) > 0 { + logger.Infof("Pull request (ID %s) detected.\n", ctxt.PullRequestKey) prInfo = &sonar.PullRequest{ Key: ctxt.PullRequestKey, Branch: ctxt.GitRef, Base: ctxt.PullRequestBase, } } - stdout, err := sonarClient.Scan( + scanStdout, err := sonarClient.Scan( sonarProject, ctxt.GitRef, ctxt.GitCommitSHA, prInfo, ) if err != nil { - fmt.Println(stdout) - fmt.Println(err) - os.Exit(1) + logger.Infof("%s\n", scanStdout) + return fmt.Errorf("scan failed: %w", err) } - fmt.Println(stdout) + logger.Infof("%s\n", scanStdout) - fmt.Println("Generating reports ...") - stdout, err = sonarClient.GenerateReports( - sonarProject, - "OpenDevStack", - ctxt.GitRef, - rootPath, - artifactPrefix, - ) + logger.Infof("Wait until compute engine task finishes ...") + err = waitUntilComputeEngineTaskIsSuccessful(logger, sonarClient) if err != nil { - fmt.Println(stdout) - fmt.Println(err) - os.Exit(1) + return fmt.Errorf("background task did not finish successfully: %w", err) + } + + if prInfo == nil { + logger.Infof("Generating reports ...\n") + reportStdout, err := sonarClient.GenerateReports( + sonarProject, + "OpenDevStack", + ctxt.GitRef, + opts.rootPath, + artifactPrefix, + ) + if err != nil { + logger.Infof("%s\n", reportStdout) + logger.Infof("%s\n", err) + os.Exit(1) + } + logger.Infof("%s\n", reportStdout) + } else { + logger.Infof("No reports are generated for pull request scans.\n") } - fmt.Println(stdout) if opts.qualityGate { - fmt.Println("Checking quality gate ...") + logger.Infof("Checking quality gate ...\n") qualityGateResult, err := sonarClient.QualityGateGet( sonar.QualityGateGetParams{Project: sonarProject}, ) if err != nil { - log.Fatalln(err) + return fmt.Errorf("quality gate could not be retrieved: %w", err) } actualStatus := qualityGateResult.ProjectStatus.Status if actualStatus != sonar.QualityGateStatusOk { - log.Fatalf( - "Quality gate status is '%s', not '%s'\n", + return fmt.Errorf( + "quality gate status is '%s', not '%s'", actualStatus, sonar.QualityGateStatusOk, ) } else { - fmt.Println("Quality gate passed") + logger.Infof("Quality gate passed.\n") + } + } + + return nil +} + +// waitUntilComputeEngineTaskIsSuccessful reads the scanner report file and +// extracts the task ID. It then waits until the corresponding background task +// in SonarQube succeeds. If the tasks fails or the timeout is reached, an +// error is returned. +func waitUntilComputeEngineTaskIsSuccessful(logger logging.LeveledLoggerInterface, sonarClient sonar.ClientInterface) error { + reportTaskID, err := sonarClient.ExtractComputeEngineTaskID(sonar.ReportTaskFile) + if err != nil { + return fmt.Errorf("cannot read task ID: %w", err) + } + params := sonar.ComputeEngineTaskGetParams{ID: reportTaskID} + attempts := 5 + sleep := time.Second + for i := 0; i < attempts; i++ { + if i > 0 { + logger.Infof("Waiting %s before checking task status again ...", sleep) + time.Sleep(sleep) + sleep *= 2 + } + task, err := sonarClient.ComputeEngineTaskGet(params) + if err != nil { + logger.Infof("cannot get status of task: %w", err) + continue + } + switch task.Status { + case sonar.TaskStatusInProgress: + logger.Infof("Background task %s has not finished yet", reportTaskID) + case sonar.TaskStatusPending: + logger.Infof("Background task %s has not started yet", reportTaskID) + case sonar.TaskStatusFailed: + return fmt.Errorf("background task %s has failed", reportTaskID) + case sonar.TaskStatusSuccess: + logger.Infof("Background task %s has finished successfully", reportTaskID) + return nil + default: + logger.Infof("Background task %s has unknown status %s", reportTaskID, task.Status) } } + return fmt.Errorf("background task %s did not succeed within timeout", reportTaskID) } diff --git a/cmd/sonar/main_test.go b/cmd/sonar/main_test.go new file mode 100644 index 00000000..6e70aa69 --- /dev/null +++ b/cmd/sonar/main_test.go @@ -0,0 +1,152 @@ +package main + +import ( + "strings" + "testing" + + "github.com/opendevstack/pipeline/pkg/logging" + "github.com/opendevstack/pipeline/pkg/pipelinectxt" + "github.com/opendevstack/pipeline/pkg/sonar" +) + +type fakeClient struct { + scanPerformed bool + passQualityGate bool + qualityGateRetrieved bool + reportGenerated bool +} + +func (c *fakeClient) Scan(sonarProject, branch, commit string, pr *sonar.PullRequest) (string, error) { + c.scanPerformed = true + return "", nil +} + +func (c *fakeClient) QualityGateGet(p sonar.QualityGateGetParams) (*sonar.QualityGate, error) { + c.qualityGateRetrieved = true + status := sonar.QualityGateStatusError + if c.passQualityGate { + status = sonar.QualityGateStatusOk + } + return &sonar.QualityGate{ProjectStatus: sonar.QualityGateProjectStatus{Status: status}}, nil +} + +func (c *fakeClient) GenerateReports(sonarProject, author, branch, rootPath, artifactPrefix string) (string, error) { + c.reportGenerated = true + return "", nil +} + +func (c *fakeClient) ExtractComputeEngineTaskID(filename string) (string, error) { + return "abc123", nil +} + +func (c *fakeClient) ComputeEngineTaskGet(params sonar.ComputeEngineTaskGetParams) (*sonar.ComputeEngineTask, error) { + return &sonar.ComputeEngineTask{Status: sonar.TaskStatusSuccess}, nil +} + +func TestSonarScan(t *testing.T) { + logger := &logging.LeveledLogger{Level: logging.LevelDebug} + + tests := map[string]struct { + // which SQ edition is in use + optSonarEdition string + // whether quality gate is required to pass + optQualityGate bool + + // PR key + ctxtPrKey string + // PR base + ctxtPrBase string + + // whether the quality gate in SQ passes (faked) + passQualityGate bool + + // whether scan should have been performed + wantScanPerformed bool + // whether report should have been generated + wantReportGenerated bool + // whether quality gate should have been retrieved + wantQualityGateRetrieved bool + // whether scanning should fail - if not empty, the actual error message + // will be checked to contain wantErr. + wantErr string + }{ + "developer edition generates report when no PR is present": { + optSonarEdition: "developer", + optQualityGate: true, + ctxtPrKey: "", + ctxtPrBase: "", + passQualityGate: true, + wantScanPerformed: true, + wantReportGenerated: true, + wantQualityGateRetrieved: true, + }, + "developer edition does not generate report when PR is present": { + optSonarEdition: "developer", + optQualityGate: true, + ctxtPrKey: "3", + ctxtPrBase: "master", + passQualityGate: true, + wantScanPerformed: true, + wantReportGenerated: false, + wantQualityGateRetrieved: true, + }, + "community edition generates report": { + optSonarEdition: "community", + optQualityGate: true, + ctxtPrKey: "", + ctxtPrBase: "", + passQualityGate: true, + wantScanPerformed: true, + wantReportGenerated: true, + wantQualityGateRetrieved: true, + }, + "does not check quality gate if disabled": { + optSonarEdition: "community", + optQualityGate: false, + ctxtPrKey: "", + ctxtPrBase: "", + passQualityGate: true, + wantScanPerformed: true, + wantReportGenerated: true, + wantQualityGateRetrieved: false, + }, + "fails if quality gate does not pass": { + optSonarEdition: "community", + optQualityGate: true, + ctxtPrKey: "", + ctxtPrBase: "", + passQualityGate: false, + wantScanPerformed: true, + wantReportGenerated: true, + wantQualityGateRetrieved: true, + wantErr: "quality gate status is 'ERROR', not 'OK'", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + opts := options{ + sonarEdition: tc.optSonarEdition, + qualityGate: tc.optQualityGate, + } + ctxt := &pipelinectxt.ODSContext{PullRequestKey: tc.ctxtPrKey, PullRequestBase: tc.ctxtPrBase} + sonarClient := &fakeClient{passQualityGate: tc.passQualityGate} + err := sonarScan(logger, opts, ctxt, sonarClient) + if err != nil { + if tc.wantErr == "" || !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("want err to contain: %s, got err: %s", tc.wantErr, err) + } + } + if sonarClient.scanPerformed != tc.wantScanPerformed { + t.Fatalf("want scan performed to be %v, got %v", tc.wantScanPerformed, sonarClient.scanPerformed) + } + if sonarClient.reportGenerated != tc.wantReportGenerated { + t.Fatalf("want report generated to be %v, got %v", tc.wantReportGenerated, sonarClient.reportGenerated) + } + if sonarClient.qualityGateRetrieved != tc.wantQualityGateRetrieved { + t.Fatalf("want quality gate retrieved to be %v, got %v", tc.wantQualityGateRetrieved, sonarClient.qualityGateRetrieved) + } + }) + } + +} diff --git a/deploy/central/tasks-chart/templates/task-ods-build-go-with-sidecar.yaml b/deploy/central/tasks-chart/templates/task-ods-build-go-with-sidecar.yaml index 0a2c3c9d..acf60c1e 100644 --- a/deploy/central/tasks-chart/templates/task-ods-build-go-with-sidecar.yaml +++ b/deploy/central/tasks-chart/templates/task-ods-build-go-with-sidecar.yaml @@ -25,9 +25,9 @@ spec: When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. The SonarQube scan will include parameters to perform a pull request analysis if - there is an open pull request for the branch being built. Pull request decoration - in Bitbucket is done automatically by SonarQube provided the ALM integration is setup - properly in SonarQube. + there is an open pull request for the branch being built. If the + link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] + is setup properly, pull request decoration in Bitbucket is done automatically. **Sidecar variant!** Use this task if you need to run a container next to the build task. For example, this could be used to run a database to allow for integration tests. diff --git a/deploy/central/tasks-chart/templates/task-ods-build-go.yaml b/deploy/central/tasks-chart/templates/task-ods-build-go.yaml index 87c67f59..f7bfca63 100644 --- a/deploy/central/tasks-chart/templates/task-ods-build-go.yaml +++ b/deploy/central/tasks-chart/templates/task-ods-build-go.yaml @@ -23,9 +23,9 @@ spec: When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. The SonarQube scan will include parameters to perform a pull request analysis if - there is an open pull request for the branch being built. Pull request decoration - in Bitbucket is done automatically by SonarQube provided the ALM integration is setup - properly in SonarQube. + there is an open pull request for the branch being built. If the + link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] + is setup properly, pull request decoration in Bitbucket is done automatically. params: - name: working-dir description: | diff --git a/deploy/central/tasks-chart/templates/task-ods-build-gradle-with-sidecar.yaml b/deploy/central/tasks-chart/templates/task-ods-build-gradle-with-sidecar.yaml index 8d67359f..c0b0ed7a 100644 --- a/deploy/central/tasks-chart/templates/task-ods-build-gradle-with-sidecar.yaml +++ b/deploy/central/tasks-chart/templates/task-ods-build-gradle-with-sidecar.yaml @@ -5,8 +5,7 @@ metadata: creationTimestamp: null name: ods-build-gradle-with-sidecar{{.Values.taskSuffix}} spec: - description: |2- - + description: |- Builds Gradle applications. The following steps are executed: @@ -89,9 +88,9 @@ spec: When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. The SonarQube scan will include parameters to perform a pull request analysis if - there is an open pull request for the branch being built. Pull request decoration - in Bitbucket is done automatically by SonarQube provided the ALM integration is setup - properly in SonarQube. + there is an open pull request for the branch being built. If the + link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] + is setup properly, pull request decoration in Bitbucket is done automatically. **Sidecar variant!** Use this task if you need to run a container next to the build task. For example, this could be used to run a database to allow for integration tests. diff --git a/deploy/central/tasks-chart/templates/task-ods-build-gradle.yaml b/deploy/central/tasks-chart/templates/task-ods-build-gradle.yaml index cf91277f..f64da2e8 100644 --- a/deploy/central/tasks-chart/templates/task-ods-build-gradle.yaml +++ b/deploy/central/tasks-chart/templates/task-ods-build-gradle.yaml @@ -4,7 +4,6 @@ metadata: name: ods-build-gradle{{.Values.taskSuffix}} spec: description: | - Builds Gradle applications. The following steps are executed: @@ -87,9 +86,9 @@ spec: When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. The SonarQube scan will include parameters to perform a pull request analysis if - there is an open pull request for the branch being built. Pull request decoration - in Bitbucket is done automatically by SonarQube provided the ALM integration is setup - properly in SonarQube. + there is an open pull request for the branch being built. If the + link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] + is setup properly, pull request decoration in Bitbucket is done automatically. params: - name: working-dir diff --git a/deploy/central/tasks-chart/templates/task-ods-build-python-with-sidecar.yaml b/deploy/central/tasks-chart/templates/task-ods-build-python-with-sidecar.yaml index 3a43b20d..7549b365 100644 --- a/deploy/central/tasks-chart/templates/task-ods-build-python-with-sidecar.yaml +++ b/deploy/central/tasks-chart/templates/task-ods-build-python-with-sidecar.yaml @@ -6,7 +6,15 @@ metadata: name: ods-build-python-with-sidecar{{.Values.taskSuffix}} spec: description: |- - ODS Build Python applications + Builds Python applications. + + When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate + is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. + The SonarQube scan will include parameters to perform a pull request analysis if + there is an open pull request for the branch being built. If the + link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] + is setup properly, pull request decoration in Bitbucket is done automatically. + **Sidecar variant!** Use this task if you need to run a container next to the build task. For example, this could be used to run a database to allow for integration tests. The sidecar image to must be supplied via `sidecar-image`. diff --git a/deploy/central/tasks-chart/templates/task-ods-build-python.yaml b/deploy/central/tasks-chart/templates/task-ods-build-python.yaml index ba8a5fd8..b37d2e7b 100644 --- a/deploy/central/tasks-chart/templates/task-ods-build-python.yaml +++ b/deploy/central/tasks-chart/templates/task-ods-build-python.yaml @@ -3,7 +3,15 @@ kind: ClusterTask metadata: name: ods-build-python{{.Values.taskSuffix}} spec: - description: ODS Build Python applications + description: | + Builds Python applications. + + When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate + is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. + The SonarQube scan will include parameters to perform a pull request analysis if + there is an open pull request for the branch being built. If the + link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] + is setup properly, pull request decoration in Bitbucket is done automatically. params: - name: working-dir description: | diff --git a/deploy/central/tasks-chart/templates/task-ods-build-typescript-with-sidecar.yaml b/deploy/central/tasks-chart/templates/task-ods-build-typescript-with-sidecar.yaml index 9bb5df55..873fda0a 100644 --- a/deploy/central/tasks-chart/templates/task-ods-build-typescript-with-sidecar.yaml +++ b/deploy/central/tasks-chart/templates/task-ods-build-typescript-with-sidecar.yaml @@ -6,7 +6,15 @@ metadata: name: ods-build-typescript-with-sidecar{{.Values.taskSuffix}} spec: description: |- - ODS Build Typescript applications + Builds Typescript applications. + + When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate + is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. + The SonarQube scan will include parameters to perform a pull request analysis if + there is an open pull request for the branch being built. If the + link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] + is setup properly, pull request decoration in Bitbucket is done automatically. + **Sidecar variant!** Use this task if you need to run a container next to the build task. For example, this could be used to run a database to allow for integration tests. The sidecar image to must be supplied via `sidecar-image`. diff --git a/deploy/central/tasks-chart/templates/task-ods-build-typescript.yaml b/deploy/central/tasks-chart/templates/task-ods-build-typescript.yaml index 4edbc344..52beb3c3 100644 --- a/deploy/central/tasks-chart/templates/task-ods-build-typescript.yaml +++ b/deploy/central/tasks-chart/templates/task-ods-build-typescript.yaml @@ -3,7 +3,15 @@ kind: ClusterTask metadata: name: ods-build-typescript{{.Values.taskSuffix}} spec: - description: ODS Build Typescript applications + description: | + Builds Typescript applications. + + When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate + is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. + The SonarQube scan will include parameters to perform a pull request analysis if + there is an open pull request for the branch being built. If the + link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] + is setup properly, pull request decoration in Bitbucket is done automatically. params: - name: working-dir description: | diff --git a/docs/design/software-design-specification.adoc b/docs/design/software-design-specification.adoc index e95b0113..daa98d6e 100644 --- a/docs/design/software-design-specification.adoc +++ b/docs/design/software-design-specification.adoc @@ -36,7 +36,7 @@ As described in the architecture, the system is split into two main containers: | SDS-SHARED-2 | `sonar` binary -a| Logic of SQ scanning. It runs `sonar-scanner` (SDS-EXT-7) on the sources, communicating with the SonarQube server specified by the `ods-sonar` config map and the `ods-sonar-auth` secret. After scanning, reports a generated using `cnes-report` (SDS-EXT-8). +a| Logic of SQ scanning. It runs `sonar-scanner` (SDS-EXT-7) on the sources, communicating with the SonarQube server specified by the `ods-sonar` config map and the `ods-sonar-auth` secret. After scanning, reports a generated using `cnes-report` (SDS-EXT-8) unless the scan is against a pull request. `cnes-report` is not compatible with PR scans, and reports are not needed for pull requests anyway as the evidence they provide is only needed for long-lived branches. The project name is fixed to `-`. diff --git a/docs/design/software-requirements-specification.adoc b/docs/design/software-requirements-specification.adoc index 3e62c19c..e567ecc9 100644 --- a/docs/design/software-requirements-specification.adoc +++ b/docs/design/software-requirements-specification.adoc @@ -257,6 +257,7 @@ a| The task shall analyze the source code statically using SonarQube. * The SQ project name shall be fixed by the task to avoid name clashes between projects. * Branch and pull request analysis shall be performed if the server edition supports it. +* Reports shall be generated unless the scan is against a pull request. | SRS-TASK-SHARED-2 a| The task shall be able to run in a subdirectory of the checked out repository. diff --git a/docs/tasks/ods-build-go-with-sidecar.adoc b/docs/tasks/ods-build-go-with-sidecar.adoc index ee9f97c5..720ad658 100644 --- a/docs/tasks/ods-build-go-with-sidecar.adoc +++ b/docs/tasks/ods-build-go-with-sidecar.adoc @@ -21,9 +21,9 @@ After tests ran successfully, the application source code is scanned by SonarQub When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. The SonarQube scan will include parameters to perform a pull request analysis if -there is an open pull request for the branch being built. Pull request decoration -in Bitbucket is done automatically by SonarQube provided the ALM integration is setup -properly in SonarQube. +there is an open pull request for the branch being built. If the +link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] +is setup properly, pull request decoration in Bitbucket is done automatically. **Sidecar variant!** Use this task if you need to run a container next to the build task. For example, this could be used to run a database to allow for integration tests. diff --git a/docs/tasks/ods-build-go.adoc b/docs/tasks/ods-build-go.adoc index 2213f462..e2add32b 100644 --- a/docs/tasks/ods-build-go.adoc +++ b/docs/tasks/ods-build-go.adoc @@ -21,9 +21,9 @@ After tests ran successfully, the application source code is scanned by SonarQub When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. The SonarQube scan will include parameters to perform a pull request analysis if -there is an open pull request for the branch being built. Pull request decoration -in Bitbucket is done automatically by SonarQube provided the ALM integration is setup -properly in SonarQube. +there is an open pull request for the branch being built. If the +link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] +is setup properly, pull request decoration in Bitbucket is done automatically. == Parameters diff --git a/docs/tasks/ods-build-gradle-with-sidecar.adoc b/docs/tasks/ods-build-gradle-with-sidecar.adoc index 571200fe..88f0985d 100644 --- a/docs/tasks/ods-build-gradle-with-sidecar.adoc +++ b/docs/tasks/ods-build-gradle-with-sidecar.adoc @@ -2,7 +2,6 @@ = ods-build-gradle-with-sidecar - Builds Gradle applications. The following steps are executed: @@ -85,9 +84,9 @@ sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport. When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. The SonarQube scan will include parameters to perform a pull request analysis if -there is an open pull request for the branch being built. Pull request decoration -in Bitbucket is done automatically by SonarQube provided the ALM integration is setup -properly in SonarQube. +there is an open pull request for the branch being built. If the +link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] +is setup properly, pull request decoration in Bitbucket is done automatically. **Sidecar variant!** Use this task if you need to run a container next to the build task. For example, this could be used to run a database to allow for integration tests. diff --git a/docs/tasks/ods-build-gradle.adoc b/docs/tasks/ods-build-gradle.adoc index 5f55e3d8..f87476fe 100644 --- a/docs/tasks/ods-build-gradle.adoc +++ b/docs/tasks/ods-build-gradle.adoc @@ -2,7 +2,6 @@ = ods-build-gradle - Builds Gradle applications. The following steps are executed: @@ -85,9 +84,9 @@ sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport. When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. The SonarQube scan will include parameters to perform a pull request analysis if -there is an open pull request for the branch being built. Pull request decoration -in Bitbucket is done automatically by SonarQube provided the ALM integration is setup -properly in SonarQube. +there is an open pull request for the branch being built. If the +link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] +is setup properly, pull request decoration in Bitbucket is done automatically. == Parameters diff --git a/docs/tasks/ods-build-python-with-sidecar.adoc b/docs/tasks/ods-build-python-with-sidecar.adoc index 23524a8c..e3cce0fa 100644 --- a/docs/tasks/ods-build-python-with-sidecar.adoc +++ b/docs/tasks/ods-build-python-with-sidecar.adoc @@ -2,7 +2,15 @@ = ods-build-python-with-sidecar -ODS Build Python applications +Builds Python applications. + +When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate +is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. +The SonarQube scan will include parameters to perform a pull request analysis if +there is an open pull request for the branch being built. If the +link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] +is setup properly, pull request decoration in Bitbucket is done automatically. + **Sidecar variant!** Use this task if you need to run a container next to the build task. For example, this could be used to run a database to allow for integration tests. The sidecar image to must be supplied via `sidecar-image`. diff --git a/docs/tasks/ods-build-python.adoc b/docs/tasks/ods-build-python.adoc index 23afb826..e18c1cc7 100644 --- a/docs/tasks/ods-build-python.adoc +++ b/docs/tasks/ods-build-python.adoc @@ -2,7 +2,15 @@ = ods-build-python -ODS Build Python applications +Builds Python applications. + +When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate +is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. +The SonarQube scan will include parameters to perform a pull request analysis if +there is an open pull request for the branch being built. If the +link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] +is setup properly, pull request decoration in Bitbucket is done automatically. + == Parameters diff --git a/docs/tasks/ods-build-typescript-with-sidecar.adoc b/docs/tasks/ods-build-typescript-with-sidecar.adoc index 67ec0713..2eadc599 100644 --- a/docs/tasks/ods-build-typescript-with-sidecar.adoc +++ b/docs/tasks/ods-build-typescript-with-sidecar.adoc @@ -2,7 +2,15 @@ = ods-build-typescript-with-sidecar -ODS Build Typescript applications +Builds Typescript applications. + +When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate +is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. +The SonarQube scan will include parameters to perform a pull request analysis if +there is an open pull request for the branch being built. If the +link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] +is setup properly, pull request decoration in Bitbucket is done automatically. + **Sidecar variant!** Use this task if you need to run a container next to the build task. For example, this could be used to run a database to allow for integration tests. The sidecar image to must be supplied via `sidecar-image`. diff --git a/docs/tasks/ods-build-typescript.adoc b/docs/tasks/ods-build-typescript.adoc index 87e0970c..c038d99a 100644 --- a/docs/tasks/ods-build-typescript.adoc +++ b/docs/tasks/ods-build-typescript.adoc @@ -2,7 +2,15 @@ = ods-build-typescript -ODS Build Typescript applications +Builds Typescript applications. + +When `sonar-quality-gate` is set to `true`, the task will fail if the quality gate +is not passed. If SonarQube is not desired, it can be disabled via `sonar-skip`. +The SonarQube scan will include parameters to perform a pull request analysis if +there is an open pull request for the branch being built. If the +link:https://docs.sonarqube.org/latest/analysis/bitbucket-integration/[ALM integration] +is setup properly, pull request decoration in Bitbucket is done automatically. + == Parameters diff --git a/pkg/sonar/client.go b/pkg/sonar/client.go index ad8015a4..3bc9512d 100644 --- a/pkg/sonar/client.go +++ b/pkg/sonar/client.go @@ -11,6 +11,14 @@ import ( "github.com/opendevstack/pipeline/pkg/pipelinectxt" ) +type ClientInterface interface { + Scan(sonarProject, branch, commit string, pr *PullRequest) (string, error) + QualityGateGet(p QualityGateGetParams) (*QualityGate, error) + GenerateReports(sonarProject, author, branch, rootPath, artifactPrefix string) (string, error) + ExtractComputeEngineTaskID(filename string) (string, error) + ComputeEngineTaskGet(p ComputeEngineTaskGetParams) (*ComputeEngineTask, error) +} + // Loosely based on https://github.com/brandur/wanikaniapi. type Client struct { httpClient *http.Client diff --git a/pkg/sonar/compute_engine.go b/pkg/sonar/compute_engine.go new file mode 100644 index 00000000..bfc8d7a8 --- /dev/null +++ b/pkg/sonar/compute_engine.go @@ -0,0 +1,61 @@ +package sonar + +import ( + "encoding/json" + "fmt" +) + +const ( + TaskStatusInProgress = "IN_PROGRESS" + TaskStatusPending = "PENDING" + TaskStatusSuccess = "SUCCESS" + TaskStatusFailed = "FAILED" +) + +type ComputeEngineTask struct { + Organization string `json:"organization"` + ID string `json:"id"` + Type string `json:"type"` + ComponentID string `json:"componentId"` + ComponentKey string `json:"componentKey"` + ComponentName string `json:"componentName"` + ComponentQualifier string `json:"componentQualifier"` + AnalysisID string `json:"analysisId"` + Status string `json:"status"` + SubmittedAt string `json:"submittedAt"` + StartedAt string `json:"startedAt"` + ExecutedAt string `json:"executedAt"` + ExecutionTimeMs int `json:"executionTimeMs"` + ErrorMessage string `json:"errorMessage"` + Logs bool `json:"logs"` + HasErrorStacktrace bool `json:"hasErrorStacktrace"` + ErrorStacktrace string `json:"errorStacktrace"` + ScannerContext string `json:"scannerContext"` + HasScannerContext bool `json:"hasScannerContext"` +} + +type computeEngineTaskResponse struct { + Task *ComputeEngineTask `json:"task"` +} + +type ComputeEngineTaskGetParams struct { + AdditionalFields string `json:"additionalFields"` + ID string `json:"id"` +} + +func (c *Client) ComputeEngineTaskGet(p ComputeEngineTaskGetParams) (*ComputeEngineTask, error) { + urlPath := fmt.Sprintf("/api/ce/task?id=%s", p.ID) + statusCode, response, err := c.get(urlPath) + if err != nil { + return nil, fmt.Errorf("request returned err: %w", err) + } + if statusCode != 200 { + return nil, fmt.Errorf("request returned unexpected response code: %d, body: %s", statusCode, string(response)) + } + var cetr *computeEngineTaskResponse + err = json.Unmarshal(response, &cetr) + if err != nil { + return nil, fmt.Errorf("could not unmarshal response: %w", err) + } + return cetr.Task, nil +} diff --git a/pkg/sonar/compute_engine_test.go b/pkg/sonar/compute_engine_test.go new file mode 100644 index 00000000..d920a284 --- /dev/null +++ b/pkg/sonar/compute_engine_test.go @@ -0,0 +1,57 @@ +package sonar + +import ( + "testing" + + "github.com/opendevstack/pipeline/test/testserver" +) + +func TestComputeEngineTaskGet(t *testing.T) { + + srv, cleanup := testserver.NewTestServer(t) + defer cleanup() + c := testClient(srv.Server.URL) + + tests := map[string]struct { + Fixture string + WantStatus string + }{ + "FAILED status": { + Fixture: "sonar/task_failed.json", + WantStatus: TaskStatusFailed, + }, + "SUCCESS status": { + Fixture: "sonar/task_success.json", + WantStatus: TaskStatusSuccess, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + srv.EnqueueResponse( + t, "/api/ce/task", + 200, tc.Fixture, + ) + taskID := "AVAn5RKqYwETbXvgas-I" + got, err := c.ComputeEngineTaskGet(ComputeEngineTaskGetParams{ID: taskID}) + if err != nil { + t.Fatalf("Unexpected error on request: %s", err) + } + + // check extracted status matches + if got.Status != tc.WantStatus { + t.Fatalf("want %s, got %s", tc.WantStatus, got.Status) + } + + // check sent task ID matches + lr, err := srv.LastRequest() + if err != nil { + t.Fatal(err) + } + q := lr.URL.Query() + if q.Get("id") != taskID { + t.Fatalf("want %s, got %s", taskID, q.Get("id")) + } + }) + } +} diff --git a/pkg/sonar/scan.go b/pkg/sonar/scan.go index 1232a4cf..bc9001e9 100644 --- a/pkg/sonar/scan.go +++ b/pkg/sonar/scan.go @@ -1,7 +1,10 @@ package sonar import ( + "bufio" "fmt" + "os" + "strings" "github.com/opendevstack/pipeline/internal/command" ) @@ -12,6 +15,23 @@ type PullRequest struct { Base string } +// Scan report +type ReportTask struct { + ProjectKey string + ServerUrl string + ServerVersion string + Branch string + DashboardUrl string + CeTaskId string + CeTaskUrl string +} + +const ( + ScannerworkDir = ".scannerwork" + ReportTaskFilename = "report-task.txt" + ReportTaskFile = ScannerworkDir + "/" + ReportTaskFilename +) + // Scan scans the source code and uploads the analysis to given SonarQube project. // If pr is non-nil, information for pull request decoration is sent. func (c *Client) Scan(sonarProject, branch, commit string, pr *PullRequest) (string, error) { @@ -25,6 +45,10 @@ func (c *Client) Scan(sonarProject, branch, commit string, pr *PullRequest) (str if c.clientConfig.Debug { scannerParams = append(scannerParams, "-X") } + // Both Branch Analysis and Pull Request Analysis are only available + // starting in Developer Edition, see + // https://docs.sonarqube.org/latest/branches/overview/ and + // https://docs.sonarqube.org/latest/analysis/pull-request/. if c.clientConfig.ServerEdition != "community" { if pr != nil { scannerParams = append( @@ -46,3 +70,36 @@ func (c *Client) Scan(sonarProject, branch, commit string, pr *PullRequest) (str } return string(stdout), nil } + +/* +Example of the file located in .scannerwork/report-task.txt: + projectKey=XXXX-python + serverUrl=https://sonarqube-ods.XXXX.com + serverVersion=8.2.0.32929 + branch=dummy + dashboardUrl=https://sonarqube-ods.XXXX.com/dashboard?id=XXXX-python&branch=dummy + ceTaskId=AXxaAoUSsjAMlIY9kNmn + ceTaskUrl=https://sonarqube-ods.XXXX.com/api/ce/task?id=AXxaAoUSsjAMlIY9kNmn +*/ +func (c *Client) ExtractComputeEngineTaskID(filename string) (string, error) { + file, err := os.Open(filename) + if err != nil { + return "", err + } + defer file.Close() + + taskIDPrefix := "ceTaskId=" + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, taskIDPrefix) { + return strings.TrimPrefix(line, taskIDPrefix), nil + } + } + + if err := scanner.Err(); err != nil { + return "", err + } + + return "", fmt.Errorf("properties file %s does not contain %s", filename, taskIDPrefix) +} diff --git a/pkg/sonar/scan_test.go b/pkg/sonar/scan_test.go new file mode 100644 index 00000000..8993edfc --- /dev/null +++ b/pkg/sonar/scan_test.go @@ -0,0 +1,24 @@ +package sonar + +import ( + "path/filepath" + "testing" + + "github.com/opendevstack/pipeline/internal/projectpath" +) + +func TestExtractComputeEngineTaskID(t *testing.T) { + + c := testClient("") + want := "AVAn5RKqYwETbXvgas-I" + fixture := filepath.Join(projectpath.Root, "test/testdata/fixtures/sonar", ReportTaskFilename) + got, err := c.ExtractComputeEngineTaskID(fixture) + if err != nil { + t.Fatal(err) + } + + // check extracted status matches + if got != want { + t.Fatalf("want %s, got %s", want, got) + } +} diff --git a/test/tasks/ods-build-go_test.go b/test/tasks/ods-build-go_test.go index a8c5512b..2b858cd7 100644 --- a/test/tasks/ods-build-go_test.go +++ b/test/tasks/ods-build-go_test.go @@ -166,6 +166,25 @@ func TestTaskODSBuildGo(t *testing.T) { } }, }, + "build go app in PR": { + WorkspaceDirMapping: map[string]string{"source": "go-sample-app"}, + PreRunFunc: func(t *testing.T, ctxt *tasktesting.TaskRunContext) { + wsDir := ctxt.Workspaces["source"] + ctxt.ODS = tasktesting.SetupGitRepo(t, ctxt.Namespace, wsDir) + writeContextFile(t, wsDir, "pr-key", "3") + writeContextFile(t, wsDir, "pr-key", "master") + ctxt.Params = map[string]string{ + "go-os": runtime.GOOS, + "go-arch": runtime.GOARCH, + "sonar-quality-gate": "true", + } + }, + WantRunSuccess: true, + PostRunFunc: func(t *testing.T, ctxt *tasktesting.TaskRunContext) { + sonarProject := sonar.ProjectKey(ctxt.ODS, "") + checkSonarQualityGate(t, ctxt.Clients.KubernetesClientSet, ctxt.Namespace, sonarProject, true, "OK") + }, + }, "build go app with redis sidecar": { TaskVariant: "with-sidecar", WorkspaceDirMapping: map[string]string{"source": "go-redis"}, diff --git a/test/testdata/fixtures/sonar/report-task.txt b/test/testdata/fixtures/sonar/report-task.txt new file mode 100644 index 00000000..a7aa8547 --- /dev/null +++ b/test/testdata/fixtures/sonar/report-task.txt @@ -0,0 +1,7 @@ +projectKey=XXXX-python +serverUrl=https://sonarqube-ods.XXXX.com +serverVersion=8.2.0.32929 +branch=dummy +dashboardUrl=https://sonarqube-ods.XXXX.com/dashboard?id=XXXX-python&branch=dummy +ceTaskId=AVAn5RKqYwETbXvgas-I +ceTaskUrl=https://sonarqube-ods.XXXX.com/api/ce/task?id=AVAn5RKqYwETbXvgas-I diff --git a/test/testdata/fixtures/sonar/task_failed.json b/test/testdata/fixtures/sonar/task_failed.json new file mode 100644 index 00000000..e11859cd --- /dev/null +++ b/test/testdata/fixtures/sonar/task_failed.json @@ -0,0 +1,23 @@ +{ + "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 + } + } diff --git a/test/testdata/fixtures/sonar/task_success.json b/test/testdata/fixtures/sonar/task_success.json new file mode 100644 index 00000000..fa8da9d5 --- /dev/null +++ b/test/testdata/fixtures/sonar/task_success.json @@ -0,0 +1,23 @@ +{ + "task": { + "organization": "my-org-1", + "id": "AVAn5RKqYwETbXvgas-I", + "type": "REPORT", + "componentId": "AVAn5RJmYwETbXvgas-H", + "componentKey": "project_1", + "componentName": "Project One", + "componentQualifier": "TRK", + "analysisId": "123456", + "status": "SUCCESS", + "submittedAt": "2015-10-02T11:32:15+0200", + "startedAt": "2015-10-02T11:32:16+0200", + "executedAt": "2015-10-02T11:32:22+0200", + "executionTimeMs": 5286, + "errorMessage": "", + "logs": false, + "hasErrorStacktrace": false, + "errorStacktrace": "", + "scannerContext": "SonarQube plugins:\n\t- Git 1.0 (scmgit)\n\t- Java 3.13.1 (java)", + "hasScannerContext": true + } + }