Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate SonarQube scan results with Bitbucket #698

Merged
merged 22 commits into from
Jul 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- Add Technical Specifications and risks related to technical specifications to the TRC document ([#690](https://github.com/opendevstack/ods-jenkins-shared-library/pull/690))
- Enable the co-existence of multiple E2E test components ([#377](https://github.com/opendevstack/ods-jenkins-shared-library/issues/377))
- Fix Document History in TIR and IVR is not correct after de deploy to P ([#695](https://github.com/opendevstack/ods-jenkins-shared-library/pull/695))
- Integrate SonarQube scan results with Bitbucket ([#698](https://github.com/opendevstack/ods-jenkins-shared-library/pull/698))

## [3.0] - 2020-08-11

Expand Down
5 changes: 5 additions & 0 deletions src/org/ods/component/Context.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,11 @@ class Context implements IContext {
config.sonarQubeEdition
}

@NonCPS
String getSonarQubeNexusRepository() {
config.sonarQubeNexusRepository
}

@NonCPS
String getSonarQubeBranch() {
config.sonarQubeBranch
Expand Down
3 changes: 3 additions & 0 deletions src/org/ods/component/IContext.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ interface IContext {
// Edition of the SonarQube server
String getSonarQubeEdition()

// Nexus repository to store SonarQube reports
String getSonarQubeNexusRepository()

// set branch on which to run SonarQube analysis.
void setSonarQubeBranch(String sonarQubeBranch)

Expand Down
50 changes: 45 additions & 5 deletions src/org/ods/component/ScanWithAquaStage.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ScanWithAquaStage extends Stage {

static final String STAGE_NAME = 'Aqua Security Scan'
static final String AQUA_CONFIG_MAP_NAME = "aqua"
static final String BITBUCKET_AQUA_REPORT_KEY = "org.opendevstack.aquasec"
private final AquaService aqua
private final BitbucketService bitbucket
private final OpenShiftService openShift
Expand Down Expand Up @@ -166,18 +167,57 @@ class ScanWithAquaStage extends Stage {
String details = "Please visit the following links to review the Aqua Security scan report:"

String result = returnCode == 0 ? "PASS" : "FAIL"
bitbucket.createCodeInsightReport(aquaScanUrl, nexusUrlReport,
context.repoName, context.gitCommit,
title, details, result, prepareMessageToBitbucket(messages))

def data = [
key: BITBUCKET_AQUA_REPORT_KEY,
title: title,
link: nexusUrlReport,
otherLinks: [
[
title: "Report",
text: "Result in Aqua",
link: aquaScanUrl
]
],
details: details,
result: result,
]
if (nexusUrlReport) {
((List)data.otherLinks).add([
title: "Report",
text: "Result in Nexus",
link: nexusUrlReport,
])
}
if (messages) {
data.put("messages",[
[ title: "Messages", value: prepareMessageToBitbucket(messages), ]
])
}

bitbucket.createCodeInsightReport(data, context.repoName, context.gitCommit)
}

private createBitbucketCodeInsightReport(String messages) {
String title = "Aqua Security"
String details = "There was some problems with Aqua:"

String result = "FAIL"
bitbucket.createCodeInsightReport(null, null,
context.repoName, context.gitCommit, title, details, result, prepareMessageToBitbucket(messages))

def data = [
key: BITBUCKET_AQUA_REPORT_KEY,
title: title,
messages: [
[
title: "Messages",
value: prepareMessageToBitbucket(messages)
]
],
details: details,
result: result,
]

bitbucket.createCodeInsightReport(data, context.repoName, context.gitCommit)
}

private String prepareMessageToBitbucket(String message = "") {
Expand Down
74 changes: 71 additions & 3 deletions src/org/ods/component/ScanWithSonarStage.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@ package org.ods.component

import groovy.transform.TypeChecked
import groovy.transform.TypeCheckingMode
import org.ods.orchestration.util.PDFUtil
import org.ods.services.BitbucketService
import org.ods.services.NexusService
import org.ods.services.SonarQubeService
import org.ods.util.ILogger

@SuppressWarnings('ParameterCount')
@TypeChecked
class ScanWithSonarStage extends Stage {

public final String STAGE_NAME = 'SonarQube Analysis'
static final String STAGE_NAME = 'SonarQube Analysis'
static final String BITBUCKET_SONARQUBE_REPORT_KEY = "org.opendevstack.sonarqube"
static final String DEFAULT_NEXUS_REPOSITORY = "leva-documentation"
private final BitbucketService bitbucket
private final SonarQubeService sonarQube
private final NexusService nexus
private final ScanWithSonarOptions options

@TypeChecked(TypeCheckingMode.SKIP)
Expand All @@ -22,6 +27,7 @@ class ScanWithSonarStage extends Stage {
Map<String, Object> config,
BitbucketService bitbucket,
SonarQubeService sonarQube,
NexusService nexus,
ILogger logger) {
super(script, context, logger)
// If user did not explicitly define which branches to scan,
Expand Down Expand Up @@ -53,6 +59,7 @@ class ScanWithSonarStage extends Stage {
this.options = new ScanWithSonarOptions(config)
this.bitbucket = bitbucket
this.sonarQube = sonarQube
this.nexus = nexus
}

// This is called from Stage#execute if the branch being built is eligible.
Expand Down Expand Up @@ -82,8 +89,10 @@ class ScanWithSonarStage extends Stage {
!context.triggeredByOrchestrationPipeline
)

// We need always the QG to put in insight report in Bitbucket
def qualityGateResult = getQualityGateResult(sonarProjectKey)
logger.info "SonarQube Quality Gate value: ${qualityGateResult}"
if (options.requireQualityGatePass) {
def qualityGateResult = getQualityGateResult(sonarProjectKey)
if (qualityGateResult == 'ERROR') {
steps.error 'Quality gate failed!'
} else if (qualityGateResult == 'UNKNOWN') {
Expand All @@ -92,6 +101,10 @@ class ScanWithSonarStage extends Stage {
steps.echo 'Quality gate passed.'
}
}
def report = generateTempFileFromReport("artifacts/" + context.getBuildArtifactURIs().get('SCRR-MD'))
URI reportUriNexus = generateAndArchiveReportInNexus(report,
context.sonarQubeNexusRepository ? context.sonarQubeNexusRepository : DEFAULT_NEXUS_REPOSITORY)
createBitbucketCodeInsightReport(qualityGateResult, reportUriNexus.toString(), sonarProjectKey)
}

private void scan(Map sonarProperties) {
Expand Down Expand Up @@ -174,6 +187,7 @@ class ScanWithSonarStage extends Stage {
includes: 'artifacts/SCRR*',
allowEmpty: true
)

context.addArtifactURI('SCRR', targetReport)
context.addArtifactURI('SCRR-MD', targetReportMd)
}
Expand All @@ -183,12 +197,66 @@ class ScanWithSonarStage extends Stage {
def qualityGateJSON = sonarQube.getQualityGateJSON(sonarProjectKey)
try {
def qualityGateResult = steps.readJSON(text: qualityGateJSON)
def status = qualityGateResult?.projectStatus?.projectStatus ?: 'UNKNOWN'
def status = qualityGateResult?.projectStatus?.status ?: 'UNKNOWN'
return status.toUpperCase()
} catch (Exception ex) {
steps.error 'Quality gate status could not be retrieved. ' +
"Status was: '${qualityGateJSON}'. Error was: ${ex}"
}
}

private createBitbucketCodeInsightReport(String qualityGateResult, String nexusUrlReport, String sonarProjectKey) {
String sorQubeScanUrl = sonarQube.getSonarQubeHostUrl() + "/dashboard?id=${sonarProjectKey}"
String title = "SonarQube"
String details = "Please visit the following links to review the SonarQube report:"
String result = qualityGateResult == "OK" ? "PASS" : "FAIL"

def data = [
key: BITBUCKET_SONARQUBE_REPORT_KEY,
title: title,
link: nexusUrlReport,
otherLinks: [
[
title: "Report",
text: "Result in SonarQube",
link: sorQubeScanUrl
],
[
title: "Report",
text: "Result in Nexus",
link: nexusUrlReport
]
],
details: details,
result: result,
]

bitbucket.createCodeInsightReport(data, context.repoName, context.gitCommit)
}

@SuppressWarnings('FileCreateTempFile')
private File generateTempFileFromReport(String report) {
// Using File directly over report path doesn't work
File file = File.createTempFile("temp", ".md")
file.write(steps.readFile(file: report) as String)

return file
}

private URI generateAndArchiveReportInNexus(File reportMd, nexusRepository) {
// Generate the PDF from temp markdown file
def pdfReport = new PDFUtil().convertFromMarkdown(reportMd, true)

URI report = nexus.storeArtifact(
"${nexusRepository}",
"${context.projectId}/${context.componentId}/" +
"${new Date().format('yyyy-MM-dd')}-${context.buildNumber}/sonarQube",
"report.pdf",
pdfReport, "application/pdf")

logger.info "Report stored in: ${report}"

return report
}

}
1 change: 1 addition & 0 deletions src/org/ods/orchestration/usecase/SonarQubeUseCase.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ class SonarQubeUseCase {
"application/text"
)
}

}
42 changes: 16 additions & 26 deletions src/org/ods/services/BitbucketService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -317,48 +317,38 @@ class BitbucketService {
*
* @param result One of: PASS, FAIL
*/
@SuppressWarnings('ParameterCount')
void createCodeInsightReport(String linkAqua, String linkNexus,
String repo, String gitCommit,
String title, String details, String result, String message = null) {
void createCodeInsightReport(Map data, String repo, String gitCommit) {
withTokenCredentials { username, token ->
def payload = "{" +
"\"title\":\"${title}\"," +
"\"title\":\"${data.title}\"," +
"\"reporter\":\"OpenDevStack\"," +
"\"createdDate\":${System.currentTimeMillis()}," +
"\"details\":\"${details}\"," +
"\"result\":\"${result}\","
if (linkNexus) {
payload += "\"link\":\"${linkNexus}\","
"\"details\":\"${data.details}\"," +
"\"result\":\"${data.result}\","
if (data.link) {
payload += "\"link\":\"${data.link}\","
}
payload += "\"data\": ["
if (linkAqua) {
data.otherLinks.eachWithIndex { Map link, i ->
payload += "{" +
"\"title\":\"Report\"," +
"\"value\":{\"linktext\":\"Result in Aqua\",\"href\":\"${linkAqua}\"}," +
"\"title\":\"${link.title}\"," +
"\"value\":{\"linktext\":\"${link.text}\",\"href\":\"${link.link}\"}," +
"\"type\":\"LINK\"" +
"}"
if (linkNexus || message) {
if (i != data.otherLinks.size() - 1 || data.messages) {
payload += ','
}
}
if (linkNexus) {
data.messages.eachWithIndex { Map message, i ->
payload += "{" +
"\"title\":\"Report\"," +
"\"value\":{\"linktext\":\"Result in Nexus\",\"href\":\"${linkNexus}\"}," +
"\"type\":\"LINK\"" +
"\"title\":\"${message.title}\"," +
"\"value\":\"${message.value}\"," +
"\"type\":\"TEXT\"" +
"}"
if (message) {
if (i != data.messages.size() - 1) {
payload += ','
}
}
if (message) {
payload += "{" +
"\"title\":\"Messages\"," +
"\"value\":\"${message}\"," +
"\"type\":\"TEXT\"" +
"}"
}
payload += "]" +
"}"
try {
Expand All @@ -372,7 +362,7 @@ class BitbucketService {
--header \"Content-Type: application/json\" \\
--data '${payload}' \\
${bitbucketUrl}/rest/insights/1.0/projects/${project}/\
repos/${repo}/commits/${gitCommit}/reports/org.opendevstack.aquasec"""
repos/${repo}/commits/${gitCommit}/reports/${data.key}"""
)
return
} catch (err) {
Expand Down
2 changes: 1 addition & 1 deletion src/org/ods/services/NexusService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class NexusService {
"Nexus responded with code: '${response.getStatus()}' and message: '${response.getBody()}'."

if (response.getStatus() == 404) {
message = "Error: unable to store artifact. Nexus could not be found at: '${this.baseURL}'."
message = "Error: unable to store artifact. Nexus could not be found at: '${this.baseURL}' with repo: ${repository}."
}

throw new RuntimeException(message)
Expand Down
13 changes: 7 additions & 6 deletions src/org/ods/services/SonarQubeService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,8 @@ class SonarQubeService {
if (logger.debugMode) {
scannerParams << '-X'
}
if (pullRequestInfo) {
if (pullRequestInfo && (sonarQubeEdition != 'community')) {
[
"-Dsonar.pullrequest.provider='Bitbucket Server'",
"-Dsonar.pullrequest.bitbucketserver.serverUrl=${pullRequestInfo.bitbucketUrl}",
"-Dsonar.pullrequest.bitbucketserver.token.secured=${pullRequestInfo.bitbucketToken}",
"-Dsonar.pullrequest.bitbucketserver.project=${pullRequestInfo.bitbucketProject}",
"-Dsonar.pullrequest.bitbucketserver.repository=${pullRequestInfo.bitbucketRepository}",
"-Dsonar.pullrequest.key=${pullRequestInfo.bitbucketPullRequestKey}",
"-Dsonar.pullrequest.branch=${pullRequestInfo.branch}",
"-Dsonar.pullrequest.base=${pullRequestInfo.baseBranch}",
Expand Down Expand Up @@ -82,6 +77,12 @@ class SonarQubeService {
}
}

String getSonarQubeHostUrl() {
withSonarServerConfig { hostUrl, authToken ->
return hostUrl
}
}

private String getScannerBinary() {
def scannerBinary = 'sonar-scanner'
def status = script.sh(
Expand Down
8 changes: 8 additions & 0 deletions test/groovy/org/ods/PipelineScript.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,12 @@ class PipelineScript {
def readJSON (Map args) {

}

def withSonarQubeEnv(String conf, Closure closure) {
closure()
}

def SONAR_HOST_URL = "https://sonarqube.example.com"

def SONAR_AUTH_TOKEN = "Token"
}
Loading