diff --git a/documentation/bin/createDocu.groovy b/documentation/bin/createDocu.groovy index 83bce877d..1c52b6696 100644 --- a/documentation/bin/createDocu.groovy +++ b/documentation/bin/createDocu.groovy @@ -375,11 +375,12 @@ class Helper { return mappings } - static getValue(Map config, def pPath) { - def p =config[pPath.head()] + static getValue(Map config, List pPath) { + def p = config[pPath.head()] if(pPath.size() == 1) return p // there is no tail if(p in Map) getValue(p, pPath.tail()) - else return p + return null // there is a remaining path which could not be resolved. + // the value we are looking for does not exist. } static resolveDocuRelevantSteps(GroovyScriptEngine gse, File stepsDir) { @@ -640,7 +641,7 @@ def handleStep(stepName, prepareDefaultValuesStep, gse, customDefaults) { it -> - def defaultValue = Helper.getValue(defaultConfig, it.split('/')) + def defaultValue = Helper.getValue(defaultConfig, it.tokenize('/')) def parameterProperties = [ defaultValue: defaultValue, @@ -675,7 +676,7 @@ def handleStep(stepName, prepareDefaultValuesStep, gse, customDefaults) { [ dependentParameterKey: dependentParameterKey, key: possibleValue, - value: Helper.getValue(defaultConfig.get(possibleValue), k.split('/')) + value: Helper.getValue(defaultConfig.get(possibleValue), k.tokenize('/')) ] } } diff --git a/documentation/docs/steps/neoDeploy.md b/documentation/docs/steps/neoDeploy.md index 4fe0f3609..3fb095615 100644 --- a/documentation/docs/steps/neoDeploy.md +++ b/documentation/docs/steps/neoDeploy.md @@ -25,15 +25,15 @@ none ## Exceptions * `Exception`: - * If `source` is not provided. - * If `propertiesFile` is not provided (when using `'WAR_PROPERTIESFILE'` deployment mode). - * If `application` is not provided (when using `'WAR_PARAMS'` deployment mode). - * If `runtime` is not provided (when using `'WAR_PARAMS'` deployment mode). - * If `runtimeVersion` is not provided (when using `'WAR_PARAMS'` deployment mode). + * If `source` is not provided. + * If `propertiesFile` is not provided (when using `'WAR_PROPERTIESFILE'` deployment mode). + * If `application` is not provided (when using `'WAR_PARAMS'` deployment mode). + * If `runtime` is not provided (when using `'WAR_PARAMS'` deployment mode). + * If `runtimeVersion` is not provided (when using `'WAR_PARAMS'` deployment mode). * `AbortException`: - * If neo-java-web-sdk is not installed, or `neoHome`is wrong. + * If neo-java-web-sdk is not installed, or `neoHome`is wrong. * `CredentialNotFoundException`: - * If the credentials cannot be resolved. + * If the credentials cannot be resolved. ## Example diff --git a/documentation/docs/steps/sonarExecuteScan.md b/documentation/docs/steps/sonarExecuteScan.md new file mode 100644 index 000000000..486ae3c17 --- /dev/null +++ b/documentation/docs/steps/sonarExecuteScan.md @@ -0,0 +1,18 @@ +# ${docGenStepName} + +## ${docGenDescription} + +## Prerequsites + +- The project needs a `sonar-project.properties` file that describes the project and defines certain settings, see [here](https://docs.sonarqube.org/display/SCAN/Advanced+SonarQube+Scanner+Usages#AdvancedSonarQubeScannerUsages-Multi-moduleProjectStructure). +- A SonarQube instance needs to be defined in the Jenkins. + +## ${docGenParameters} + +## ${docGenConfiguration} + +## Exceptions + +none + +## Examples diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 211bf7c1e..241174973 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -37,6 +37,7 @@ nav: - setupCommonPipelineEnvironment: steps/setupCommonPipelineEnvironment.md - slackSendNotification: steps/slackSendNotification.md - snykExecute: steps/snykExecute.md + - sonarExecuteScan: steps/sonarExecuteScan.md - testsPublishResults: steps/testsPublishResults.md - transportRequestCreate: steps/transportRequestCreate.md - transportRequestRelease: steps/transportRequestRelease.md diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index 1bb4c467c..71f11f6d1 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -463,6 +463,12 @@ steps: - 'opensourceConfiguration' toJson: false toHtml: false + sonarExecuteScan: + dockerImage: 'maven:3.5-jdk-8' + instance: 'SonarCloud' + options: [] + pullRequestProvider: 'github' + sonarScannerDownloadUrl: 'https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-3.3.0.1492-linux.zip' testsPublishResults: failOnError: false junit: diff --git a/test/groovy/SonarExecuteTest.groovy b/test/groovy/SonarExecuteTest.groovy new file mode 100644 index 000000000..70c96d0f7 --- /dev/null +++ b/test/groovy/SonarExecuteTest.groovy @@ -0,0 +1,237 @@ +import static org.hamcrest.Matchers.containsString +import static org.hamcrest.Matchers.hasItem +import static org.hamcrest.Matchers.is +import static org.hamcrest.Matchers.allOf + +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.ExpectedException +import static org.junit.Assert.assertThat + +import util.BasePiperTest +import util.JenkinsDockerExecuteRule +import util.JenkinsShellCallRule +import util.JenkinsReadYamlRule +import util.JenkinsStepRule +import util.JenkinsLoggingRule +import util.Rules + +class SonarExecuteScanTest extends BasePiperTest { + private ExpectedException thrown = ExpectedException.none() + private JenkinsReadYamlRule readYamlRule = new JenkinsReadYamlRule(this) + private JenkinsStepRule jsr = new JenkinsStepRule(this) + private JenkinsLoggingRule jlr = new JenkinsLoggingRule(this) + private JenkinsShellCallRule jscr = new JenkinsShellCallRule(this) + private JenkinsDockerExecuteRule jedr = new JenkinsDockerExecuteRule(this) + + @Rule + public RuleChain rules = Rules + .getCommonRules(this) + .around(readYamlRule) + .around(thrown) + .around(jedr) + .around(jscr) + .around(jlr) + .around(jsr) + + def sonarInstance + + @Before + void init() throws Exception { + sonarInstance = null + helper.registerAllowedMethod("withSonarQubeEnv", [String.class, Closure.class], { string, closure -> + sonarInstance = string + return closure() + }) + helper.registerAllowedMethod("unstash", [String.class], { stashInput -> return []}) + helper.registerAllowedMethod("fileExists", [String.class], { file -> return file }) + helper.registerAllowedMethod('string', [Map], { m -> m }) + helper.registerAllowedMethod('withCredentials', [List, Closure], { l, c -> + try { + binding.setProperty(l[0].variable, 'TOKEN_'+l[0].credentialsId) + c() + } finally { + binding.setProperty(l[0].variable, null) + } + }) + nullScript.commonPipelineEnvironment.setArtifactVersion('1.2.3-20180101') + } + + @Test + void testWithDefaults() throws Exception { + jsr.step.sonarExecuteScan( + script: nullScript, + juStabUtils: utils + ) + + // asserts + assertThat('Sonar instance is not set to the default value', sonarInstance, is('SonarCloud')) + assertThat('Sonar project version is not set to the default value', jscr.shell, hasItem(containsString('sonar-scanner -Dsonar.projectVersion=1'))) + assertThat('Docker image is not set to the default value', jedr.dockerParams.dockerImage, is('maven:3.5-jdk-8')) + assertJobStatusSuccess() + } + + @Test + void testWithCustomVersion() throws Exception { + jsr.step.sonarExecuteScan( + script: nullScript, + juStabUtils: utils, + projectVersion: '2' + ) + + // asserts + assertThat('Sonar project version is not set to the custom value', jscr.shell, hasItem(containsString('sonar-scanner -Dsonar.projectVersion=2'))) + assertJobStatusSuccess() + } + + @Test + void testWithCustomOptions() throws Exception { + jsr.step.sonarExecuteScan( + script: nullScript, + juStabUtils: utils, + options: '-Dsonar.host.url=localhost' + ) + + // asserts + assertThat('Sonar options are not set to the custom value', jscr.shell, hasItem(containsString('sonar-scanner -Dsonar.host.url=localhost'))) + assertJobStatusSuccess() + } + + @Test + void testWithCustomOptionsList() throws Exception { + jsr.step.sonarExecuteScan( + script: nullScript, + juStabUtils: utils, + options: ['sonar.host.url=localhost'] + ) + + // asserts + assertThat('Sonar options are not set to the custom value', jscr.shell, hasItem(containsString('sonar-scanner -Dsonar.host.url=localhost'))) + assertJobStatusSuccess() + } + + @Test + void testWithCustomInstance() throws Exception { + jsr.step.sonarExecuteScan( + script: nullScript, + juStabUtils: utils, + instance: 'MySonarInstance' + ) + + // asserts + assertThat('Sonar instance is not set to the custom value', sonarInstance.toString(), is('MySonarInstance')) + assertJobStatusSuccess() + } + + @Test + void testWithPRHandling() throws Exception { + binding.setVariable('env', [ + 'CHANGE_ID': '42', + 'CHANGE_TARGET': 'master', + 'BRANCH_NAME': 'feature/anything' + ]) + nullScript.commonPipelineEnvironment.setGithubOrg('testOrg') + //nullScript.commonPipelineEnvironment.setGithubRepo('testRepo') + + jsr.step.sonarExecuteScan( + script: nullScript, + juStabUtils: utils, + //githubOrg: 'testOrg', + githubRepo: 'testRepo' + ) + // asserts + assertThat(jscr.shell, hasItem(allOf( + containsString('-Dsonar.pullrequest.key=42'), + containsString('-Dsonar.pullrequest.base=master'), + containsString('-Dsonar.pullrequest.branch=feature/anything'), + containsString('-Dsonar.pullrequest.provider=github'), + containsString('-Dsonar.pullrequest.github.repository=testOrg/testRepo') + ))) + assertJobStatusSuccess() + } + + @Test + void testWithPRHandlingWithoutMandatory() throws Exception { + thrown.expect(Exception) + thrown.expectMessage('ERROR - NO VALUE AVAILABLE FOR githubRepo') + + binding.setVariable('env', ['CHANGE_ID': '42']) + jsr.step.sonarExecuteScan( + script: nullScript, + juStabUtils: utils, + githubOrg: 'testOrg' + ) + + // asserts + assertJobStatusFailure() + } + + @Test + void testWithLegacyPRHandling() throws Exception { + binding.setVariable('env', ['CHANGE_ID': '42']) + nullScript.commonPipelineEnvironment.setGithubOrg('testOrg') + //nullScript.commonPipelineEnvironment.setGithubRepo('testRepo') + + jsr.step.sonarExecuteScan( + script: nullScript, + juStabUtils: utils, + legacyPRHandling: true, + githubTokenCredentialsId: 'githubId', + //githubOrg: 'testOrg', + githubRepo: 'testRepo' + ) + // asserts + assertThat(jscr.shell, hasItem(allOf( + containsString('-Dsonar.analysis.mode=preview'), + containsString('-Dsonar.github.pullRequest=42'), + containsString('-Dsonar.github.oauth=TOKEN_githubId'), + containsString('-Dsonar.github.repository=testOrg/testRepo') + ))) + assertJobStatusSuccess() + } + + @Test + void testWithLegacyPRHandlingWithoutMandatory() throws Exception { + thrown.expect(Exception) + thrown.expectMessage('ERROR - NO VALUE AVAILABLE FOR githubTokenCredentialsId') + + binding.setVariable('env', ['CHANGE_ID': '42']) + jsr.step.sonarExecuteScan( + script: nullScript, + juStabUtils: utils, + legacyPRHandling: true, + githubOrg: 'testOrg', + githubRepo: 'testRepo' + ) + + // asserts + assertJobStatusFailure() + } + + @Test + void testWithSonarAuth() throws Exception { + jsr.step.sonarExecuteScan( + script: nullScript, + juStabUtils: utils, + sonarTokenCredentialsId: 'githubId' + ) + // asserts + assertThat(jscr.shell, hasItem(containsString('-Dsonar.login=TOKEN_githubId'))) + assertJobStatusSuccess() + } + + @Test + void testWithSonarCloudOrganization() throws Exception { + jsr.step.sonarExecuteScan( + script: nullScript, + juStabUtils: utils, + organization: 'TestOrg-github' + ) + + // asserts + assertThat(jscr.shell, hasItem(containsString('-Dsonar.organization=TestOrg-github'))) + assertJobStatusSuccess() + } +} diff --git a/test/groovy/util/JenkinsSetupRule.groovy b/test/groovy/util/JenkinsSetupRule.groovy index a4f55105a..b89b67267 100644 --- a/test/groovy/util/JenkinsSetupRule.groovy +++ b/test/groovy/util/JenkinsSetupRule.groovy @@ -41,7 +41,8 @@ class JenkinsSetupRule implements TestRule { JOB_NAME : 'p', BUILD_NUMBER: '1', BUILD_URL : 'http://build.url', - BRANCH_NAME: 'master' + BRANCH_NAME: 'master', + WORKSPACE: 'any/path' ]) base.evaluate() diff --git a/vars/sonarExecuteScan.groovy b/vars/sonarExecuteScan.groovy new file mode 100644 index 000000000..e843fb8f3 --- /dev/null +++ b/vars/sonarExecuteScan.groovy @@ -0,0 +1,193 @@ +import com.sap.piper.ConfigurationHelper +import com.sap.piper.GenerateDocumentation +import com.sap.piper.Utils + +import static com.sap.piper.Prerequisites.checkScript + +import groovy.transform.Field +import groovy.text.SimpleTemplateEngine + +@Field String STEP_NAME = getClass().getName() + +@Field Set GENERAL_CONFIG_KEYS = [ + /** + * Pull-Request voting only: + * The URL to the Github API. see https://docs.sonarqube.org/display/PLUG/GitHub+Plugin#GitHubPlugin-Usage + * deprecated: only supported in LTS / < 7.2 + */ + 'githubApiUrl', + /** + * Pull-Request voting only: + * The Github organization. + * @default: `commonPipelineEnvironment.getGithubOrg()` + */ + 'githubOrg', + /** + * Pull-Request voting only: + * The Github repository. + * @default: `commonPipelineEnvironment.getGithubRepo()` + */ + 'githubRepo', + /** + * Pull-Request voting only: + * The Jenkins credentialId for a Github token. It is needed to report findings back to the pull-request. + * deprecated: only supported in LTS / < 7.2 + * @possibleValues Jenkins credential id + */ + 'githubTokenCredentialsId', + /** + * The Jenkins credentialsId for a SonarQube token. It is needed for non-anonymous analysis runs. see https://sonarcloud.io/account/security + * @possibleValues Jenkins credential id + */ + 'sonarTokenCredentialsId', +] +@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([ + /** + * Pull-Request voting only: + * Disables the pull-request decoration with inline comments. + * deprecated: only supported in LTS / < 7.2 + * @possibleValues `true`, `false` + */ + 'disableInlineComments', + /** + * Name of the docker image that should be used. If empty, Docker is not used and the command is executed directly on the Jenkins system. + * see dockerExecute + */ + 'dockerImage', + /** + * The name of the SonarQube instance defined in the Jenkins settings. + */ + 'instance', + /** + * Pull-Request voting only: + * Activates the pull-request handling using the [GitHub Plugin](https://docs.sonarqube.org/display/PLUG/GitHub+Plugin) (deprecated). + * deprecated: only supported in LTS / < 7.2 + * @possibleValues `true`, `false` + */ + 'legacyPRHandling', + /** + * A list of options which are passed to the `sonar-scanner`. + */ + 'options', + /** + * Organization that the project will be assigned to in SonarCloud.io. + */ + 'organization', + /** + * The project version that is reported to SonarQube. + * @default: major number of `commonPipelineEnvironment.getArtifactVersion()` + */ + 'projectVersion' +]) +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS + +/** + * The step executes the [sonar-scanner](https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner) cli command to scan the defined sources and publish the results to a SonarQube instance. + */ +@GenerateDocumentation +void call(Map parameters = [:]) { + handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) { + def utils = parameters.juStabUtils ?: new Utils() + def script = checkScript(this, parameters) ?: this + // load default & individual configuration + Map configuration = ConfigurationHelper.newInstance(this) + .loadStepDefaults() + .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) + .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, GENERAL_CONFIG_KEYS) + .mixin(parameters, PARAMETER_KEYS) + .addIfEmpty('projectVersion', script.commonPipelineEnvironment.getArtifactVersion()?.tokenize('.')?.get(0)) + .addIfEmpty('githubOrg', script.commonPipelineEnvironment.getGithubOrg()) + .addIfEmpty('githubRepo', script.commonPipelineEnvironment.getGithubRepo()) + // check mandatory parameters + .withMandatoryProperty('githubTokenCredentialsId', null, { config -> config.legacyPRHandling && isPullRequest() }) + .withMandatoryProperty('githubOrg', null, { isPullRequest() }) + .withMandatoryProperty('githubRepo', null, { isPullRequest() }) + .use() + + if(configuration.options instanceof String) + configuration.options = [].plus(configuration.options) + + def worker = { config -> + withSonarQubeEnv(config.instance) { + loadSonarScanner(config) + + if(config.organization) config.options.add("sonar.organization=${config.organization}") + if(config.projectVersion) config.options.add("sonar.projectVersion=${config.projectVersion}") + // prefix options + config.options = config.options.collect { it.startsWith('-D') ? it : "-D${it}" } + + sh "PATH=\$PATH:${env.WORKSPACE}/.sonar-scanner/bin sonar-scanner ${config.options.join(' ')}" + } + } + + if(configuration.sonarTokenCredentialsId){ + def workerForSonarAuth = worker + worker = { config -> + withCredentials([string( + credentialsId: config.sonarTokenCredentialsId, + variable: 'SONAR_TOKEN' + )]){ + config.options.add("sonar.login=$SONAR_TOKEN") + workerForSonarAuth(config) + } + } + } + + if(isPullRequest()){ + def workerForGithubAuth = worker + worker = { config -> + if(config.legacyPRHandling) { + withCredentials([string( + credentialsId: config.githubTokenCredentialsId, + variable: 'GITHUB_TOKEN' + )]){ + // support for https://docs.sonarqube.org/display/PLUG/GitHub+Plugin + config.options.add('sonar.analysis.mode=preview') + config.options.add("sonar.github.oauth=$GITHUB_TOKEN") + config.options.add("sonar.github.pullRequest=${env.CHANGE_ID}") + config.options.add("sonar.github.repository=${config.githubOrg}/${config.githubRepo}") + if(config.githubApiUrl) config.options.add("sonar.github.endpoint=${config.githubApiUrl}") + if(config.disableInlineComments) config.options.add("sonar.github.disableInlineComments=${config.disableInlineComments}") + workerForGithubAuth(config) + } + } else { + // see https://sonarcloud.io/documentation/analysis/pull-request/ + config.options.add("sonar.pullrequest.key=${env.CHANGE_ID}") + config.options.add("sonar.pullrequest.base=${env.CHANGE_TARGET}") + config.options.add("sonar.pullrequest.branch=${env.BRANCH_NAME}") + config.options.add("sonar.pullrequest.provider=${config.pullRequestProvider}") + switch(config.pullRequestProvider){ + case 'github': + config.options.add("sonar.pullrequest.github.repository=${config.githubOrg}/${config.githubRepo}") + break; + default: error "Pull-Request provider '${config.pullRequestProvider}' is not supported!" + } + workerForGithubAuth(config) + } + } + } + + dockerExecute( + script: script, + dockerImage: configuration.dockerImage + ){ + worker(configuration) + } + } +} + +private Boolean isPullRequest(){ + return env.CHANGE_ID +} + +private void loadSonarScanner(config){ + def filename = new File(config.sonarScannerDownloadUrl).getName() + def foldername = filename.replace('.zip', '').replace('cli-', '') + + sh """ + curl --remote-name --remote-header-name --location --silent --show-error ${config.sonarScannerDownloadUrl} + unzip -q ${filename} + mv ${foldername} .sonar-scanner + """ +}