1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00

Add step containerExecuteStructureTest (#441)

* add step containerExecuteStructureTest
* include PR-review feedback
* documentation
This commit is contained in:
Oliver Nocon 2019-01-31 09:39:13 +01:00 committed by GitHub
parent bca5b8ccf1
commit 3b2e42c74f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 383 additions and 4 deletions

View File

@ -0,0 +1,82 @@
# containerExecuteStructureTests
## Description
In this step [Container Structure Tests](https://github.com/GoogleContainerTools/container-structure-test) are executed.
This testing framework allows you to execute different test types against a Docker container, for example:
* Command tests (only if a Docker Deamon is available)
* File existence tests
* File content tests
* Metadata test
## Prerequisites
Test configuration is available.
## Example
```
containerExecuteStructureTests(
script: this,
testConfiguration: 'config.yml',
testImage: 'node:latest'
)
```
## Parameters
| parameter | mandatory | default | possible values |
| ----------|-----------|---------|-----------------|
|script|yes|||
|containerCommand|no|``||
|containerShell|no|``||
|dockerImage|yes|`ppiper/container-structure-test`||
|dockerOptions|no|`-u 0 --entrypoint=''`||
|failOnError|no|`true`|`true`, `false`|
|pullImage|no||`true`, `false`|
|stashContent|no|<ul><li>`tests`</li></ul>||
|testConfiguration|no|||
|testDriver|no|||
|testImage|no|||
|testReportFilePath|no|`cst-report.json`||
|verbose|no||`true`, `false`|
Details:
* `script` defines the global script environment of the Jenkinsfile run. Typically `this` is passed to this parameter. This allows the function to access the [`commonPipelineEnvironment`](commonPipelineEnvironment.md) for storing the measured duration.
* `containerCommand`: Only for Kubernetes environments: Command which is executed to keep container alive, defaults to '/usr/bin/tail -f /dev/null'
* containerShell: Only for Kubernetes environments: Shell to be used inside container, defaults to '/bin/sh'
* dockerImage: Docker image for code execution.
* dockerOptions: Options to be passed to Docker image when starting it (only relevant for non-Kubernetes case).
* failOnError: Defines the behavior, in case tests fail.
* pullImage: Only relevant for testDriver 'docker'.
* stashContent: If specific stashes should be considered for the tests, you can pass this via this parameter.
* testConfiguration: Container structure test configuration in yml or json format. You can pass a pattern in order to execute multiple tests.
* testDriver: Container structure test driver to be used for testing, please see [https://github.com/GoogleContainerTools/container-structure-test](https://github.com/GoogleContainerTools/container-structure-test) for details.
* testImage: Image to be tested
* testReportFilePath: Path and name of the test report which will be generated
* verbose: Print more detailed information into the log.
## Step configuration
We recommend to define values of step parameters via [config.yml file](../configuration.md).
In following sections the configuration is possible:
| parameter | general | step | stage |
| ----------|-----------|---------|-----------------|
|script||||
|containerCommand||X|X|
|containerShell||X|X|
|dockerImage||X|X|
|dockerOptions||X|X|
|failOnError||X|X|
|pullImage||X|X|
|stashContent||X|X|
|testConfiguration||X|X|
|testDriver||X|X|
|testImage||X|X|
|testReportFilePath||X|X|
|verbose|X|X|X|

View File

@ -9,6 +9,7 @@ nav:
- checksPublishResults: steps/checksPublishResults.md
- cloudFoundryDeploy: steps/cloudFoundryDeploy.md
- commonPipelineEnvironment: steps/commonPipelineEnvironment.md
- containerExecuteStructureTests: steps/containerExecuteStructureTests.md
- dockerExecute: steps/dockerExecute.md
- dockerExecuteOnKubernetes: steps/dockerExecuteOnKubernetes.md
- durationMeasure: steps/durationMeasure.md

View File

@ -143,6 +143,15 @@ steps:
mtaDeployPlugin:
dockerImage: 's4sdk/docker-cf-cli'
dockerWorkspace: '/home/piper'
containerExecuteStructureTests:
containerCommand: '/busybox/tail -f /dev/null'
containerShell: '/busybox/sh'
dockerImage: 'ppiper/container-structure-test'
dockerOptions: "-u 0 --entrypoint=''"
failOnError: true
stashContent:
- 'tests'
testReportFilePath: 'cst-report.json'
dockerExecute:
stashContent: []
dockerExecuteOnKubernetes:
@ -261,7 +270,7 @@ steps:
opensourceConfiguration: '**/srcclr.yml, **/vulas-custom.properties, **/.nsprc, **/.retireignore, **/.retireignore.json, **/.snyk'
pipelineConfigAndTests: '.pipeline/**'
securityDescriptor: '**/xs-security.json'
tests: '**/pom.xml, **/*.json, **/*.xml, **/src/**, **/node_modules/**, **/specs/**, **/env/**, **/*.js'
tests: '**/pom.xml, **/*.json, **/*.xml, **/src/**, **/node_modules/**, **/specs/**, **/env/**, **/*.js, **/tests/**'
stashExcludes:
buildDescriptor: '**/node_modules/**/package.json'
deployDescriptor: ''

View File

@ -0,0 +1,150 @@
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.rules.RuleChain
import util.*
import static org.hamcrest.Matchers.*
import static org.junit.Assert.assertThat
class ContainerExecuteStructureTestsTest extends BasePiperTest {
private ExpectedException thrown = ExpectedException.none()
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(new JenkinsReadYamlRule(this))
.around(thrown)
.around(jedr)
.around(jscr)
.around(jlr)
.around(jsr) // needs to be activated after jedr, otherwise executeDocker is not mocked
@Before
void init() throws Exception {
helper.registerAllowedMethod('stash', [String.class], null)
helper.registerAllowedMethod("findFiles", [Map.class], { map ->
def files
if(map.glob == 'notFound.json')
files = []
else if(map.glob == 'cst/*.yml')
files = [
new File("cst/test1.yml"),
new File("cst/test2.yml")
]
else
files = [new File(map.glob)]
return files.toArray()
})
}
@Test
void testExecuteContainterStructureTestsDefault() throws Exception {
helper.registerAllowedMethod('readFile', [String.class], {s ->
return '{testResult: true}'
})
jsr.step.containerExecuteStructureTests(
script: nullScript,
juStabUtils: utils,
testConfiguration: 'cst/*.yml',
testImage: 'myRegistry/myImage:myTag'
)
// asserts
assertThat(jscr.shell, hasItem(allOf(
stringContainsInOrder(['#!/busybox/sh', 'container-structure-test', '--config']),
containsString("--config cst${File.separator}test1.yml"),
containsString("--config cst${File.separator}test2.yml"),
containsString('--driver docker'),
containsString('--image myRegistry/myImage:myTag'),
containsString('--test-report cst-report.json'),
)))
//currently no default Docker image
assertThat(jedr.dockerParams.dockerImage, is('ppiper/container-structure-test'))
assertThat(jedr.dockerParams.dockerOptions, is("-u 0 --entrypoint=''"))
assertThat(jedr.dockerParams.containerCommand, is('/busybox/tail -f /dev/null'))
assertThat(jedr.dockerParams.containerShell, is('/busybox/sh'))
assertThat(jlr.log, containsString('{testResult: true}'))
assertThat(jscr.shell, hasItem('docker pull myRegistry/myImage:myTag'))
}
@Test
void testExecuteContainterStructureTestsK8S() throws Exception {
def envDefault = nullScript.env
nullScript.env = [ON_K8S: 'true']
jsr.step.containerExecuteStructureTests(
script: nullScript,
juStabUtils: utils,
containerCommand: '/busybox/tail -f /dev/null',
containerShell: '/bin/sh',
dockerImage: 'myRegistry:55555/pathTo/myImage:myTag',
testConfiguration: 'cst/*.yml',
testImage: 'myRegistry/myImage:myTag'
)
nullScript.env = envDefault
// asserts
assertThat(jscr.shell, hasItem(allOf(
stringContainsInOrder(['#!/bin/sh', 'container-structure-test', '--config']),
containsString("--config cst${File.separator}test1.yml"),
containsString("--config cst${File.separator}test2.yml"),
containsString('--driver tar'),
containsString('--image myRegistry/myImage:myTag'),
containsString('--test-report cst-report.json'),
)))
assertThat(jedr.dockerParams.dockerImage, is('myRegistry:55555/pathTo/myImage:myTag'))
assertThat(jedr.dockerParams.containerCommand, is('/busybox/tail -f /dev/null'))
assertThat(jscr.shell, not(hasItem('docker pull myRegistry/myImage:myTag')))
}
@Test
void testExecuteContainterStructureTestsError() throws Exception {
helper.registerAllowedMethod('readFile', [String.class], {s ->
return '{testResult: true}'
})
helper.registerAllowedMethod('sh', [String.class], {s ->
if (s.startsWith('#!/busybox/sh\ncontainer-structure-test test')) {
throw new GroovyRuntimeException('shell call failed')
}
return null
})
thrown.expectMessage('shell call failed')
jsr.step.containerExecuteStructureTests(
script: nullScript,
juStabUtils: utils,
containerCommand: '/busybox/tail -f /dev/null',
containerShell: '/busybox/sh',
testConfiguration: 'cst/*.yml',
testImage: 'myRegistry/myImage:myTag'
)
}
@Test
void testExecuteContainterStructureTestsErrorNoFailure() throws Exception {
helper.registerAllowedMethod('readFile', [String.class], {s ->
return '{testResult: true}'
})
helper.registerAllowedMethod('sh', [String.class], {s ->
if (s.startsWith('#!/busybox/sh\ncontainer-structure-test test')) {
throw new GroovyRuntimeException('shell call failed')
}
return null
})
jsr.step.containerExecuteStructureTests(
script: nullScript,
juStabUtils: utils,
containerCommand: '/busybox/tail -f /dev/null',
containerShell: '/busybox/sh',
failOnError: false,
testConfiguration: 'cst/*.yml',
testImage: 'myRegistry/myImage:myTag'
)
assertThat(jlr.log, containsString('Test execution failed'))
}
}

View File

@ -16,6 +16,7 @@ class BasePiperTestContext {
Script nullScript() {
def nullScript = InvokerHelper.createScript(null, new Binding())
nullScript.currentBuild = [:]
nullScript.env = [:]
LibraryLoadingTestExecutionListener.prepareObjectInterceptors(nullScript)
return nullScript
}

View File

@ -0,0 +1,132 @@
import static com.sap.piper.Prerequisites.checkScript
import com.sap.piper.ConfigurationHelper
import com.sap.piper.Utils
import groovy.transform.Field
@Field String STEP_NAME = getClass().getName()
@Field Set GENERAL_CONFIG_KEYS = [
/**
* Print more detailed information into the log.
* @possibleValues `true`, `false`
*/
'verbose'
]
@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([
/**
* Only for Kubernetes environments: Command which is executed to keep container alive, defaults to '/usr/bin/tail -f /dev/null'
*/
'containerCommand',
/**
* Only for Kubernetes environments: Shell to be used inside container, defaults to '/bin/sh'
*/
'containerShell',
/**
* Docker image for code execution.
*/
'dockerImage',
/**
* Options to be passed to Docker image when starting it (only relevant for non-Kubernetes case).
*/
'dockerOptions',
/**
* Defines the behavior, in case tests fail.
* @possibleValues `true`, `false`
*/
'failOnError',
/**
* Only relevant for testDriver 'docker'.
* @possibleValues `true`, `false`
*/
'pullImage',
/**
* If specific stashes should be considered for the tests, you can pass this via this parameter.
*/
'stashContent',
/**
* Container structure test configuration in yml or json format. You can pass a pattern in order to execute multiple tests.
*/
'testConfiguration',
/**
* Container structure test driver to be used for testing, please see https://github.com/GoogleContainerTools/container-structure-test for details.
*/
'testDriver',
/**
* Image to be tested
*/
'testImage',
/**
* Path and name of the test report which will be generated
*/
'testReportFilePath',
])
@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS
void call(Map parameters = [:]) {
handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) {
def script = checkScript(this, parameters) ?: this
def utils = parameters?.juStabUtils ?: new Utils()
// load default & individual configuration
Map config = 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)
.addIfEmpty('testDriver', Boolean.valueOf(script.env.ON_K8S) ? 'tar' : 'docker')
.addIfNull('pullImage', !Boolean.valueOf(script.env.ON_K8S))
.withMandatoryProperty('dockerImage')
.use()
utils.pushToSWA([step: STEP_NAME], config)
config.stashContent = utils.unstashAll(config.stashContent)
List testConfig = findFiles(glob: config.testConfiguration)?.toList()
if (testConfig.isEmpty()) {
error "[${STEP_NAME}] No test description found with pattern '${config.testConfiguration}'"
} else {
echo "[${STEP_NAME}] Found files ${testConfig}"
}
def testConfigArgs = ''
testConfig.each {conf ->
testConfigArgs += "--config ${conf} "
}
//workaround for non-working '--pull' option in version 1.7.0 of container-structure-tests, see https://github.com/GoogleContainerTools/container-structure-test/issues/193
if (config.pullImage) {
if (config.verbose) echo "[${STEP_NAME}] Pulling image since configuration option pullImage is set to '${config.pullImage}'"
sh "docker pull ${config.testImage}"
}
try {
dockerExecute(
script: script,
containerCommand: config.containerCommand,
containerShell: config.containerShell,
dockerImage: config.dockerImage,
dockerOptions: config.dockerOptions,
stashContent: config.stashContent
) {
sh """#!${config.containerShell?:'/bin/sh'}
container-structure-test test ${testConfigArgs} --driver ${config.testDriver} --image ${config.testImage} --test-report ${config.testReportFilePath}${config.verbose ? ' --verbosity debug' : ''}"""
}
} catch (err) {
echo "[${STEP_NAME}] Test execution failed"
script.currentBuild.result = 'UNSTABLE'
if (config.failOnError) throw err
} finally {
echo "${readFile(config.testReportFilePath)}"
archiveArtifacts artifacts: config.testReportFilePath, allowEmptyArchive: true
}
}
}

View File

@ -84,12 +84,13 @@ void executeOnPod(Map config, utils, Closure body) {
if (config.containerShell) {
containerParams.shell = config.containerShell
}
echo "ContainerConfig: ${containerParams}"
container(containerParams){
try {
utils.unstashAll(config.stashContent)
body()
} finally {
stashWorkspace(config, 'container')
stashWorkspace(config, 'container', true)
}
}
} else {
@ -103,11 +104,14 @@ void executeOnPod(Map config, utils, Closure body) {
}
}
private String stashWorkspace(config, prefix) {
private String stashWorkspace(config, prefix, boolean chown = false) {
def stashName = "${prefix}-${config.uniqueId}"
try {
// Every dockerImage used in the dockerExecuteOnKubernetes should have user id 1000
sh "chown -R 1000:1000 ."
if (chown) {
sh """#!${config.containerShell?:'/bin/sh'}
chown -R 1000:1000 ."""
}
stash(
name: stashName,
includes: config.stashIncludes.workspace,