1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00
sap-jenkins-library/cmd/sonarExecuteScan.go
Googlom ac5cf17317
refactor(orchestrator): Use singleton in orchestrator package and rename methods (#4639)
* rename interface, types and methods.
some type changes and refactor

* update dependent methods and variables

* fix unit tests

* a bit more refactor and fix

* concurrent safe singleton

* return old Options struct

* refactor creating config provider and fix nil pointer derefernce

* fix unit test and linter errors

* introduce resetting config provider (for unit tests)

* fix annoying error message when config provider is not configured

---------

Co-authored-by: Gulom Alimov <gulomjon.alimov@sap.com>
Co-authored-by: Muhammadali Nazarov <muhammadalinazarov@gmail.com>
2024-01-09 16:01:15 +05:00

508 lines
18 KiB
Go

package cmd
import (
"fmt"
"net"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"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"
keytool "github.com/SAP/jenkins-library/pkg/java"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/orchestrator"
"github.com/SAP/jenkins-library/pkg/piperutils"
SonarUtils "github.com/SAP/jenkins-library/pkg/sonar"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/SAP/jenkins-library/pkg/versioning"
)
type sonarSettings struct {
workingDir string
binary string
environment []string
options []string
}
func (s *sonarSettings) addEnvironment(element string) {
s.environment = append(s.environment, element)
}
func (s *sonarSettings) addOption(element string) {
s.options = append(s.options, element)
}
var (
sonar sonarSettings
execLookPath = exec.LookPath
fileUtilsExists = piperutils.FileExists
fileUtilsUnzip = piperutils.Unzip
osRename = os.Rename
osStat = os.Stat
doublestarGlob = doublestar.Glob
)
const (
javaBinaries = "sonar.java.binaries="
javaLibraries = "sonar.java.libraries="
coverageExclusions = "sonar.coverage.exclusions="
pomXMLPattern = "**/pom.xml"
)
func sonarExecuteScan(config sonarExecuteScanOptions, _ *telemetry.CustomData, influx *sonarExecuteScanInflux) {
runner := command.Command{
ErrorCategoryMapping: map[string][]string{
log.ErrorConfiguration.String(): {
"You must define the following mandatory properties for '*': *",
"org.sonar.java.AnalysisException: Your project contains .java files, please provide compiled classes with sonar.java.binaries property, or exclude them from the analysis with sonar.exclusions property.",
"ERROR: Invalid value for *",
"java.lang.IllegalStateException: No files nor directories matching '*'",
},
log.ErrorInfrastructure.String(): {
"ERROR: SonarQube server [*] can not be reached",
"Caused by: java.net.SocketTimeoutException: timeout",
"java.lang.IllegalStateException: Fail to request *",
"java.lang.IllegalStateException: Fail to download plugin [*] into *",
},
},
}
// reroute command output to logging framework
runner.Stdout(log.Writer())
runner.Stderr(log.Writer())
// 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{}
proxy := config.Proxy
if proxy != "" {
transportProxy, err := url.Parse(proxy)
if err != nil {
log.Entry().WithError(err).Fatalf("Failed to parse proxy string %v into a URL structure", proxy)
}
host, port, err := net.SplitHostPort(transportProxy.Host)
if err != nil {
log.Entry().WithError(err).Fatalf("Failed to retrieve host and port from the proxy URL")
}
// provide proxy setting for Java based Sonar scanner
javaToolOptions := fmt.Sprintf("-Dhttp.proxyHost=%v -Dhttp.proxyPort=%v", host, port)
os.Setenv("JAVA_TOOL_OPTIONS", javaToolOptions)
apiClient.SetOptions(piperhttp.ClientOptions{TransportProxy: transportProxy, TransportSkipVerification: true})
log.Entry().Infof("HTTP client instructed to use %v proxy", proxy)
} else {
//TODO: implement certificate handling
apiClient.SetOptions(piperhttp.ClientOptions{TransportSkipVerification: true})
}
sonar = sonarSettings{
workingDir: "./",
binary: "sonar-scanner",
environment: []string{},
options: []string{},
}
influx.step_data.fields.sonar = false
fileUtils := piperutils.Files{}
if err := runSonar(config, downloadClient, &runner, apiClient, &fileUtils, 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)
}
log.Entry().WithError(err).Fatal("Execution failed")
}
influx.step_data.fields.sonar = true
}
func runSonar(config sonarExecuteScanOptions, client piperhttp.Downloader, runner command.ExecRunner, apiClient SonarUtils.Sender, utils piperutils.FileUtils, influx *sonarExecuteScanInflux) error {
// Set config based on orchestrator-specific environment variables
detectParametersFromCI(&config)
if len(config.ServerURL) > 0 {
sonar.addEnvironment("SONAR_HOST_URL=" + config.ServerURL)
}
if len(config.Token) == 0 {
log.Entry().Warn("sonar token not set")
// use token provided by sonar-scanner-jenkins plugin
// https://github.com/SonarSource/sonar-scanner-jenkins/blob/441ef2f485884758b60767bed2ef8a1a0a7fc863/src/main/java/hudson/plugins/sonar/SonarBuildWrapper.java#L132
if len(os.Getenv("SONAR_AUTH_TOKEN")) > 0 {
log.Entry().Info("using token from env var SONAR_AUTH_TOKEN")
config.Token = os.Getenv("SONAR_AUTH_TOKEN")
}
}
if len(config.Token) > 0 {
sonar.addEnvironment("SONAR_TOKEN=" + config.Token)
}
if len(config.Organization) > 0 {
sonar.addOption("sonar.organization=" + config.Organization)
}
if len(config.Version) > 0 {
version := config.CustomScanVersion
if len(version) > 0 {
log.Entry().Infof("Using custom version: %v", version)
} else {
version = versioning.ApplyVersioningModel(config.VersioningModel, config.Version)
}
sonar.addOption("sonar.projectVersion=" + version)
}
if GeneralConfig.Verbose {
sonar.addOption("sonar.verbose=true")
}
if len(config.ProjectKey) > 0 {
sonar.addOption("sonar.projectKey=" + config.ProjectKey)
}
if len(config.M2Path) > 0 && config.InferJavaLibraries {
sonar.addOption(javaLibraries + filepath.Join(config.M2Path, "**"))
}
if len(config.CoverageExclusions) > 0 && !isInOptions(config, coverageExclusions) {
sonar.addOption(coverageExclusions + strings.Join(config.CoverageExclusions, ","))
}
if config.InferJavaBinaries && !isInOptions(config, javaBinaries) {
addJavaBinaries()
}
if config.WaitForQualityGate {
sonar.addOption("sonar.qualitygate.wait=true")
}
if err := handlePullRequest(config); err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
if err := loadSonarScanner(config.SonarScannerDownloadURL, client); err != nil {
log.SetErrorCategory(log.ErrorInfrastructure)
return err
}
if err := loadCertificates(config.CustomTLSCertificateLinks, client, runner); err != nil {
log.SetErrorCategory(log.ErrorInfrastructure)
return err
}
if len(config.Options) > 0 {
sonar.options = append(sonar.options, config.Options...)
}
sonar.options = piperutils.PrefixIfNeeded(piperutils.Trim(sonar.options), "-D")
log.Entry().
WithField("command", sonar.binary).
WithField("options", sonar.options).
WithField("environment", sonar.environment).
Debug("Executing sonar scan command")
// execute scan
runner.SetEnv(sonar.environment)
err := runner.RunExecutable(sonar.binary, sonar.options...)
if err != nil {
return err
}
// as PRs are handled locally for legacy SonarQube systems, no measurements will be fetched.
if len(config.ChangeID) > 0 && config.LegacyPRHandling {
return nil
}
// load task results
taskReport, err := SonarUtils.ReadTaskReport(sonar.workingDir)
if err != nil {
log.Entry().WithError(err).Warning("no scan report found")
return nil
}
var serverUrl string
if len(config.Proxy) > 0 {
serverUrl = config.ServerURL
} else {
serverUrl = taskReport.ServerURL
}
// write reports JSON
reports := []piperutils.Path{
{
Target: "sonarscan.json",
Mandatory: false,
},
}
// write links JSON
links := []piperutils.Path{
{
Target: taskReport.DashboardURL,
Name: "Sonar Web UI",
},
}
piperutils.PersistReportsAndLinks("sonarExecuteScan", sonar.workingDir, utils, reports, links)
if len(config.Token) == 0 {
log.Entry().Warn("no measurements are fetched due to missing credentials")
return nil
}
taskService := SonarUtils.NewTaskService(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(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
}
reportData := SonarUtils.ReportData{
ServerURL: taskReport.ServerURL,
ProjectKey: taskReport.ProjectKey,
TaskID: taskReport.TaskID,
ChangeID: config.ChangeID,
BranchName: config.BranchName,
Organization: config.Organization,
NumberOfIssues: SonarUtils.Issues{
Blocker: influx.sonarqube_data.fields.blocker_issues,
Critical: influx.sonarqube_data.fields.critical_issues,
Major: influx.sonarqube_data.fields.major_issues,
Minor: influx.sonarqube_data.fields.minor_issues,
Info: influx.sonarqube_data.fields.info_issues,
}}
componentService := SonarUtils.NewMeasuresComponentService(serverUrl, config.Token, taskReport.ProjectKey, config.Organization, config.BranchName, config.ChangeID, apiClient)
cov, err := componentService.GetCoverage()
if err != nil {
log.Entry().Warnf("failed to retrieve sonar coverage data: %v", err)
} else {
reportData.Coverage = cov
}
loc, err := componentService.GetLinesOfCode()
if err != nil {
log.Entry().Warnf("failed to retrieve sonar lines of code data: %v", err)
} else {
reportData.LinesOfCode = loc
}
log.Entry().Debugf("Influx values: %v", influx.sonarqube_data.fields)
err = SonarUtils.WriteReport(reportData, sonar.workingDir, os.WriteFile)
if err != nil {
return err
}
return nil
}
// isInOptions returns true, if the given property is already provided in config.Options.
func isInOptions(config sonarExecuteScanOptions, property string) bool {
property = strings.TrimSuffix(property, "=")
return piperutils.ContainsStringPart(config.Options, property)
}
func addJavaBinaries() {
pomFiles, err := doublestarGlob(pomXMLPattern)
if err != nil {
log.Entry().Warnf("failed to glob for pom modules: %v", err)
return
}
var binaries []string
var classesDirs = []string{"classes", "test-classes"}
for _, pomFile := range pomFiles {
module := filepath.Dir(pomFile)
for _, classDir := range classesDirs {
classesPath := filepath.Join(module, "target", classDir)
_, err := osStat(classesPath)
if err == nil {
binaries = append(binaries, classesPath)
}
}
}
if len(binaries) > 0 {
sonar.addOption(javaBinaries + strings.Join(binaries, ","))
}
}
func handlePullRequest(config sonarExecuteScanOptions) error {
if len(config.ChangeID) > 0 {
if config.LegacyPRHandling {
// see https://docs.sonarqube.org/display/PLUG/GitHub+Plugin
sonar.addOption("sonar.analysis.mode=preview")
sonar.addOption("sonar.github.pullRequest=" + config.ChangeID)
if len(config.GithubAPIURL) > 0 {
sonar.addOption("sonar.github.endpoint=" + config.GithubAPIURL)
}
if len(config.GithubToken) > 0 {
sonar.addOption("sonar.github.oauth=" + config.GithubToken)
}
if len(config.Owner) > 0 && len(config.Repository) > 0 {
sonar.addOption("sonar.github.repository=" + config.Owner + "/" + config.Repository)
}
if config.DisableInlineComments {
sonar.addOption("sonar.github.disableInlineComments=" + strconv.FormatBool(config.DisableInlineComments))
}
} else {
// see https://sonarcloud.io/documentation/analysis/pull-request/
provider := strings.ToLower(config.PullRequestProvider)
if provider == "github" {
if len(config.Owner) > 0 && len(config.Repository) > 0 {
sonar.addOption("sonar.pullrequest.github.repository=" + config.Owner + "/" + config.Repository)
}
} else {
return errors.New("Pull-Request provider '" + provider + "' is not supported!")
}
sonar.addOption("sonar.pullrequest.key=" + config.ChangeID)
sonar.addOption("sonar.pullrequest.base=" + config.ChangeTarget)
sonar.addOption("sonar.pullrequest.branch=" + config.ChangeBranch)
sonar.addOption("sonar.pullrequest.provider=" + provider)
}
} else if len(config.BranchName) > 0 {
sonar.addOption("sonar.branch.name=" + config.BranchName)
}
return nil
}
func loadSonarScanner(url string, client piperhttp.Downloader) error {
if scannerPath, err := execLookPath(sonar.binary); err == nil {
// using existing sonar-scanner
log.Entry().WithField("path", scannerPath).Debug("Using local sonar-scanner")
} else if len(url) != 0 {
// download sonar-scanner-cli into TEMP folder
log.Entry().WithField("url", url).Debug("Downloading sonar-scanner")
tmpFolder := getTempDir()
defer os.RemoveAll(tmpFolder) // clean up
archive := filepath.Join(tmpFolder, path.Base(url))
if err := client.DownloadFile(url, archive, nil, nil); err != nil {
return errors.Wrap(err, "Download of sonar-scanner failed")
}
// unzip sonar-scanner-cli
log.Entry().WithField("source", archive).WithField("target", tmpFolder).Debug("Extracting sonar-scanner")
if _, err := fileUtilsUnzip(archive, tmpFolder); err != nil {
return errors.Wrap(err, "Extraction of sonar-scanner failed")
}
// move sonar-scanner-cli to .sonar-scanner/
toolPath := ".sonar-scanner"
foldername := strings.ReplaceAll(strings.ReplaceAll(archive, ".zip", ""), "cli-", "")
log.Entry().WithField("source", foldername).WithField("target", toolPath).Debug("Moving sonar-scanner")
if err := osRename(foldername, toolPath); err != nil {
return errors.Wrap(err, "Moving of sonar-scanner failed")
}
// update binary path
sonar.binary = filepath.Join(getWorkingDir(), toolPath, "bin", sonar.binary)
log.Entry().Debug("Download completed")
}
return nil
}
func loadCertificates(certificateList []string, client piperhttp.Downloader, runner command.ExecRunner) error {
truststorePath := filepath.Join(getWorkingDir(), ".certificates")
truststoreFile := filepath.Join(truststorePath, "cacerts")
if exists, _ := fileUtilsExists(truststoreFile); exists {
// use local existing trust store
sonar.addEnvironment("SONAR_SCANNER_OPTS=" + keytool.GetMavenOpts(truststoreFile))
log.Entry().WithField("trust store", truststoreFile).Info("Using local trust store")
} else if len(certificateList) > 0 {
// create download temp dir
tmpFolder := getTempDir()
defer os.RemoveAll(tmpFolder) // clean up
if err := os.MkdirAll(truststorePath, 0777); err != nil {
log.Entry().Warningf("failed to create directory %v: %v", truststorePath, err)
}
// copying existing truststore
defaultTruststorePath := keytool.GetDefaultTruststorePath()
if exists, _ := fileUtilsExists(defaultTruststorePath); exists {
if err := keytool.ImportTruststore(runner, truststoreFile, defaultTruststorePath); err != nil {
return errors.Wrap(err, "Copying existing keystore failed")
}
}
// use local created trust store with downloaded certificates
for _, certificate := range certificateList {
target := filepath.Join(tmpFolder, path.Base(certificate))
log.Entry().WithField("source", certificate).WithField("target", target).Info("Downloading TLS certificate")
// download certificate
if err := client.DownloadFile(certificate, target, nil, nil); err != nil {
return errors.Wrapf(err, "Download of TLS certificate failed")
}
// add certificate to keystore
if err := keytool.ImportCert(runner, truststoreFile, target); err != nil {
log.Entry().Warnf("Adding certificate to keystore failed")
// return errors.Wrap(err, "Adding certificate to keystore failed")
}
}
sonar.addEnvironment("SONAR_SCANNER_OPTS=" + keytool.GetMavenOpts(truststoreFile))
log.Entry().WithField("trust store", truststoreFile).Info("Using local trust store")
} else {
log.Entry().Debug("Download of TLS certificates skipped")
}
return nil
}
func getWorkingDir() string {
workingDir, err := os.Getwd()
if err != nil {
log.Entry().WithError(err).WithField("path", workingDir).Debug("Retrieving of work directory failed")
}
return workingDir
}
func getTempDir() string {
tmpFolder, err := os.MkdirTemp(".", "temp-")
if err != nil {
log.Entry().WithError(err).WithField("path", tmpFolder).Debug("Creating temp directory failed")
}
return tmpFolder
}
// Fetches parameters from environment variables and updates the options accordingly (only if not already set)
func detectParametersFromCI(options *sonarExecuteScanOptions) {
provider, err := orchestrator.GetOrchestratorConfigProvider(nil)
if err != nil {
log.Entry().WithError(err).Warning("Cannot infer config from CI environment")
return
}
if provider.IsPullRequest() {
config := provider.PullRequestConfig()
if len(options.ChangeBranch) == 0 {
log.Entry().Info("Inferring parameter changeBranch from environment: " + config.Branch)
options.ChangeBranch = config.Branch
}
if len(options.ChangeTarget) == 0 {
log.Entry().Info("Inferring parameter changeTarget from environment: " + config.Base)
options.ChangeTarget = config.Base
}
if len(options.ChangeID) == 0 {
log.Entry().Info("Inferring parameter changeId from environment: " + config.Key)
options.ChangeID = config.Key
}
} else {
branch := provider.Branch()
if options.InferBranchName && len(options.BranchName) == 0 {
log.Entry().Info("Inferring parameter branchName from environment: " + branch)
options.BranchName = branch
}
}
}