From 111080cbfe855eabb0625c9c8acbde99686cb5ce Mon Sep 17 00:00:00 2001 From: Sven Merk <33895725+nevskrem@users.noreply.github.com> Date: Tue, 4 Jun 2019 08:01:43 +0200 Subject: [PATCH] Add new step for Dockerfile linting (#723) * Add new step for Dockerfile linting * Add documentation template file * Remove newlines * Remove internal URL * Rephrase comment * Ammend stash * Fix test * move dockerImage to general * use explicit curl options * small changes * small changes * skip GIT blame * First comments * Also add remark to URL parameter * Second set of comments * Fix return code handling * Switch type to set * Revert unrelated changes * Avoid modification of config * add quality gate defaults * Update hadolintExecute.groovy * fix code climate issue --- documentation/docs/steps/hadolintExecute.md | 17 ++++ resources/default_pipeline_environment.yml | 15 ++- test/groovy/HadolintExecuteTest.groovy | 67 +++++++++++++ vars/hadolintExecute.groovy | 104 ++++++++++++++++++++ 4 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 documentation/docs/steps/hadolintExecute.md create mode 100644 test/groovy/HadolintExecuteTest.groovy create mode 100644 vars/hadolintExecute.groovy diff --git a/documentation/docs/steps/hadolintExecute.md b/documentation/docs/steps/hadolintExecute.md new file mode 100644 index 000000000..eb062cddb --- /dev/null +++ b/documentation/docs/steps/hadolintExecute.md @@ -0,0 +1,17 @@ +# ${docGenStepName} + +## ${docGenDescription} + +## ${docGenParameters} + +## ${docGenConfiguration} + +## Exceptions + +None + +## Examples + +```groovy +hadolintExecute script: this +``` diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index 32e782382..6a128649f 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -251,6 +251,19 @@ steps: languageRunner: 'ruby' runCommand: 'bundle install && bundle exec gauge run' testOptions: 'specs' + hadolintExecute: + configurationFile: '.hadolint.yaml' + configurationUrl: '' + dockerFile: './Dockerfile' + dockerImage: 'hadolint/hadolint:latest-debian' + qualityGates: + - threshold: 1 + type: 'TOTAL_ERROR' + unstable: false + reportFile: 'hadolint.xml' + reportName: 'HaDoLint' + stashContent: + - 'buildDescriptor' handlePipelineStepErrors: echoDetails: true failOnError: true @@ -408,7 +421,7 @@ steps: noDefaultExludes: [] pipelineStashFilesBeforeBuild: stashIncludes: - buildDescriptor: '**/pom.xml, **/.mvn/**, **/assembly.xml, **/.swagger-codegen-ignore, **/package.json, **/requirements.txt, **/setup.py, **/mta*.y*ml, **/.npmrc, Dockerfile, **/VERSION, **/version.txt, **/Gopkg.*, **/build.sbt, **/sbtDescriptor.json, **/project/*' + buildDescriptor: '**/pom.xml, **/.mvn/**, **/assembly.xml, **/.swagger-codegen-ignore, **/package.json, **/requirements.txt, **/setup.py, **/mta*.y*ml, **/.npmrc, Dockerfile, .hadolint.yaml, **/VERSION, **/version.txt, **/Gopkg.*, **/build.sbt, **/sbtDescriptor.json, **/project/*' deployDescriptor: '**/manifest*.y*ml, **/*.mtaext.y*ml, **/*.mtaext, **/xs-app.json, helm/**, *.y*ml' git: '.git/**' opa5: '**/*.*' diff --git a/test/groovy/HadolintExecuteTest.groovy b/test/groovy/HadolintExecuteTest.groovy new file mode 100644 index 000000000..852ee28a6 --- /dev/null +++ b/test/groovy/HadolintExecuteTest.groovy @@ -0,0 +1,67 @@ +import hudson.AbortException + +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.rules.RuleChain +import util.BasePiperTest +import util.JenkinsDockerExecuteRule +import util.JenkinsLoggingRule +import util.JenkinsReadYamlRule +import util.JenkinsShellCallRule +import util.JenkinsStepRule +import util.Rules + +import static org.junit.Assert.assertThat +import static org.hamcrest.Matchers.* + +class HadolintExecuteTest extends BasePiperTest { + + private ExpectedException thrown = new ExpectedException().none() + private JenkinsShellCallRule shellRule = new JenkinsShellCallRule(this) + private JenkinsDockerExecuteRule dockerExecuteRule = new JenkinsDockerExecuteRule(this) + private JenkinsStepRule stepRule = new JenkinsStepRule(this) + private JenkinsReadYamlRule yamlRule = new JenkinsReadYamlRule(this) + private JenkinsLoggingRule loggingRule = new JenkinsLoggingRule(this) + + @Rule + public RuleChain ruleChain = Rules + .getCommonRules(this) + .around(thrown) + .around(yamlRule) + .around(dockerExecuteRule) + .around(shellRule) + .around(stepRule) + .around(loggingRule) + + @Before + void init() { + helper.registerAllowedMethod 'stash', [String, String], { name, includes -> assertThat(name, is('hadolintConfiguration')); assertThat(includes, is('.hadolint.yaml')) } + helper.registerAllowedMethod 'fileExists', [String], { s -> s == './Dockerfile' } + helper.registerAllowedMethod 'checkStyle', [Map], { m -> assertThat(m.pattern, is('hadolint.xml')); return 'checkstyle' } + helper.registerAllowedMethod 'recordIssues', [Map], { m -> assertThat(m.tools, hasItem('checkstyle')) } + helper.registerAllowedMethod 'archiveArtifacts', [String], { String p -> assertThat('hadolint.xml', is(p)) } + } + + @Test + void testHadolintExecute() { + stepRule.step.hadolintExecute(script: nullScript, juStabUtils: utils, dockerImage: 'hadolint/hadolint:latest-debian', configurationUrl: 'https://github.wdf.sap.corp/raw/SGS/Hadolint-Dockerfile/master/.hadolint.yaml') + assertThat(dockerExecuteRule.dockerParams.dockerImage, is('hadolint/hadolint:latest-debian')) + assertThat(loggingRule.log, containsString("Unstash content: buildDescriptor")) + assertThat(shellRule.shell, + hasItems( + "curl --fail --location --output .hadolint.yaml https://github.wdf.sap.corp/raw/SGS/Hadolint-Dockerfile/master/.hadolint.yaml", + "hadolint ./Dockerfile --config .hadolint.yaml --format checkstyle > hadolint.xml" + ) + ) + } + + @Test + void testNoDockerfile() { + helper.registerAllowedMethod 'fileExists', [String], { false } + thrown.expect AbortException + thrown.expectMessage '[hadolintExecute] Dockerfile \'./Dockerfile\' is not found.' + stepRule.step.hadolintExecute(script: nullScript, juStabUtils: utils, dockerImage: 'hadolint/hadolint:latest-debian') + } +} diff --git a/vars/hadolintExecute.groovy b/vars/hadolintExecute.groovy new file mode 100644 index 000000000..33201e393 --- /dev/null +++ b/vars/hadolintExecute.groovy @@ -0,0 +1,104 @@ +import static com.sap.piper.Prerequisites.checkScript +import com.sap.piper.GenerateDocumentation +import com.sap.piper.ConfigurationHelper +import com.sap.piper.Utils +import groovy.transform.Field + +@Field def STEP_NAME = getClass().getName() +@Field Set GENERAL_CONFIG_KEYS = [ + /** + * Dockerfile to be used for the assessment. + */ + 'dockerFile', + /** + * Name of the docker image that should be used, in which node should be installed and configured. Default value is 'hadolint/hadolint:latest-debian'. + */ + 'dockerImage' +] +@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([ + /** + * Name of the configuration file used locally within the step. If a file with this name is detected as part of your repo downloading the central configuration via `configurationUrl` will be skipped. If you change the file's name make sure your stashing configuration also reflects this. + */ + 'configurationFile', + /** + * URL pointing to the .hadolint.yaml exclude configuration to be used for linting. Also have a look at `configurationFile` which could avoid central configuration download in case the file is part of your repository. + */ + 'configurationUrl', + /** + * Docker options to be set when starting the container. + */ + 'dockerOptions', + /** + * Quality Gates to fail the build, see [warnings-ng plugin documentation](https://github.com/jenkinsci/warnings-plugin/blob/master/doc/Documentation.md#quality-gate-configuration). + */ + 'qualityGates', + /** + * Name of the result file used locally within the step. + */ + 'reportFile' +]) +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS +/** + * Executes the Haskell Dockerfile Linter which is a smarter Dockerfile linter that helps you build [best practice](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) Docker images. + * The linter is parsing the Dockerfile into an abstract syntax tree (AST) and performs rules on top of the AST. + */ +@GenerateDocumentation +void call(Map parameters = [:]) { + handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) { + final script = checkScript(this, parameters) ?: this + final utils = parameters.juStabUtils ?: new Utils() + + // 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, STEP_CONFIG_KEYS) + .mixin(parameters, PARAMETER_KEYS) + .use() + + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], configuration) + + def existingStashes = utils.unstashAll(configuration.stashContent) + + if (!fileExists(configuration.dockerFile)) { + error "[${STEP_NAME}] Dockerfile '${configuration.dockerFile}' is not found." + } + + if(!fileExists(configuration.configurationFile) && configuration.configurationUrl) { + sh "curl --fail --location --output ${configuration.configurationFile} ${configuration.configurationUrl}" + if(existingStashes) { + def stashName = 'hadolintConfiguration' + stash name: stashName, includes: configuration.configurationFile + existingStashes += stashName + } + } + + def options = [ + "--config ${configuration.configurationFile}", + "--format checkstyle > ${configuration.reportFile}" + ] + + dockerExecute( + script: script, + dockerImage: configuration.dockerImage, + dockerOptions: configuration.dockerOptions, + stashContent: existingStashes + ) { + // HaDoLint status code is ignore, results will be handled by recordIssues / archiveArtifacts + def ignore = sh returnStatus: true, script: "hadolint ${configuration.dockerFile} ${options.join(' ')}" + + archiveArtifacts configuration.reportFile + recordIssues( + tools: [checkStyle(name: configuration.reportName, pattern: configuration.reportFile)], + qualityGates: configuration.qualityGates, + enabledForFailure: true, + blameDisabled: true + ) + } + } +}