mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-02-07 13:42:23 +02:00
add step pipelineRestartSteps (#337)
This step allows to restart a set of steps in order to retry in case of e.g. infrastructure failures which first need to be fixed. * update documentation
This commit is contained in:
parent
0e5ccabdae
commit
455461d3c1
75
documentation/docs/steps/pipelineRestartSteps.md
Normal file
75
documentation/docs/steps/pipelineRestartSteps.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# pipelineRestartSteps
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Support of restarting failed stages or steps in a pipeline is limited in Jenkins.
|
||||||
|
|
||||||
|
This has been documented in the [Jenkins Jira issue JENKINS-33846](https://issues.jenkins-ci.org/browse/JENKINS-33846).
|
||||||
|
|
||||||
|
For declarative pipelines there is a solution available which partially addresses this topic:
|
||||||
|
https://jenkins.io/doc/book/pipeline/running-pipelines/#restart-from-a-stage.
|
||||||
|
|
||||||
|
Nonetheless, still features are missing, so it can't be used in all cases.
|
||||||
|
The more complex Piper pipelines which share a state via [`commonPipelineEnvironment`](commonPipelineEnvironment.md) will for example not work with the standard _restart-from-stage_.
|
||||||
|
|
||||||
|
The step `pipelineRestartSteps` aims to address this gap and allows individual parts of a pipeline (e.g. a failed deployment) to be restarted.
|
||||||
|
|
||||||
|
This is done in a way that the pipeline waits for user input to restart the pipeline in case of a failure. In case this user input is not provided the pipeline stops after a timeout which can be configured.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
none
|
||||||
|
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Usage of pipeline step:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
pipelineRestartSteps (script: this) {
|
||||||
|
node {
|
||||||
|
//your steps ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! caution
|
||||||
|
Use `node` inside the step. If a `node` exists outside the step context, the `input` step which is triggered in the process will block a Jenkins executor.
|
||||||
|
|
||||||
|
In case you cannot use `node` inside this step, please choose the parameter `timeoutInSeconds` carefully!
|
||||||
|
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
| parameter | mandatory | default | possible values |
|
||||||
|
| ----------|-----------|---------|-----------------|
|
||||||
|
|script|yes|||
|
||||||
|
|sendMail|no|`true`||
|
||||||
|
|timeoutInSeconds|no|`900`||
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
* If `sendMail: true` the step `mailSendNotification` will be triggered in case of an error
|
||||||
|
* `timeoutInSeconds` defines the time period where the job waits for input. Default is 15 minutes. Once this time is passed the job enters state FAILED.
|
||||||
|
|
||||||
|
|
||||||
|
## 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||||
|
||||||
|
|sendMail|X|X|X|
|
||||||
|
|timeoutInSeconds|X|X|X|
|
||||||
|
|
||||||
|
## Return value
|
||||||
|
none
|
||||||
|
|
||||||
|
## Side effects
|
||||||
|
none
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
none
|
||||||
|
|
@ -21,6 +21,7 @@ nav:
|
|||||||
- mtaBuild: steps/mtaBuild.md
|
- mtaBuild: steps/mtaBuild.md
|
||||||
- neoDeploy: steps/neoDeploy.md
|
- neoDeploy: steps/neoDeploy.md
|
||||||
- pipelineExecute: steps/pipelineExecute.md
|
- pipelineExecute: steps/pipelineExecute.md
|
||||||
|
- pipelineRestartSteps: steps/pipelineRestartSteps.md
|
||||||
- pipelineStashFiles: steps/pipelineStashFiles.md
|
- pipelineStashFiles: steps/pipelineStashFiles.md
|
||||||
- prepareDefaultValues: steps/prepareDefaultValues.md
|
- prepareDefaultValues: steps/prepareDefaultValues.md
|
||||||
- seleniumExecuteTests: steps/seleniumExecuteTests.md
|
- seleniumExecuteTests: steps/seleniumExecuteTests.md
|
||||||
|
@ -193,6 +193,9 @@ steps:
|
|||||||
newmanRunCommand: "run ${config.newmanCollection} --environment '${config.newmanEnvironment}' --globals '${config.newmanGlobals}' --reporters junit,html --reporter-junit-export target/newman/TEST-${collectionDisplayName}.xml --reporter-html-export target/newman/TEST-${collectionDisplayName}.html"
|
newmanRunCommand: "run ${config.newmanCollection} --environment '${config.newmanEnvironment}' --globals '${config.newmanGlobals}' --reporters junit,html --reporter-junit-export target/newman/TEST-${collectionDisplayName}.xml --reporter-html-export target/newman/TEST-${collectionDisplayName}.html"
|
||||||
stashContent:
|
stashContent:
|
||||||
- 'tests'
|
- 'tests'
|
||||||
|
pipelineRestartSteps:
|
||||||
|
sendMail: true
|
||||||
|
timeoutInSeconds: 900
|
||||||
pipelineStashFilesAfterBuild:
|
pipelineStashFilesAfterBuild:
|
||||||
runOpaTests: false
|
runOpaTests: false
|
||||||
stashIncludes:
|
stashIncludes:
|
||||||
|
@ -2,8 +2,19 @@ package com.sap.piper
|
|||||||
|
|
||||||
import com.cloudbees.groovy.cps.NonCPS
|
import com.cloudbees.groovy.cps.NonCPS
|
||||||
import jenkins.model.Jenkins
|
import jenkins.model.Jenkins
|
||||||
|
import org.jenkinsci.plugins.workflow.steps.MissingContextVariableException
|
||||||
|
|
||||||
@NonCPS
|
@NonCPS
|
||||||
static def isPluginActive(pluginId) {
|
static def isPluginActive(pluginId) {
|
||||||
return Jenkins.instance.pluginManager.plugins.find { p -> p.isActive() && p.getShortName() == pluginId }
|
return Jenkins.instance.pluginManager.plugins.find { p -> p.isActive() && p.getShortName() == pluginId }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def nodeAvailable() {
|
||||||
|
try {
|
||||||
|
sh "echo 'Node is available!'"
|
||||||
|
} catch (MissingContextVariableException e) {
|
||||||
|
echo "No node context available."
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
126
test/groovy/PipelineRestartStepsTest.groovy
Normal file
126
test/groovy/PipelineRestartStepsTest.groovy
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
#!groovy
|
||||||
|
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.RuleChain
|
||||||
|
import util.*
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.containsString
|
||||||
|
import static org.hamcrest.CoreMatchers.is
|
||||||
|
import static org.junit.Assert.assertThat
|
||||||
|
|
||||||
|
class PipelineRestartStepsTest extends BasePiperTest {
|
||||||
|
|
||||||
|
private JenkinsErrorRule jer = new JenkinsErrorRule(this)
|
||||||
|
private JenkinsLoggingRule jlr = new JenkinsLoggingRule(this)
|
||||||
|
private JenkinsStepRule jsr = new JenkinsStepRule(this)
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public RuleChain chain = Rules.getCommonRules(this)
|
||||||
|
.around(new JenkinsReadYamlRule(this))
|
||||||
|
.around(jer)
|
||||||
|
.around(jlr)
|
||||||
|
.around(jsr)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testError() throws Exception {
|
||||||
|
|
||||||
|
def mailBuildResult = ''
|
||||||
|
helper.registerAllowedMethod('mailSendNotification', [Map.class], { m ->
|
||||||
|
mailBuildResult = m.buildResult
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
helper.registerAllowedMethod('timeout', [Map.class, Closure.class], { m, closure ->
|
||||||
|
assertThat(m.time, is(1))
|
||||||
|
assertThat(m.unit, is('SECONDS'))
|
||||||
|
return closure()
|
||||||
|
})
|
||||||
|
|
||||||
|
def iterations = 0
|
||||||
|
helper.registerAllowedMethod('input', [Map.class], { m ->
|
||||||
|
iterations ++
|
||||||
|
assertThat(m.message, is('Do you want to restart?'))
|
||||||
|
assertThat(m.ok, is('Restart'))
|
||||||
|
if (iterations > 1) {
|
||||||
|
throw new FlowInterruptedException()
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
jsr.step.pipelineRestartSteps ([
|
||||||
|
script: nullScript,
|
||||||
|
jenkinsUtilsStub: jenkinsUtils,
|
||||||
|
sendMail: true,
|
||||||
|
timeoutInSeconds: 1
|
||||||
|
|
||||||
|
]) {
|
||||||
|
throw new hudson.AbortException('I just created an error')
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
assertThat(jlr.log, containsString('ERROR occured: hudson.AbortException: I just created an error'))
|
||||||
|
assertThat(mailBuildResult, is('UNSTABLE'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testErrorNoMail() throws Exception {
|
||||||
|
|
||||||
|
def mailBuildResult = ''
|
||||||
|
helper.registerAllowedMethod('mailSendNotification', [Map.class], { m ->
|
||||||
|
mailBuildResult = m.buildResult
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
helper.registerAllowedMethod('timeout', [Map.class, Closure.class], { m, closure ->
|
||||||
|
assertThat(m.time, is(1))
|
||||||
|
assertThat(m.unit, is('SECONDS'))
|
||||||
|
return closure()
|
||||||
|
})
|
||||||
|
|
||||||
|
def iterations = 0
|
||||||
|
helper.registerAllowedMethod('input', [Map.class], { m ->
|
||||||
|
iterations ++
|
||||||
|
assertThat(m.message, is('Do you want to restart?'))
|
||||||
|
assertThat(m.ok, is('Restart'))
|
||||||
|
if (iterations > 1) {
|
||||||
|
throw new FlowInterruptedException()
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
jsr.step.pipelineRestartSteps ([
|
||||||
|
script: nullScript,
|
||||||
|
jenkinsUtilsStub: jenkinsUtils,
|
||||||
|
sendMail: false,
|
||||||
|
timeoutInSeconds: 1
|
||||||
|
|
||||||
|
]) {
|
||||||
|
throw new hudson.AbortException('I just created an error')
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
assertThat(jlr.log, containsString('ERROR occured: hudson.AbortException: I just created an error'))
|
||||||
|
assertThat(mailBuildResult, is(''))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuccess() throws Exception {
|
||||||
|
|
||||||
|
jsr.step.pipelineRestartSteps ([
|
||||||
|
script: nullScript,
|
||||||
|
jenkinsUtilsStub: jenkinsUtils,
|
||||||
|
sendMail: false,
|
||||||
|
timeoutInSeconds: 1
|
||||||
|
|
||||||
|
]) {
|
||||||
|
nullScript.echo 'This is a test'
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(jlr.log, containsString('This is a test'))
|
||||||
|
}
|
||||||
|
}
|
45
test/groovy/com/sap/piper/JenkinsUtilsTest.groovy
Normal file
45
test/groovy/com/sap/piper/JenkinsUtilsTest.groovy
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package com.sap.piper
|
||||||
|
|
||||||
|
import org.jenkinsci.plugins.workflow.steps.MissingContextVariableException
|
||||||
|
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.JenkinsLoggingRule
|
||||||
|
import util.JenkinsShellCallRule
|
||||||
|
import util.Rules
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.*
|
||||||
|
import static org.junit.Assert.assertThat
|
||||||
|
|
||||||
|
class JenkinsUtilsTest extends BasePiperTest {
|
||||||
|
private JenkinsLoggingRule jlr = new JenkinsLoggingRule(this)
|
||||||
|
private JenkinsShellCallRule jscr = new JenkinsShellCallRule(this)
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public RuleChain rules = Rules
|
||||||
|
.getCommonRules(this)
|
||||||
|
.around(jscr)
|
||||||
|
.around(jlr)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNodeAvailable() {
|
||||||
|
def result = jenkinsUtils.nodeAvailable()
|
||||||
|
assertThat(jscr.shell, contains("echo 'Node is available!'"))
|
||||||
|
assertThat(result, is(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNoNodeAvailable() {
|
||||||
|
helper.registerAllowedMethod('sh', [String.class], {s ->
|
||||||
|
throw new MissingContextVariableException(String.class)
|
||||||
|
})
|
||||||
|
|
||||||
|
def result = jenkinsUtils.nodeAvailable()
|
||||||
|
assertThat(jlr.log, containsString('No node context available.'))
|
||||||
|
assertThat(result, is(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -4,6 +4,7 @@ package util
|
|||||||
|
|
||||||
import com.lesfurets.jenkins.unit.BasePipelineTest
|
import com.lesfurets.jenkins.unit.BasePipelineTest
|
||||||
import com.sap.piper.GitUtils
|
import com.sap.piper.GitUtils
|
||||||
|
import com.sap.piper.JenkinsUtils
|
||||||
import com.sap.piper.Utils
|
import com.sap.piper.Utils
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@ -23,6 +24,9 @@ abstract class BasePiperTest extends BasePipelineTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
Utils utils
|
Utils utils
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
JenkinsUtils jenkinsUtils
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Before
|
@Before
|
||||||
void setUp() throws Exception {
|
void setUp() throws Exception {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import com.sap.piper.GitUtils
|
import com.sap.piper.GitUtils
|
||||||
|
import com.sap.piper.JenkinsUtils
|
||||||
import com.sap.piper.Utils
|
import com.sap.piper.Utils
|
||||||
import org.codehaus.groovy.runtime.InvokerHelper
|
import org.codehaus.groovy.runtime.InvokerHelper
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
@ -36,4 +37,11 @@ class BasePiperTestContext {
|
|||||||
LibraryLoadingTestExecutionListener.prepareObjectInterceptors(mockUtils)
|
LibraryLoadingTestExecutionListener.prepareObjectInterceptors(mockUtils)
|
||||||
return mockUtils
|
return mockUtils
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
JenkinsUtils mockJenkinsUtils() {
|
||||||
|
def mockJenkinsUtils = new JenkinsUtils()
|
||||||
|
LibraryLoadingTestExecutionListener.prepareObjectInterceptors(mockJenkinsUtils)
|
||||||
|
return mockJenkinsUtils
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
52
vars/pipelineRestartSteps.groovy
Normal file
52
vars/pipelineRestartSteps.groovy
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import com.sap.piper.JenkinsUtils
|
||||||
|
import com.sap.piper.ConfigurationHelper
|
||||||
|
import groovy.transform.Field
|
||||||
|
|
||||||
|
@Field String STEP_NAME = 'pipelineRestartSteps'
|
||||||
|
@Field Set STEP_CONFIG_KEYS = [
|
||||||
|
'sendMail',
|
||||||
|
'timeoutInSeconds'
|
||||||
|
]
|
||||||
|
@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS
|
||||||
|
|
||||||
|
def call(Map parameters = [:], body) {
|
||||||
|
handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters) {
|
||||||
|
def script = parameters.script ?: [commonPipelineEnvironment: commonPipelineEnvironment]
|
||||||
|
def jenkinsUtils = parameters.jenkinsUtilsStub ?: new JenkinsUtils()
|
||||||
|
// load default & individual configuration
|
||||||
|
Map config = ConfigurationHelper
|
||||||
|
.loadStepDefaults(this)
|
||||||
|
.mixinGeneralConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS)
|
||||||
|
.mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS)
|
||||||
|
.mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, STEP_CONFIG_KEYS)
|
||||||
|
.mixin(parameters, PARAMETER_KEYS)
|
||||||
|
.use()
|
||||||
|
|
||||||
|
def restart = true
|
||||||
|
while (restart) {
|
||||||
|
try {
|
||||||
|
body()
|
||||||
|
restart = false
|
||||||
|
} catch (Throwable err) {
|
||||||
|
echo "ERROR occured: ${err}"
|
||||||
|
if (config.sendMail)
|
||||||
|
if (jenkinsUtils.nodeAvailable()) {
|
||||||
|
mailSendNotification script: script, buildResult: 'UNSTABLE'
|
||||||
|
} else {
|
||||||
|
node {
|
||||||
|
mailSendNotification script: script, buildResult: 'UNSTABLE'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
timeout(time: config.timeoutInSeconds, unit: 'SECONDS') {
|
||||||
|
input message: 'Do you want to restart?', ok: 'Restart'
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
restart = false
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user