jenkins-pipeline-shared-libraries/vars/util.groovy (413 lines of code) (raw):

/** * * @param projectUrl the github project url */ def getProject(String projectUrl) { return (projectUrl =~ /((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?(github.com\/))([\w\.@\:\/\-~]+)(\.git)(\/)?/)[0][8] } /** * * @param projectUrl the github project url */ def getGroup(String projectUrl) { return getProjectGroupName(getProject(projectUrl))[0] } /** * Returns an array containing group and name * * @param project the project * @param defaultGroup the default project group. Optional. */ def getProjectGroupName(String project, String defaultGroup = "apache") { def projectNameGroup = project.split("\\/") def group = projectNameGroup.size() > 1 ? projectNameGroup[0] : defaultGroup def name = projectNameGroup.size() > 1 ? projectNameGroup[1] : project return [group, name] } /** * Returns the path to the project dir * @param projectGroupName * @return */ def getProjectDirPath(String project, String defaultGroup = "apache") { def projectGroupName = getProjectGroupName(project, defaultGroup) return "${env.WORKSPACE}/${projectGroupName[0]}_${projectGroupName[1]}" } /** * * Stores git information into an env variable to be retrievable at any point of the pipeline * * @param projectName to store commit */ def storeGitInformation(String projectName) { def gitInformationReport = env.GIT_INFORMATION_REPORT ? "${env.GIT_INFORMATION_REPORT}; " : "" gitInformationReport += "${projectName}=${githubscm.getCommit().replace(';', '').replace('=', '')} Branch [${githubscm.getBranch().replace(';', '').replace('=', '')}] Remote [${githubscm.getRemoteInfo('origin', 'url').replace(';', '').replace('=', '')}]" env.GIT_INFORMATION_REPORT = gitInformationReport def gitHashes = env.GIT_INFORMATION_HASHES ? "${env.GIT_INFORMATION_HASHES};" : "" gitHashes += "${projectName}=${githubscm.getCommitHash()}" env.GIT_INFORMATION_HASHES = gitHashes } /** * * prints GIT_INFORMATION_REPORT variable */ def printGitInformationReport() { if (env.GIT_INFORMATION_REPORT?.trim()) { def result = env.GIT_INFORMATION_REPORT.split(';').inject([:]) { map, token -> token.split('=').with { key, value -> map[key.trim()] = value.trim() } map } def report = ''' ------------------------------------------ GIT INFORMATION REPORT ------------------------------------------ ''' result.each { key, value -> report += "${key}: ${value}\n" } println report } else { println '[WARNING] The variable GIT_INFORMATION_REPORT does not exist' } } /* * Get the next major/minor/micro version, with a specific suffix if needed. * The version string needs to be in the form X.Y.Z */ def getNextVersion(String version, String type, String suffix = 'SNAPSHOT', boolean resetSubVersions = true) { assert ['major', 'minor', 'micro'].contains(type) Integer[] versionSplit = parseVersion(version) if (versionSplit != null) { int majorVersion = versionSplit[0] + (type == 'major' ? 1 : 0) int minorVersion = resetSubVersions && type == 'major' ? 0 : (versionSplit[1] + (type == 'minor' ? 1 : 0)) int microVersion = resetSubVersions && (type == 'major' || type == 'minor') ? 0 : (versionSplit[2] + (type == 'micro' ? 1 : 0)) return "${majorVersion}.${minorVersion}.${microVersion}${suffix ? '-' + suffix : ''}" } else { return null } } String getMajorMinorVersion(String version) { try { String[] versionSplit = version.split("\\.") return "${versionSplit[0]}.${versionSplit[1]}" } catch (err) { println "[ERROR] ${version} cannot be reduced to Major.minor" throw err } } /* * It parses a version string, which needs to be in the format X.Y.Z or X.Y.Z.suffix and returns the 3 numbers * in an array. The optional suffix must not be numeric. * <p> * Valid version examples: * 1.0.0 * 1.0.0.Final */ Integer[] parseVersion(String version) { String[] versionSplit = version.split("\\.") boolean hasNonNumericSuffix = versionSplit.length == 4 && !(versionSplit[3].isNumber()) if (versionSplit.length == 3 || hasNonNumericSuffix) { if (versionSplit[0].isNumber() && versionSplit[1].isNumber() && versionSplit[2].isNumber()) { Integer[] vs = new Integer[3] vs[0] = Integer.parseInt(versionSplit[0]) vs[1] = Integer.parseInt(versionSplit[1]) vs[2] = Integer.parseInt(versionSplit[2]) return vs } else { error "Version ${version} is not in the required format. The major, minor, and micro parts should contain only numeric characters." } } else { error "Version ${version} is not in the required format X.Y.Z or X.Y.Z.suffix." } } String getReleaseBranchFromVersion(String version) { Integer[] versionSplit = parseVersion(version) return "${versionSplit[0]}.${versionSplit[1]}.x" } String calculateTargetReleaseBranch(String currentReleaseBranch, int addToMajor = 0, int addToMinor = 0) { String targetBranch = currentReleaseBranch String [] versionSplit = targetBranch.split("\\.") if (versionSplit.length == 3 && versionSplit[0].isNumber() && versionSplit[1].isNumber() && (versionSplit[2] == 'x' || versionSplit[2] == 'x-prod')) { Integer newMajor = Integer.parseInt(versionSplit[0]) + addToMajor Integer newMinor = Integer.parseInt(versionSplit[1]) + addToMinor targetBranch = "${newMajor}.${newMinor}.${versionSplit[2]}" } else { println "Cannot parse targetBranch as release branch so going further with current value: ${targetBranch}" } return targetBranch } /** * It prepares the environment to avoid problems with plugins. For example files from SCM pipeline are deleted during checkout */ def prepareEnvironment() { println """ [INFO] Preparing Environment [INFO] Copying WORKSPACE content env folder """ def envFolderName = '.ci-env' if (fileExists("${env.WORKSPACE}/${envFolderName}")) { println "[WARNING] folder ${env.WORKSPACE}/${envFolderName} already exist, won't create env folder again." } else { dir(env.WORKSPACE) { sh "mkdir ${envFolderName}" sh "cp -r `ls -A | grep -v '${envFolderName}'` ${envFolderName}/" } } } /* * Generate a hash composed of alphanumeric characters (lowercase) of a given size */ String generateHash(int size) { String alphabet = (('a'..'z') + ('0'..'9')).join("") def random = new Random() return (1..size).collect { alphabet[random.nextInt(alphabet.length())] }.join("") } String generateTempFile() { return sh(returnStdout: true, script: 'mktemp').trim() } String generateTempFolder() { return sh(returnStdout: true, script: 'mktemp -d').trim() } void executeWithCredentialsMap(Map credentials, Closure closure) { if (credentials.token) { withCredentials([string(credentialsId: credentials.token, variable: 'QUAY_TOKEN')]) { closure() } } else if (credentials.usernamePassword) { withCredentials([usernamePassword(credentialsId: credentials.usernamePassword, usernameVariable: 'QUAY_USER', passwordVariable: 'QUAY_TOKEN')]) { closure() } } else { error 'No credentials given to execute the given closure' } } void cleanNode(String containerEngine = '') { println '[INFO] Clean workspace' cleanWs(disableDeferredWipeout: true) println '[INFO] Workspace cleaned' println '[INFO] Cleanup Maven artifacts' maven.cleanRepository() println '[INFO] .m2/repository cleaned' if (containerEngine) { println "[INFO] Cleanup ${containerEngine} containers/images" cloud.cleanContainersAndImages(containerEngine) } } def spaceLeft() { dir(env.WORKSPACE) { println '[INFO] space left on the machine' sh 'df -h' println '[INFO] space of /home/jenkins' sh "du -h -d1 /home/jenkins" println '[INFO] space of workspace' sh "du -h -d3 /home/jenkins/workspace" } } def replaceInAllFilesRecursive(String findPattern, String oldValueSedPattern, String newSedValue) { sh "find . -name '${findPattern}' -type f -exec sed -i 's/${oldValueSedPattern}/${newSedValue}/g' {} \\;" } /* * Removes any partial downloaded dependencies from .m2 if the previous run was interrupted and no post actions were * executed (cleanRepository()) and a new build is executed on the same machine */ def rmPartialDeps(){ dir("${env.WORKSPACE}/.m2") { sh "find . -regex \".*\\.part\\(\\.lock\\)?\" -exec rm -rf {} \\;" } } String retrieveConsoleLog(int numberOfLines = 100, String buildUrl = "${BUILD_URL}") { return sh(returnStdout: true, script: "wget --no-check-certificate -qO - ${buildUrl}consoleText | tail -n ${numberOfLines}") } String archiveConsoleLog(String id = '', int numberOfLines = 100, String buildUrl = "${BUILD_URL}") { String filename = "${id ? "${id}_" : ''}console.log" sh "rm -rf ${filename}" writeFile(text: retrieveConsoleLog(numberOfLines, buildUrl), file: filename) archiveArtifacts(artifacts: filename) } def retrieveTestResults(String buildUrl = "${BUILD_URL}") { return readJSON(text: sh(returnStdout: true, script: "wget --no-check-certificate -qO - ${buildUrl}testReport/api/json?depth=1")) } def retrieveFailedTests(String buildUrl = "${BUILD_URL}") { def testResults = retrieveTestResults(buildUrl) def allCases = [] testResults.suites?.each { testSuite -> allCases.addAll(testSuite.cases) } def failedTests = [] testResults.suites?.each { testSuite -> testSuite.cases?.each { testCase -> if (!['PASSED', 'SKIPPED', 'FIXED'].contains(testCase.status)) { def failedTest = [:] boolean hasSameNameCases = allCases.findAll { it.name == testCase.name && it.className == testCase.className }.size() > 1 failedTest.status = testCase.status // Retrieve class name fullClassName = testCase.className int lastIndexOf = fullClassName.lastIndexOf('.') packageName = fullClassName.substring(0, lastIndexOf) className = fullClassName.substring(lastIndexOf + 1) failedTest.name = testCase.name failedTest.packageName = packageName failedTest.className = className failedTest.enclosingBlockNames = testSuite.enclosingBlockNames?.reverse()?.join(' / ') failedTest.fullName = "${packageName}.${className}.${failedTest.name}" // If other cases have the same className / name, Jenkins uses the enclosingBlockNames for the URL distinction if (hasSameNameCases && testSuite.enclosingBlockNames) { failedTest.fullName = "${testSuite.enclosingBlockNames.reverse().join(' / ')} / ${failedTest.fullName}" } // Construct test url String urlLeaf = '' // If other cases have the same className / name, Jenkins uses the enclosingBlockNames for the URL distinction if (hasSameNameCases && testSuite.enclosingBlockNames) { urlLeaf += testSuite.enclosingBlockNames.reverse().join('___') } urlLeaf += urlLeaf ? '___' : urlLeaf urlLeaf += "${failedTest.name == "(?)" ? "___" : failedTest.name}/" urlLeaf = urlLeaf.replaceAll(' ', '_') .replaceAll('&', '_') .replaceAll('-', '_') failedTest.url = "${buildUrl}testReport/${packageName}/${className}/${urlLeaf}" failedTest.details = [null, 'null'].contains(testCase.errorDetails) ? '' : testCase.errorDetails failedTest.stacktrace = [null, 'null'].contains(testCase.errorStackTrace) ? '' : testCase.errorStackTrace failedTests.add(failedTest) } } } return failedTests } String retrieveArtifact(String artifactPath, String buildUrl = "${BUILD_URL}") { String finalUrl = "${buildUrl}artifact/${artifactPath}" String httpCode = sh(returnStdout: true, script: "curl -o /dev/null --silent -Iw '%{http_code}' ${finalUrl}") return httpCode == "200" ? sh(returnStdout: true, script: "wget --no-check-certificate -qO - ${finalUrl}") : '' } def retrieveJobInformation(String buildUrl = "${BUILD_URL}") { return readJSON(text: sh(returnStdout: true, script: "wget --no-check-certificate -qO - ${buildUrl}api/json?depth=0")) } boolean isJobResultSuccess(String jobResult) { return jobResult == 'SUCCESS' } boolean isJobResultFailure(String jobResult) { return jobResult == 'FAILURE' } boolean isJobResultAborted(String jobResult) { return jobResult == 'ABORTED' } boolean isJobResultUnstable(String jobResult) { return jobResult == 'UNSTABLE' } /* * Return the build/test summary of a job * * outputStyle possibilities: 'ZULIP' (default), 'GITHUB' */ String getMarkdownTestSummary(String jobId = '', String additionalInfo = '', String buildUrl = "${BUILD_URL}", String outputStyle = 'ZULIP') { def jobInfo = retrieveJobInformation(buildUrl) // Check if any *_console.log is available as artifact first String defaultConsoleLogId = 'Console Logs' Map consoleLogs = jobInfo.artifacts?.collect { it.fileName } .findAll { it.endsWith('console.log') } .collectEntries { filename -> int index = filename.lastIndexOf('_') String logId = index > 0 ? filename.substring(0, index) : defaultConsoleLogId return [ (logId) : retrieveArtifact(filename, buildUrl) ] } ?: [ (defaultConsoleLogId) : retrieveConsoleLog(50, buildUrl)] String jobResult = jobInfo.result String summary = """ ${jobId ? "**${jobId} job**" : 'Job'} ${formatBuildNumber(outputStyle, BUILD_NUMBER)} was: **${jobResult}** """ if (!isJobResultSuccess(jobResult)) { summary += "Possible explanation: ${getResultExplanationMessage(jobResult)}\n" } if (additionalInfo) { summary += """ ${additionalInfo} """ } if (!isJobResultSuccess(jobResult)) { boolean testResultsFound = false summary += "\nPlease look here: ${buildUrl}display/redirect" try { def testResults = retrieveTestResults(buildUrl) def failedTests = retrieveFailedTests(buildUrl) testResultsFound=true summary += """ \n**Test results:** - PASSED: ${testResults.passCount} - FAILED: ${testResults.failCount} """ summary += 'GITHUB'.equalsIgnoreCase(outputStyle) ? """ Those are the test failures: ${failedTests.size() <= 0 ? 'none' : '\n'}${failedTests.collect { failedTest -> return """<details> <summary><a href="${failedTest.url}">${failedTest.fullName}</a></summary> ${formatTextForHtmlDisplay(failedTest.details ?: failedTest.stacktrace)} </details>""" }.join('\n')} """ : """ Those are the test failures: ${failedTests.size() <= 0 ? 'none' : '\n'}${failedTests.collect { failedTest -> return """```spoiler [${failedTest.fullName}](${failedTest.url}) ${failedTest.details ?: failedTest.stacktrace} ```""" }.join('\n')} """ } catch (err) { echo 'No test results found' } // Display console logs if no test results found if (!(jobResult == 'UNSTABLE' && testResultsFound)) { summary += 'GITHUB'.equalsIgnoreCase(outputStyle) ? """ See console log: ${consoleLogs.collect { key, value -> return """<details> <summary><b>${key}</b></summary> ${formatTextForHtmlDisplay(value)} </details> """ }.join('')}""" : """ See console log: ${consoleLogs.collect { key, value -> return """```spoiler ${key} ${value} ``` """ }.join('')}""" } } return summary } String getResultExplanationMessage(String jobResult) { switch (jobResult) { case 'SUCCESS': return 'Do I need to explain ?' case 'UNSTABLE': return 'This should be test failures' case 'FAILURE': return 'Pipeline failure or project build failure' case 'ABORTED': return 'Most probably a timeout, please review' default: return 'Woops ... I don\'t know about this result value ... Please ask maintainer.' } } String formatTextForHtmlDisplay(String text) { return text.replaceAll('\n', '<br/>') } String formatBuildNumber(String outputStyle, String buildNumber) { return 'GITHUB'.equalsIgnoreCase(outputStyle) ? "`#${buildNumber}`" : "#${buildNumber}" } /** * Encode the provided string value in the provided encoding * @param value string to encode * @param encoding [default UTF-8] * @return the encoded string */ String encode(String value, String encoding='UTF-8') { return URLEncoder.encode(value, encoding) } /** * Serialize the parameters converting a Map into an URL query string, like: * {A: 1, B: 2} --> 'A=1&B=2' * @param params key-value map representation of the parameters * @return URL query string */ String serializeQueryParams(Map params) { return params.collect { "${it.getKey()}=${encode(it.getValue() as String)}" }.join('&') } /** * Execute the provided closure within Kerberos authentication context * @param keytabId id of the keytab to be used * @param closure code to run in the kerberos auth context * @param domain kerberos domain to look for into the keytab * @param retry number of max retries to perform if kinit fails */ def withKerberos(String keytabId, Closure closure, String domain = 'REDHAT.COM', int nRetries = 5) { withCredentials([file(credentialsId: keytabId, variable: 'KEYTAB_FILE')]) { env.KERBEROS_PRINCIPAL = sh(returnStdout: true, script: "klist -kt $KEYTAB_FILE |grep $domain | awk -F' ' 'NR==1{print \$4}' ").trim() if (!env.KERBEROS_PRINCIPAL?.trim()) { throw new Exception("[ERROR] found blank KERBEROS_PRINCIPAL, kerberos authetication failed.") } // check if kerberos authentication already exists with provided principal def currentPrincipal = sh(returnStdout: true, script: "klist | grep -i 'Default principal' | awk -F':' 'NR==1{print \$2}' ").trim() if (currentPrincipal != env.KERBEROS_PRINCIPAL) { def kerberosStatus = 0 for (int i = 0; i < nRetries; i++) { kerberosStatus = sh(returnStatus: true, script: "kinit ${env.KERBEROS_PRINCIPAL} -kt $KEYTAB_FILE") if (kerberosStatus == 0) { // exit at first success break } } // if the kerberos status is still != 0 after nRetries throw exception if (kerberosStatus != 0) { throw new Exception("[ERROR] kinit failed with non-zero status.") } } else { println "[INFO] ${env.KERBEROS_PRINCIPAL} already authenticated, skipping kinit." } closure() } } def runWithPythonVirtualEnv(String cmd, String virtualEnvName, boolean returnStdout = false) { return sh(returnStdout: returnStdout, script: """ source ~/virtenvs/${virtualEnvName}/bin/activate ${cmd} """) } int getJobDurationInSeconds() { long startTimestamp = retrieveJobInformation().timestamp long currentTimestamp = new Date().getTime() return (int) ((currentTimestamp - startTimestamp) / 1000) } String displayDurationFromSeconds(int durationInSec) { String result = '' int seconds = durationInSec int minutes = durationInSec / 60 if (minutes > 0) { seconds = seconds - minutes * 60 int hours = minutes / 60 if (hours > 0) { minutes = minutes - hours*60 result += "${hours}h" } result += "${minutes}m" } result += "${seconds}s" return result } /** * Method to wait for dockerd to be started. Put in initial pipeline stages to make sure all starts in time. */ void waitForDocker() { sleep(10) // give it some ahead time not to invoke docker exec immediately after container start sh 'wait-for-docker.sh' // script in kogito-ci-build image itself put in /usr/local/bin } /** * Method to wrap original label and exclude nodes that were marked as faulty in some of the parent folders. * Example usage in agent block: `label util.avoidFaultyNodes('ubuntu')` * `FAULTY_NODES` environment variable is inherited down the folder tree and available in jobs. * @param label Node label to be used. If empty, 'ubuntu' will be used as default * @return Node label extended with an expression ensuring to exclude nodes marked as faulty. */ String avoidFaultyNodes(String label = 'ubuntu') { if (label.isEmpty()) { label = 'ubuntu' } String faultyNodesString = env.FAULTY_NODES if((faultyNodesString == null) || faultyNodesString.isEmpty()) { return label } String[] faultyNodes = faultyNodesString.split(',') String result = "(${label}) && !(${String.join(' || ', faultyNodes)})" return result.toString() }