From e8b9d5a1b533761d7aebec40c38b360e56ac981c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Jul 2024 21:14:51 +0000 Subject: [PATCH] Add `publishDistributionBuildResults` and `publishIntegTestResults` to publish the OpenSearch build and integration test results (#459) Signed-off-by: Prudhvi Godithi (cherry picked from commit cd984fa9e99dce286590c81fb9fea2b09066dea4) Signed-off-by: github-actions[bot] --- README.md | 2 + build.gradle | 2 +- ...TestPublishDistributionBuildResults.groovy | 212 ++++++++++++++++ .../TestPublishIntegTestResults.groovy | 214 ++++++++++++++++ vars/publishDistributionBuildResults.groovy | 170 +++++++++++++ vars/publishIntegTestResults.groovy | 233 ++++++++++++++++++ 6 files changed, 832 insertions(+), 1 deletion(-) create mode 100644 tests/jenkins/TestPublishDistributionBuildResults.groovy create mode 100644 tests/jenkins/TestPublishIntegTestResults.groovy create mode 100644 vars/publishDistributionBuildResults.groovy create mode 100644 vars/publishIntegTestResults.groovy diff --git a/README.md b/README.md index d19ffc6a..61ef66d1 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ lib = library(identifier: 'jenkins@', retriever: modernSCM([ | [publishGradleCheckTestResults.groovy](./vars/publishGradleCheckTestResults.groovy) | This library runs part of Gradle Check and publishes the failed test data to the [OpenSearch Metrics Cluster](https://metrics.opensearch.org/_dashboards/app/dashboards#/view/e5e64d40-ed31-11ee-be99-69d1dbc75083). | [gradleCheckFlakyTestDetector.groovy](./vars/gradleCheckFlakyTestDetector.groovy) | This library detects the flaky tests from [OpenSearch Metrics Cluster](https://metrics.opensearch.org/_dashboards/app/dashboards#/view/e5e64d40-ed31-11ee-be99-69d1dbc75083) and generates a test report. | [gradleCheckFlakyTestGitHubIssue.groovy](./vars/gradleCheckFlakyTestGitHubIssue.groovy) | This library is used in [gradleCheckFlakyTestDetector.groovy](./vars/gradleCheckFlakyTestDetector.groovy) to create/edit the GitHub Issue using the generated test report. +| [publishDistributionBuildResults.groovy](./vars/publishDistributionBuildResults.groovy) | This library is used for publishing the OpenSearch Project Distribution build results to the OpenSearch Metrics cluster. +| [publishIntegTestResults.groovy](./vars/publishIntegTestResults.groovy) | This library is used for publishing the OpenSearch Project Integration Test results to the OpenSearch Metrics cluster. ## Contributing diff --git a/build.gradle b/build.gradle index bad04427..53a65ded 100644 --- a/build.gradle +++ b/build.gradle @@ -127,7 +127,7 @@ jacocoTestReport { } } -String version = '6.6.1' +String version = '6.7.0' task updateVersion { doLast { diff --git a/tests/jenkins/TestPublishDistributionBuildResults.groovy b/tests/jenkins/TestPublishDistributionBuildResults.groovy new file mode 100644 index 00000000..0d99ade6 --- /dev/null +++ b/tests/jenkins/TestPublishDistributionBuildResults.groovy @@ -0,0 +1,212 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package jenkins.tests + +import org.junit.Before +import static org.mockito.Mockito.* +import groovy.json.JsonSlurper +import org.junit.Test +import groovy.json.JsonOutput + +class TestPublishDistributionBuildResults extends BuildPipelineTest { + + @Override + @Before + void setUp() { + super.setUp() + binding.setVariable('currentBuild', [ + number: 123, + startTimeInMillis: System.currentTimeMillis(), + ]) + binding.setVariable('env', [ + RUN_DISPLAY_URL: 'http://example.com/build/123' + ]) + binding.setVariable('sh', { cmd -> println cmd }) + binding.setVariable('readFile', { filePath -> 'components:\n- name: component1\n repository: repo1\n ref: ref1' }) + binding.setVariable('writeFile', { params -> println params.text }) + binding.setVariable('withCredentials', { creds, closure -> closure() }) + helper.registerAllowedMethod("withAWS", [Map, Closure], { args, closure -> + closure.delegate = delegate + return helper.callClosure(closure) + }) + binding.setVariable('curl', { params -> println params }) + } + + @Test + void testIndexBuildData() { + def indexName = 'test-index' + def testRecordsFile = 'test-records.ndjson' + + def script = loadScript('vars/publishDistributionBuildResults.groovy') + + def calledCommands = new ArrayList() + script.metaClass.sh = { String command -> + calledCommands << command + if (command.contains("curl -I")) { + return "HTTP/1.1 200 OK" + } else if (command.contains("curl -s -XPUT") && command.contains("test-index")) { + return '{"acknowledged":true}' + } else if (command.contains("curl -XPOST") && command.contains("test-index")) { + return '{"took":10, "errors":false}' + } else { + throw new IllegalArgumentException("Unexpected command: $command") + } + } + + script.indexFailedTestData(indexName, testRecordsFile) + + def expectedCommandBlock = '''set +e + set +x + echo "INDEX NAME IS test-index" + INDEX_MAPPING='{ + "mappings": { + "properties": { + "component": { + "type": "keyword" + }, + "component_repo": { + "type": "keyword" + }, + "component_ref": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "distribution_build_number": { + "type": "integer" + }, + "distribution_build_url": { + "type": "keyword" + }, + "build_start_time": { + "type": "date", + "format": "epoch_millis" + }, + "rc": { + "type": "keyword" + }, + "rc_number": { + "type": "integer" + }, + "component_category": { + "type": "keyword" + }, + "component_build_result": { + "type": "keyword" + } + } + } + }' + curl -I "METRICS_HOST_URL/test-index" --aws-sigv4 "aws:amz:us-east-1:es" --user "null:null" -H "x-amz-security-token:null" | grep -E "HTTP\\/[0-9]+(\\.[0-9]+)? 200" + if [ $? -eq 0 ]; then + echo "Index already exists. Indexing Results" + else + echo "Index does not exist. Creating..." + create_index_response=$(curl -s -XPUT "METRICS_HOST_URL/test-index" --aws-sigv4 "aws:amz:us-east-1:es" --user "null:null" -H "x-amz-security-token:null" -H 'Content-Type: application/json' -d "${INDEX_MAPPING}") + if [[ $create_index_response == *'"acknowledged":true'* ]]; then + echo "Index created successfully." + else + echo "Failed to create index. Error message: $create_index_response" + exit 1 + fi + fi + if [ -s test-records.ndjson ]; then + echo "File Exists, indexing results." + curl -XPOST "METRICS_HOST_URL/test-index/_bulk" --aws-sigv4 "aws:amz:us-east-1:es" --user "null:null" -H "x-amz-security-token:null" -H "Content-Type: application/x-ndjson" --data-binary "@test-records.ndjson" + else + echo "File Does not exist. No tests records to process." + fi''' + assert calledCommands.size() == 1 + assert normalizeString(calledCommands[0]) == normalizeString(expectedCommandBlock) + } + + @Test + void testGenerateJson() { + def script = loadScript('vars/publishDistributionBuildResults.groovy') + def result = script.generateJson( + 'component1', 'repo1', 'ref1', '1.0', 123, + 'http://example.com/build/123', System.currentTimeMillis(), 'rc1', 1, 'test-category', 'failed' + ) + + def parsedResult = new JsonSlurper().parseText(result) + def expectedJson = [ + component: 'component1', + component_repo: 'repo1', + component_ref: 'ref1', + version: '1.0', + distribution_build_number: 123, + distribution_build_url: 'http://example.com/build/123', + // Ignore build_start_time for comparison + rc: 'rc1', + rc_number: 1, + component_category: 'test-category', + component_build_result: 'failed' + ] + + // Remove the dynamic field for comparison + parsedResult.remove('build_start_time') + assert parsedResult == expectedJson + } + + def normalizeString(String str) { + return str.replaceAll(/\s+/, " ").trim() + } + + @Test + void testGenerateAndAppendJson() { + def script = loadScript('vars/publishDistributionBuildResults.groovy') + // Test valid parameters + def indexName = "test-index" + def component = "componentA" + def componentRepo = "repoA" + def componentRef = "refA" + def version = "1.0.0" + def distributionBuildNumber = "123" + def distributionBuildUrl = "http://example.com/build/123" + def buildStartTime = "2024-07-19T00:00:00Z" + def rc = true + def rcNumber = "RC1" + def componentCategory = "categoryA" + def status = "success" + + def result = script.generateAndAppendJson(indexName, component, componentRepo, componentRef, version, distributionBuildNumber, distributionBuildUrl, buildStartTime, rc, rcNumber, componentCategory, status) + def expectedJson = JsonOutput.toJson([ + component: component, + component_repo: componentRepo, + component_ref: componentRef, + version: version, + distribution_build_number: distributionBuildNumber, + distribution_build_url: distributionBuildUrl, + build_start_time: buildStartTime, + rc: rc, + rc_number: rcNumber, + component_category: componentCategory, + component_build_result: status + ]) + assert result == expectedJson + + result = script.generateAndAppendJson(indexName, null, null, null, null, null, null, null, null, null, null, null) + expectedJson = JsonOutput.toJson([ + component: null, + component_repo: null, + component_ref: null, + version: null, + distribution_build_number: null, + distribution_build_url: null, + build_start_time: null, + rc: null, + rc_number: null, + component_category: null, + component_build_result: null + ]) + assert result == expectedJson + } +} diff --git a/tests/jenkins/TestPublishIntegTestResults.groovy b/tests/jenkins/TestPublishIntegTestResults.groovy new file mode 100644 index 00000000..dff548b6 --- /dev/null +++ b/tests/jenkins/TestPublishIntegTestResults.groovy @@ -0,0 +1,214 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package jenkins.tests + +import org.junit.Before +import static org.mockito.Mockito.* +import groovy.json.JsonSlurper +import org.junit.Test + + +class TestPublishIntegTestResults extends BuildPipelineTest { + + @Override + @Before + void setUp() { + super.setUp() + binding.setVariable('currentBuild', [ + number: 123, + startTimeInMillis: System.currentTimeMillis(), + ]) + binding.setVariable('env', [ + RUN_DISPLAY_URL: 'http://example.com/build/123' + ]) + binding.setVariable('sh', { cmd -> println cmd }) + binding.setVariable('readFile', { filePath -> 'components:\n- name: component1\n configs:\n - name: with-security\n status: pass\n - name: without-security\n status: fail' }) + binding.setVariable('writeFile', { params -> println params.text }) + binding.setVariable('withCredentials', { creds, closure -> closure() }) + helper.registerAllowedMethod("withAWS", [Map, Closure], { args, closure -> + closure.delegate = delegate + return helper.callClosure(closure) + }) + binding.setVariable('curl', { params -> println params }) + } + + @Test + void testIndexFailedTestData() { + def indexName = 'test-index' + def testRecordsFile = 'test-records.ndjson' + + def script = loadScript('vars/publishIntegTestResults.groovy') + + def calledCommands = new ArrayList() + script.metaClass.sh = { String command -> + calledCommands << command + if (command.contains("curl -I")) { + return "HTTP/1.1 200 OK" + } else if (command.contains("curl -s -XPUT") && command.contains("test-index")) { + return '{"acknowledged":true}' + } else if (command.contains("curl -XPOST") && command.contains("test-index")) { + return '{"took":10, "errors":false}' + } else { + throw new IllegalArgumentException("Unexpected command: $command") + } + } + + script.indexFailedTestData(indexName, testRecordsFile) + + def expectedCommandBlock = '''set +e + set +x + echo "INDEX NAME IS test-index" + INDEX_MAPPING='{ + "mappings": { + "properties": { + "component": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "integ_test_build_number": { + "type": "integer" + }, + "integ_test_build_url": { + "type": "keyword" + }, + "distribution_build_number": { + "type": "integer" + }, + "distribution_build_url": { + "type": "keyword" + }, + "build_start_time": { + "type": "date", + "format": "epoch_millis" + }, + "rc": { + "type": "keyword" + }, + "rc_number": { + "type": "integer" + }, + "platform": { + "type": "keyword" + }, + "architecture": { + "type": "keyword" + }, + "distribution": { + "type": "keyword" + }, + "component_category": { + "type": "keyword" + }, + "component_build_result": { + "type": "keyword" + }, + "test_report_manifest_yml": { + "type": "keyword" + }, + "with_security": { + "type": "keyword" + }, + "with_security_build_yml": { + "type": "keyword" + }, + "with_security_cluster_stdout": { + "type": "keyword" + }, + "with_security_cluster_stderr": { + "type": "keyword" + }, + "without_security": { + "type": "keyword" + }, + "without_security_build_yml": { + "type": "keyword" + }, + "without_security_cluster_stdout": { + "type": "keyword" + }, + "without_security_cluster_stderr": { + "type": "keyword" + } + } + } + }' + curl -I "METRICS_HOST_URL/test-index" --aws-sigv4 "aws:amz:us-east-1:es" --user "null:null" -H "x-amz-security-token:null" | grep -E "HTTP\\/[0-9]+(\\.[0-9]+)? 200" + if [ $? -eq 0 ]; then + echo "Index already exists. Indexing Results" + else + echo "Index does not exist. Creating..." + create_index_response=$(curl -s -XPUT "METRICS_HOST_URL/test-index" --aws-sigv4 "aws:amz:us-east-1:es" --user "null:null" -H "x-amz-security-token:null" -H 'Content-Type: application/json' -d "${INDEX_MAPPING}") + if [[ $create_index_response == *'"acknowledged":true'* ]]; then + echo "Index created successfully." + else + echo "Failed to create index. Error message: $create_index_response" + exit 1 + fi + fi + if [ -s test-records.ndjson ]; then + echo "File Exists, indexing results." + curl -XPOST "METRICS_HOST_URL/test-index/_bulk" --aws-sigv4 "aws:amz:us-east-1:es" --user "null:null" -H "x-amz-security-token:null" -H "Content-Type: application/x-ndjson" --data-binary "@test-records.ndjson" + else + echo "File Does not exist. No tests records to process." + fi''' + assert calledCommands.size() == 1 + assert normalizeString(calledCommands[0]) == normalizeString(expectedCommandBlock) + } + + + @Test + void testGenerateJson() { + def script = loadScript('vars/publishIntegTestResults.groovy') + def result = script.generateJson( + 'component1', '1.0', 123, + 'http://example.com/build/123', 456, 'http://example.com/distribution/456', + System.currentTimeMillis(), 'rc1', 1, 'linux', 'x64', 'tar', 'test-category', + 'failed', 'http://example.com/test-report.yml', 'pass', 'yml1', ['stdout1'], ['stderr1'], + 'fail', 'yml2', ['stdout2'], ['stderr2'] + ) + + def parsedResult = new JsonSlurper().parseText(result) + def expectedJson = [ + component: 'component1', + version: '1.0', + integ_test_build_number: 123, + integ_test_build_url: 'http://example.com/build/123', + distribution_build_number: 456, + distribution_build_url: 'http://example.com/distribution/456', + // Ignore build_start_time for comparison + rc: 'rc1', + rc_number: 1, + platform: 'linux', + architecture: 'x64', + distribution: 'tar', + component_category: 'test-category', + component_build_result: 'failed', + test_report_manifest_yml: 'http://example.com/test-report.yml', + with_security: 'pass', + with_security_build_yml: 'yml1', + with_security_cluster_stdout: ['stdout1'], + with_security_cluster_stderr: ['stderr1'], + without_security: 'fail', + without_security_build_yml: 'yml2', + without_security_cluster_stdout: ['stdout2'], + without_security_cluster_stderr: ['stderr2'] + ] + + // Remove the dynamic field for comparison + parsedResult.remove('build_start_time') + assert parsedResult == expectedJson + } + + def normalizeString(String str) { + return str.replaceAll(/\s+/, " ").trim() + } + } \ No newline at end of file diff --git a/vars/publishDistributionBuildResults.groovy b/vars/publishDistributionBuildResults.groovy new file mode 100644 index 00000000..ed601bbc --- /dev/null +++ b/vars/publishDistributionBuildResults.groovy @@ -0,0 +1,170 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** Library to fetch the failing Integration test details at the end of Integration Test Jenkins build and index the results to OpenSearch Metrics cluster. + * + * @param Map args = [:] args A map of the following parameters. + * @param args.rc - If the integration tests are running on an RC. + * @param args.rcNumber - The RC number against which the integration test is executed. + * @param args.failureMessages - Failure message retrieved from buildFailureMessage() method. + * @param args.passMessages - Passing message retrieved from buildFailureMessage() method. Used to get the passed components list. + * @param args.componentCategory - The OpenSearch or OpenSearch Dashboards plugin + * @param args.inputManifestPath - Path to input manifest. + */ + +import groovy.json.JsonOutput +import java.text.SimpleDateFormat +import java.util.Date + +void call(Map args = [:]) { + def distributionBuildNumber = currentBuild.number + def distributionBuildUrl = env.RUN_DISPLAY_URL + def buildStartTime = currentBuild.startTimeInMillis + def currentDate = new Date() + def formattedDate = new SimpleDateFormat("MM-yyyy").format(currentDate) + def rc = args.rc + def rcNumber = args.rcNumber.toInteger() + def indexName = "opensearch-distribution-build-results-${formattedDate}" + def failureMessages = args.failureMessages + def passMessages = args.passMessages + def componentCategory = args.componentCategory + def inputManifest = readYaml(file: args.inputManifestPath) + def version = inputManifest.build.version + def finalJsonDoc = "" + def failedComponents = extractUniqueComponents(failureMessages, /(?<=\bError building\s)\S+/) + def passedComponents = extractUniqueComponents(passMessages, /(?<=\bSuccessfully built\s)\S+/) + inputManifest.components.each { component -> + if (failedComponents.contains(component.name)) { + println("Component ${component.name} failed") + def jsonData = generateAndAppendJson(indexName, component.name, component.repository, component.ref, + version, distributionBuildNumber, distributionBuildUrl, + buildStartTime, rc, rcNumber, componentCategory, "failed" + ) + finalJsonDoc += "{\"index\": {\"_index\": \"${indexName}\"}}\n${jsonData}\n" + } else if (passedComponents.contains(component.name)) { + println("Component ${component.name} passed") + def jsonData = generateAndAppendJson(indexName, component.name, component.repository, component.ref, + version, distributionBuildNumber, distributionBuildUrl, + buildStartTime, rc, rcNumber, componentCategory, "passed" + ) + finalJsonDoc += "{\"index\": {\"_index\": \"${indexName}\"}}\n${jsonData}\n" + } + } + writeFile file: "test-records.json", text: finalJsonDoc + def fileContents = readFile(file: "test-records.json").trim() + indexFailedTestData(indexName, "test-records.json") + +} + +void indexFailedTestData(indexName, testRecordsFile) { + withCredentials([ + string(credentialsId: 'jenkins-health-metrics-account-number', variable: 'METRICS_HOST_ACCOUNT'), + string(credentialsId: 'jenkins-health-metrics-cluster-endpoint', variable: 'METRICS_HOST_URL') + ]) { + withAWS(role: 'OpenSearchJenkinsAccessRole', roleAccount: "${METRICS_HOST_ACCOUNT}", duration: 900, roleSessionName: 'jenkins-session') { + def awsAccessKey = env.AWS_ACCESS_KEY_ID + def awsSecretKey = env.AWS_SECRET_ACCESS_KEY + def awsSessionToken = env.AWS_SESSION_TOKEN + sh """ + set +e + set +x + echo "INDEX NAME IS ${indexName}" + INDEX_MAPPING='{ + "mappings": { + "properties": { + "component": { + "type": "keyword" + }, + "component_repo": { + "type": "keyword" + }, + "component_ref": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "distribution_build_number": { + "type": "integer" + }, + "distribution_build_url": { + "type": "keyword" + }, + "build_start_time": { + "type": "date", + "format": "epoch_millis" + }, + "rc": { + "type": "keyword" + }, + "rc_number": { + "type": "integer" + }, + "component_category": { + "type": "keyword" + }, + "component_build_result": { + "type": "keyword" + } + } + } + }' + curl -I "${METRICS_HOST_URL}/${indexName}" --aws-sigv4 \"aws:amz:us-east-1:es\" --user \"${awsAccessKey}:${awsSecretKey}\" -H \"x-amz-security-token:${awsSessionToken}\" | grep -E "HTTP\\/[0-9]+(\\.[0-9]+)? 200" + if [ \$? -eq 0 ]; then + echo "Index already exists. Indexing Results" + else + echo "Index does not exist. Creating..." + create_index_response=\$(curl -s -XPUT "${METRICS_HOST_URL}/${indexName}" --aws-sigv4 \"aws:amz:us-east-1:es\" --user \"${awsAccessKey}:${awsSecretKey}\" -H \"x-amz-security-token:${awsSessionToken}\" -H 'Content-Type: application/json' -d "\${INDEX_MAPPING}") + if [[ \$create_index_response == *'"acknowledged":true'* ]]; then + echo "Index created successfully." + else + echo "Failed to create index. Error message: \$create_index_response" + exit 1 + fi + fi + if [ -s ${testRecordsFile} ]; then + echo "File Exists, indexing results." + curl -XPOST "${METRICS_HOST_URL}/$indexName/_bulk" --aws-sigv4 \"aws:amz:us-east-1:es\" --user \"${awsAccessKey}:${awsSecretKey}\" -H \"x-amz-security-token:${awsSessionToken}\" -H "Content-Type: application/x-ndjson" --data-binary "@${testRecordsFile}" + else + echo "File Does not exist. No tests records to process." + fi + """ + } + } +} + +def generateJson(component, componentRepo, componentRef, version, distributionBuildNumber, distributionBuildUrl, buildStartTime, rc, rcNumber, componentCategory, componentResult) { + def json = [ + component: component, + component_repo: componentRepo, + component_ref: componentRef, + version: version, + distribution_build_number: distributionBuildNumber, + distribution_build_url: distributionBuildUrl, + build_start_time: buildStartTime, + rc: rc, + rc_number: rcNumber, + component_category: componentCategory, + component_build_result: componentResult, + ] + return JsonOutput.toJson(json) +} + +def extractUniqueComponents(messages, regex) { + messages.collect { it.find(regex) }.unique() +} + +def generateAndAppendJson(indexName, component, componentRepo, componentRef, version, distributionBuildNumber, distributionBuildUrl, buildStartTime, rc, rcNumber, componentCategory, status) { + def jsonData = generateJson( + component, componentRepo, componentRef, version, + distributionBuildNumber, distributionBuildUrl, buildStartTime, + rc, rcNumber, componentCategory, status + ) + return jsonData +} diff --git a/vars/publishIntegTestResults.groovy b/vars/publishIntegTestResults.groovy new file mode 100644 index 00000000..a8efb3f3 --- /dev/null +++ b/vars/publishIntegTestResults.groovy @@ -0,0 +1,233 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** Library to fetch the failing Integration test details at the end of Integration Test Jenkins build and index the results to OpenSearch Metrics cluster. + * + * @param Map args = [:] args A map of the following parameters. + * @param args.version - The version against which the integration test is executed. + * @param args.distributionBuildNumber - The jenkins distribution build number. + * @param args.distributionBuildUrl - The jenkins distribution build number. + * @param args.rc - If the integration tests are running on an RC. + * @param args.rcNumber - The RC number against which the integration test is executed. + * @param args.platform - The platform of the integration test build. + * @param args.architecture - The architecture of the integration test build. + * @param args.distribution - The distribution of the integration test build. + * @param args.testReportManifestYml - The generated test report YAML file using test report workflow. + */ + +import groovy.json.JsonOutput +import java.text.SimpleDateFormat +import java.util.Date + +void call(Map args = [:]) { + + + // To ensure the test TestOpenSearchIntegTest from opensearch-build repo passes. + def isNullOrEmpty = { str -> + str == null || (str instanceof String && str.trim().isEmpty()) + } + if (isNullOrEmpty(args.version) || isNullOrEmpty(args.distributionBuildNumber) || isNullOrEmpty(args.distributionBuildUrl) || + isNullOrEmpty(args.rcNumber) || isNullOrEmpty(args.rc) || isNullOrEmpty(args.platform) || + isNullOrEmpty(args.architecture) || isNullOrEmpty(args.distribution) || isNullOrEmpty(args.testReportManifestYml)) { + return null + } + + + def version = args.version.toString() + def integTestBuildNumber = currentBuild.number + def integTestBuildUrl = env.RUN_DISPLAY_URL + def distributionBuildNumber = args.distributionBuildNumber + def distributionBuildUrl = args.distributionBuildUrl + def buildStartTime = currentBuild.startTimeInMillis + def currentDate = new Date() + def formattedDate = new SimpleDateFormat("MM-yyyy").format(currentDate) + def rc = args.rc + def rcNumber = args.rcNumber.toInteger() + def platform = args.platform + def architecture = args.architecture + def distribution = args.distribution + def testReportManifestYml = args.testReportManifestYml + def testReportManifestYmlUrl = "https://ci.opensearch.org/ci/dbc/integ-test/${version}/${distributionBuildNumber}/${platform}/${architecture}/${distribution}/test-results/${integTestBuildNumber}/integ-test/test-report.yml" + def manifestFile = readFile testReportManifestYml + def manifest = readYaml text: manifestFile + def indexName = "opensearch-integration-test-results-${formattedDate}" + def finalJsonDoc = "" + manifest.components.each { component -> + def componentName = component.name + def componentCategory = manifest.name + def withSecurity = component.configs.find { it.name == 'with-security' }?.status?.toLowerCase() ?: 'unknown' + def withoutSecurity = component.configs.find { it.name == 'without-security' }?.status?.toLowerCase() ?: 'unknown' + def componentResult = (withSecurity == 'fail' || withoutSecurity == 'fail') ? 'failed' : 'passed' + def withSecurityYml = component.configs.find { it.name == 'with-security' }?.yml ?: '' + def withSecurityStdout = component.configs.find { it.name == 'with-security' }?.cluster_stdout ?: [] + def withSecurityStderr = component.configs.find { it.name == 'with-security' }?.cluster_stderr ?: [] + def withoutSecurityYml = component.configs.find { it.name == 'without-security' }?.yml ?: '' + def withoutSecurityStdout = component.configs.find { it.name == 'without-security' }?.cluster_stdout ?: [] + def withoutSecurityStderr = component.configs.find { it.name == 'without-security' }?.cluster_stderr ?: [] + def jsonContent = generateJson( + componentName, version, integTestBuildNumber, + integTestBuildUrl, distributionBuildNumber, distributionBuildUrl, + buildStartTime, rc, rcNumber, + platform, architecture, distribution, + componentCategory, componentResult, testReportManifestYmlUrl, + withSecurity, withSecurityYml, withSecurityStdout, + withSecurityStderr, withoutSecurity, withoutSecurityYml, + withoutSecurityStdout, withoutSecurityStderr + ) + finalJsonDoc += "{\"index\": {\"_index\": \"${indexName}\"}}\n" + "${jsonContent}\n" + } + writeFile file: "test-records.json", text: finalJsonDoc + def fileContents = readFile(file: "test-records.json").trim() + indexFailedTestData(indexName, "test-records.json") +} + +boolean argCheck(String str) { return (str == null || str.allWhitespace || str.isEmpty()) } + +void indexFailedTestData(indexName, testRecordsFile) { + withCredentials([ + string(credentialsId: 'jenkins-health-metrics-account-number', variable: 'METRICS_HOST_ACCOUNT'), + string(credentialsId: 'jenkins-health-metrics-cluster-endpoint', variable: 'METRICS_HOST_URL') + ]) { + withAWS(role: 'OpenSearchJenkinsAccessRole', roleAccount: "${METRICS_HOST_ACCOUNT}", duration: 900, roleSessionName: 'jenkins-session') { + def awsAccessKey = env.AWS_ACCESS_KEY_ID + def awsSecretKey = env.AWS_SECRET_ACCESS_KEY + def awsSessionToken = env.AWS_SESSION_TOKEN + sh """ + set +e + set +x + echo "INDEX NAME IS ${indexName}" + INDEX_MAPPING='{ + "mappings": { + "properties": { + "component": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "integ_test_build_number": { + "type": "integer" + }, + "integ_test_build_url": { + "type": "keyword" + }, + "distribution_build_number": { + "type": "integer" + }, + "distribution_build_url": { + "type": "keyword" + }, + "build_start_time": { + "type": "date", + "format": "epoch_millis" + }, + "rc": { + "type": "keyword" + }, + "rc_number": { + "type": "integer" + }, + "platform": { + "type": "keyword" + }, + "architecture": { + "type": "keyword" + }, + "distribution": { + "type": "keyword" + }, + "component_category": { + "type": "keyword" + }, + "component_build_result": { + "type": "keyword" + }, + "test_report_manifest_yml": { + "type": "keyword" + }, + "with_security": { + "type": "keyword" + }, + "with_security_build_yml": { + "type": "keyword" + }, + "with_security_cluster_stdout": { + "type": "keyword" + }, + "with_security_cluster_stderr": { + "type": "keyword" + }, + "without_security": { + "type": "keyword" + }, + "without_security_build_yml": { + "type": "keyword" + }, + "without_security_cluster_stdout": { + "type": "keyword" + }, + "without_security_cluster_stderr": { + "type": "keyword" + } + } + } + }' + curl -I "${METRICS_HOST_URL}/${indexName}" --aws-sigv4 \"aws:amz:us-east-1:es\" --user \"${awsAccessKey}:${awsSecretKey}\" -H \"x-amz-security-token:${awsSessionToken}\" | grep -E "HTTP\\/[0-9]+(\\.[0-9]+)? 200" + if [ \$? -eq 0 ]; then + echo "Index already exists. Indexing Results" + else + echo "Index does not exist. Creating..." + create_index_response=\$(curl -s -XPUT "${METRICS_HOST_URL}/${indexName}" --aws-sigv4 \"aws:amz:us-east-1:es\" --user \"${awsAccessKey}:${awsSecretKey}\" -H \"x-amz-security-token:${awsSessionToken}\" -H 'Content-Type: application/json' -d "\${INDEX_MAPPING}") + if [[ \$create_index_response == *'"acknowledged":true'* ]]; then + echo "Index created successfully." + else + echo "Failed to create index. Error message: \$create_index_response" + exit 1 + fi + fi + if [ -s ${testRecordsFile} ]; then + echo "File Exists, indexing results." + curl -XPOST "${METRICS_HOST_URL}/$indexName/_bulk" --aws-sigv4 \"aws:amz:us-east-1:es\" --user \"${awsAccessKey}:${awsSecretKey}\" -H \"x-amz-security-token:${awsSessionToken}\" -H "Content-Type: application/x-ndjson" --data-binary "@${testRecordsFile}" + else + echo "File Does not exist. No tests records to process." + fi + """ + } + } +} + +def generateJson(component, version, integTestBuildNumber, integTestBuildUrl, distributionBuildNumber, distributionBuildUrl, buildStartTime, rc, rcNumber, platform, architecture, distribution, componentCategory, componentResult, testReportManifestYmlUrl, withSecurity, withSecurityYml, withSecurityStdout, withSecurityStderr, withoutSecurity, withoutSecurityYml, withoutSecurityStdout, withoutSecurityStderr) { + def json = [ + component: component, + version: version, + integ_test_build_number: integTestBuildNumber, + integ_test_build_url: integTestBuildUrl, + distribution_build_number: distributionBuildNumber, + distribution_build_url: distributionBuildUrl, + build_start_time: buildStartTime, + rc: rc, + rc_number: rcNumber, + platform: platform, + architecture: architecture, + distribution: distribution, + component_category: componentCategory, + component_build_result: componentResult, + test_report_manifest_yml: testReportManifestYmlUrl, + with_security: withSecurity, + with_security_build_yml: withSecurityYml, + with_security_cluster_stdout: withSecurityStdout, + with_security_cluster_stderr: withSecurityStderr, + without_security: withoutSecurity, + without_security_build_yml: withoutSecurityYml, + without_security_cluster_stdout: withoutSecurityStdout, + without_security_cluster_stderr: withoutSecurityStderr + ] + return JsonOutput.toJson(json) +} +