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:
parent
bca5b8ccf1
commit
3b2e42c74f
82
documentation/docs/steps/containerExecuteStructureTests.md
Normal file
82
documentation/docs/steps/containerExecuteStructureTests.md
Normal 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|
|
@ -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
|
||||
|
@ -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: ''
|
||||
|
150
test/groovy/ContainerExecuteStructureTestsTest.groovy
Normal file
150
test/groovy/ContainerExecuteStructureTestsTest.groovy
Normal 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'))
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ class BasePiperTestContext {
|
||||
Script nullScript() {
|
||||
def nullScript = InvokerHelper.createScript(null, new Binding())
|
||||
nullScript.currentBuild = [:]
|
||||
nullScript.env = [:]
|
||||
LibraryLoadingTestExecutionListener.prepareObjectInterceptors(nullScript)
|
||||
return nullScript
|
||||
}
|
||||
|
132
vars/containerExecuteStructureTests.groovy
Normal file
132
vars/containerExecuteStructureTests.groovy
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user