From c628d208c79199ea4334d13ee1a92f23e77df1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20A=C3=9Fmus?= Date: Fri, 7 Feb 2020 16:30:08 +0100 Subject: [PATCH] Implement archiving the debug report as step (#1152) * Implement archiving the debug report as step --- resources/debug_report.txt | 2 +- resources/default_pipeline_environment.yml | 2 + src/com/sap/piper/DebugReport.groovy | 43 ++++++----- test/groovy/DebugReportArchiveTest.groovy | 58 +++++++++++++++ .../com/sap/piper/DebugReportTest.groovy | 5 +- vars/debugReportArchive.groovy | 73 +++++++++++++++++++ 6 files changed, 162 insertions(+), 21 deletions(-) create mode 100644 test/groovy/DebugReportArchiveTest.groovy create mode 100644 vars/debugReportArchive.groovy diff --git a/resources/debug_report.txt b/resources/debug_report.txt index 472c8859e..d5e795a55 100644 --- a/resources/debug_report.txt +++ b/resources/debug_report.txt @@ -17,7 +17,7 @@ The log was generated at: `$utcTimestamp UTC` <% print failedBuild.get("stack_trace") ? "```" : "" %> <% print failedBuild.get("stack_trace").collect({each -> "${each}"}).join("\n") %> <% print failedBuild.get("stack_trace") ? "```" : "" %> -<% print failedBuild.get("fatal") ? "#### Failure was fatal." : "" %> +<% print failedBuild.get("fatal") ? "#### Failure was fatal." : "#### Failure was non-fatal." %> ## Pipeline Environment diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index b7211a7cf..5c4fa9878 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -199,6 +199,8 @@ steps: cloudFoundryCreateServiceKey: dockerImage: 'ppiper/cf-cli' dockerWorkspace: '/home/piper' + debugReportArchive: + shareConfidentialInformation: false detectExecuteScan: detect: projectVersion: '1' diff --git a/src/com/sap/piper/DebugReport.groovy b/src/com/sap/piper/DebugReport.groovy index f859131ec..1fd3de51e 100644 --- a/src/com/sap/piper/DebugReport.groovy +++ b/src/com/sap/piper/DebugReport.groovy @@ -5,7 +5,6 @@ import groovy.text.SimpleTemplateEngine @Singleton class DebugReport { - String fileName String projectIdentifier = null Map environment = ['environment': 'custom'] String buildTool = null @@ -20,7 +19,6 @@ class DebugReport { String sharedConfigFilePath = null Set additionalSharedLibraries = [] Map failedBuild = [:] - boolean shareConfidentialInformation /** * Initialize debug report information from the environment variables. @@ -85,14 +83,10 @@ class DebugReport { failedBuild.put('step', stepName) failedBuild.put('reason', err) failedBuild.put('stack_trace', err.getStackTrace()) - if (failedOnError) { - failedBuild.put('fatal', 'true') - } else { - failedBuild.remove('fatal') - } + failedBuild.put('fatal', failedOnError ? 'true' : 'false') } - String generateReport(Script script) { + Map generateReport(Script script, boolean shareConfidentialInformation) { String template = script.libraryResource 'debug_report.txt' if (!projectIdentifier) { @@ -107,19 +101,34 @@ class DebugReport { script.echo "Failed to retrieve Jenkins plugins for debug report (${t.getMessage()})" } - Map binding = getProperties() Date now = new Date() - binding.utcTimestamp = now.format('yyyy-MM-dd HH:mm', TimeZone.getTimeZone('UTC')) + Map binding = [ + 'projectIdentifier' : projectIdentifier, + 'environment' : environment, + 'buildTool': buildTool, + 'modulesMap' : modulesMap, + 'npmModules' : npmModules, + 'plugins' : plugins, + 'gitRepo' : gitRepo, + 'localExtensions' : localExtensions, + 'globalExtensionRepository' : globalExtensionRepository, + 'globalExtensions' : globalExtensions, + 'globalExtensionConfigurationFilePath' : globalExtensionConfigurationFilePath, + 'sharedConfigFilePath' : sharedConfigFilePath, + 'additionalSharedLibraries' : additionalSharedLibraries, + 'failedBuild' : failedBuild, + 'shareConfidentialInformation' : shareConfidentialInformation, + 'utcTimestamp' : now.format('yyyy-MM-dd HH:mm', TimeZone.getTimeZone('UTC')) + ] + String fileNameTimestamp = now.format('yyyy-MM-dd-HH-mm', TimeZone.getTimeZone('UTC')) + String fileNamePrefix = shareConfidentialInformation ? 'confidential' : 'redacted' - if (shareConfidentialInformation) { - fileName = "confidential_debug_log_${fileNameTimestamp}_${projectIdentifier}.txt" - } else { - fileName = "redacted_debug_log_${fileNameTimestamp}_${projectIdentifier}.txt" - } - - return fillTemplate(template, binding) + Map result = [:] + result.fileName = "${fileNamePrefix}_debug_log_${fileNameTimestamp}_${projectIdentifier}.txt" + result.contents = fillTemplate(template, binding) + return result } @NonCPS diff --git a/test/groovy/DebugReportArchiveTest.groovy b/test/groovy/DebugReportArchiveTest.groovy new file mode 100644 index 000000000..e3bec132c --- /dev/null +++ b/test/groovy/DebugReportArchiveTest.groovy @@ -0,0 +1,58 @@ +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import util.BasePiperTest +import util.JenkinsLoggingRule +import util.JenkinsReadYamlRule +import util.JenkinsStepRule +import util.JenkinsWriteFileRule +import util.Rules + +import static org.hamcrest.Matchers.containsString +import static org.junit.Assert.assertThat + +class DebugReportArchiveTest extends BasePiperTest { + + private JenkinsLoggingRule loggingRule = new JenkinsLoggingRule(this) + private JenkinsStepRule stepRule = new JenkinsStepRule(this) + private JenkinsWriteFileRule writeFileRule = new JenkinsWriteFileRule(this) + + @Rule + public RuleChain ruleChain = Rules + .getCommonRules(this) + .around(new JenkinsReadYamlRule(this)) + .around(loggingRule) + .around(writeFileRule) + .around(stepRule) + + @Before + void init() { + helper.registerAllowedMethod("libraryResource", [String.class], { path -> + + File resource = new File(new File('resources'), path) + if (resource.exists()) { + return resource.getText() + } + + return '' + }) + } + + @Test + void testDebugReportArchive() { + stepRule.step.debugReportArchive( + script: nullScript, + juStabUtils: utils, + stageName: 'test', + printToConsole: true + ) + + String debugReportSnippet = 'The debug log is generated with each build and should be included in every support request' + + assertThat(loggingRule.log, containsString('Successfully archived debug report')) + assertThat(loggingRule.log, containsString(debugReportSnippet)) + + assertThat(writeFileRule.files.find({ it.toString().contains('debug_log') }) as String, containsString(debugReportSnippet)) + } +} diff --git a/test/groovy/com/sap/piper/DebugReportTest.groovy b/test/groovy/com/sap/piper/DebugReportTest.groovy index c81280532..14e95d1ca 100644 --- a/test/groovy/com/sap/piper/DebugReportTest.groovy +++ b/test/groovy/com/sap/piper/DebugReportTest.groovy @@ -43,7 +43,7 @@ class DebugReportTest extends BasePiperTest { DebugReport.instance.initFromEnvironment(createEnv()) DebugReport.instance.setGitRepoInfo('GIT_URL' : 'git://url', 'GIT_LOCAL_BRANCH' : 'some-branch') - String debugReport = DebugReport.instance.generateReport(mockScript()) + String debugReport = DebugReport.instance.generateReport(mockScript(), false) Assert.assertTrue(debugReport.contains('## Pipeline Environment')) Assert.assertTrue(debugReport.contains('## Local Extensions')) @@ -57,9 +57,8 @@ class DebugReportTest extends BasePiperTest { void testLogOutputConfidential() { DebugReport.instance.initFromEnvironment(createEnv()) DebugReport.instance.setGitRepoInfo('GIT_URL' : 'git://url', 'GIT_LOCAL_BRANCH' : 'some-branch') - DebugReport.instance.shareConfidentialInformation = true - String debugReport = DebugReport.instance.generateReport(mockScript()) + String debugReport = DebugReport.instance.generateReport(mockScript(), true) Assert.assertTrue(debugReport.contains('## Pipeline Environment')) Assert.assertTrue(debugReport.contains('## Local Extensions')) diff --git a/vars/debugReportArchive.groovy b/vars/debugReportArchive.groovy new file mode 100644 index 000000000..ebb0d153b --- /dev/null +++ b/vars/debugReportArchive.groovy @@ -0,0 +1,73 @@ +import com.sap.piper.ConfigurationHelper +import com.sap.piper.DebugReport +import com.sap.piper.GenerateDocumentation +import com.sap.piper.Utils +import groovy.transform.Field + +import static com.sap.piper.Prerequisites.checkScript + +@Field def STEP_NAME = getClass().getName() +@Field Set GENERAL_CONFIG_KEYS = [] +@Field Set STEP_CONFIG_KEYS = [ + /** + * Flag to control whether potentially confidential information will be included in the + * debug_report.txt. Default value is `false`. Additional information written to the log + * when this flag is `true` includes MTA modules, NPM modules, the GitHub repository and + * branch, the global extension repository if used, a shared config file path, and all + * used global and local shared libraries. + * @possibleValues `true`, `false` + */ + 'shareConfidentialInformation' +] +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS + [ + /** + * Flag to enable printing the generated debug_report.txt also to the console. + */ + 'printToConsole' +] +/** + * Archives the debug_report.txt artifact which facilitates analyzing pipeline errors by collecting + * information about the Jenkins environment in which the pipeline was run. There is a single + * config option 'shareConfidentialInformation' to enable including (possibly) confidential + * information in the debug report, which could be helpful depending on the specific error. + * By default this information is not included. + */ +@GenerateDocumentation +void call(Map parameters = [:]) { + final script = checkScript(this, parameters) ?: this + try { + String stageName = parameters.stageName ?: env.STAGE_NAME + // ease handling extension + stageName = stageName?.replace('Declarative: ', '') + def utils = parameters.juStabUtils ?: new Utils() + + Map configuration = ConfigurationHelper.newInstance(this) + .loadStepDefaults() + .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) + .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixinStageConfig(script.commonPipelineEnvironment, stageName, STEP_CONFIG_KEYS) + .mixin(parameters, PARAMETER_KEYS) + .use() + + utils.pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], configuration) + + boolean shareConfidentialInformation = configuration?.get('shareConfidentialInformation') ?: false + + Map result = DebugReport.instance.generateReport(script, shareConfidentialInformation) + + if (parameters.printToConsole) { + echo result.contents + } + + script.writeFile file: result.fileName, text: result + script.archiveArtifacts artifacts: result.fileName + echo "Successfully archived debug report as '${result.fileName}'" + } catch (Exception e) { + println("WARNING: The debug report was not created, it threw the following error message:") + println("${e}") + } +}