blob: b679f172ebc2eca44e25df0b0e1276b8658571ad [file] [log] [blame]
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +03001package com.mirantis.mk
2
3/**
4 *
5 * Run a simple workflow
6 *
7 * Function runScenario() executes a sequence of jobs, like
8 * - Parameters for the jobs are taken from the 'env' object
9 * - URLs of artifacts from completed jobs may be passed
10 * as parameters to the next jobs.
11 *
12 * No constants, environment specific logic or other conditional dependencies.
13 * All the logic should be placed in the workflow jobs, and perform necessary
14 * actions depending on the job parameters.
15 * The runScenario() function only provides the
16 *
17 */
18
19
20/**
21 * Run a Jenkins job using the collected parameters
22 *
23 * @param job_name Name of the running job
24 * @param job_parameters Map that declares which values from global_variables should be used, in the following format:
25 * {'PARAM_NAME': {'type': <job parameter $class name>, 'use_variable': <a key from global_variables>}, ...}
Dennis Dmitrievce470932019-09-18 18:31:11 +030026 * or
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +030027 * {'PARAM_NAME': {'type': <job parameter $class name>, 'get_variable_from_url': <a key from global_variables which contains URL with required content>}, ...}
28 * or
Dennis Dmitrievce470932019-09-18 18:31:11 +030029 * {'PARAM_NAME': {'type': <job parameter $class name>, 'use_template': <a GString multiline template with variables from global_variables>}, ...}
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030030 * @param global_variables Map that keeps the artifact URLs and used 'env' objects:
31 * {'PARAM1_NAME': <param1 value>, 'PARAM2_NAME': 'http://.../artifacts/param2_value', ...}
Dennis Dmitrieve09e0292019-07-30 16:39:27 +030032 * @param propagate Boolean. If false: allows to collect artifacts after job is finished, even with FAILURE status
33 * If true: immediatelly fails the pipeline. DO NOT USE 'true' if you want to collect artifacts
34 * for 'finally' steps
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030035 */
Dennis Dmitrieve09e0292019-07-30 16:39:27 +030036def runJob(job_name, job_parameters, global_variables, Boolean propagate = false) {
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030037 def parameters = []
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +030038 def http = new com.mirantis.mk.Http()
Dennis Dmitrievce470932019-09-18 18:31:11 +030039 def engine = new groovy.text.GStringTemplateEngine()
40 def template
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +030041 def base = [:]
42 base["url"] = ''
43 def variable_content
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030044
45 // Collect required parameters from 'global_variables' or 'env'
46 for (param in job_parameters) {
Dennis Dmitrievce470932019-09-18 18:31:11 +030047 if (param.value.containsKey('use_variable')) {
48 if (!global_variables[param.value.use_variable]) {
49 global_variables[param.value.use_variable] = env[param.value.use_variable] ?: ''
50 }
51 parameters.add([$class: "${param.value.type}", name: "${param.key}", value: global_variables[param.value.use_variable]])
52 println "${param.key}: <${param.value.type}> ${global_variables[param.value.use_variable]}"
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +030053 } else if (param.value.containsKey('get_variable_from_url')) {
54 if (!global_variables[param.value.get_variable_from_url]) {
55 global_variables[param.value.get_variable_from_url] = env[param.value.get_variable_from_url] ?: ''
56 }
Andrew Baraniuke0aef1e2019-10-16 14:50:10 +030057 if (global_variables[param.value.get_variable_from_url]) {
Dennis Dmitriev37828362019-11-11 18:06:49 +020058 variable_content = http.restGet(base, global_variables[param.value.get_variable_from_url]).trim()
Andrew Baraniuke0aef1e2019-10-16 14:50:10 +030059 parameters.add([$class: "${param.value.type}", name: "${param.key}", value: variable_content])
60 println "${param.key}: <${param.value.type}> ${variable_content}"
61 } else {
62 println "${param.key} is empty, skipping get_variable_from_url"
63 }
Dennis Dmitrievce470932019-09-18 18:31:11 +030064 } else if (param.value.containsKey('use_template')) {
65 template = engine.createTemplate(param.value.use_template).make(global_variables)
66 parameters.add([$class: "${param.value.type}", name: "${param.key}", value: template.toString()])
67 println "${param.key}: <${param.value.type}>\n${template.toString()}"
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030068 }
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030069 }
70
71 // Build the job
Dennis Dmitrieve09e0292019-07-30 16:39:27 +030072 def job_info = build job: "${job_name}", parameters: parameters, propagate: propagate
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030073 return job_info
74}
75
76/**
77 * Store URLs of the specified artifacts to the global_variables
78 *
79 * @param build_url URL of the completed job
80 * @param step_artifacts Map that contains artifact names in the job, and variable names
81 * where the URLs to that atrifacts should be stored, for example:
82 * {'ARTIFACT1': 'logs.tar.gz', 'ARTIFACT2': 'test_report.xml', ...}
83 * @param global_variables Map that will keep the artifact URLs. Variable 'ARTIFACT1', for example,
84 * be used in next job parameters: {'ARTIFACT1_URL':{ 'use_variable': 'ARTIFACT1', ...}}
85 *
86 * If the artifact with the specified name not found, the parameter ARTIFACT1_URL
87 * will be empty.
88 *
89 */
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +030090def storeArtifacts(build_url, step_artifacts, global_variables, job_name, build_num) {
91 def common = new com.mirantis.mk.Common()
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030092 def http = new com.mirantis.mk.Http()
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +030093 def baseJenkins = [:]
94 def baseArtifactory = [:]
95 build_url = build_url.replaceAll(~/\/+$/, "")
96 artifactory_url = "https://artifactory.mcp.mirantis.net/api/storage/si-local/jenkins-job-artifacts"
97 baseArtifactory["url"] = artifactory_url + "/${job_name}/${build_num}"
98
99 baseJenkins["url"] = build_url
100 def job_config = http.restGet(baseJenkins, "/api/json/")
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300101 def job_artifacts = job_config['artifacts']
102 for (artifact in step_artifacts) {
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300103 try {
104 artifactoryResp = http.restGet(baseArtifactory, "/${artifact.value}")
105 global_variables[artifact.key] = artifactoryResp.downloadUri
106 println "Artifact URL ${artifactoryResp.downloadUri} stored to ${artifact.key}"
107 continue
108 } catch (Exception e) {
109 common.warningMsg("Can't find an artifact in ${artifactory_url}/${job_name}/${build_num}/${artifact.value} error code ${e.message}")
110 }
111
112 job_artifact = job_artifacts.findAll { item -> artifact.value == item['fileName'] || artifact.value == item['relativePath'] }
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300113 if (job_artifact.size() == 1) {
114 // Store artifact URL
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300115 def artifact_url = "${build_url}/artifact/${job_artifact[0]['relativePath']}"
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300116 global_variables[artifact.key] = artifact_url
117 println "Artifact URL ${artifact_url} stored to ${artifact.key}"
118 } else if (job_artifact.size() > 1) {
119 // Error: too many artifacts with the same name, fail the job
120 error "Multiple artifacts ${artifact.value} for ${artifact.key} found in the build results ${build_url}, expected one:\n${job_artifact}"
121 } else {
122 // Warning: no artifact with expected name
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300123 println "Artifact ${artifact.value} for ${artifact.key} not found in the build results ${build_url} and in the artifactory ${artifactory_url}/${job_name}/${build_num}/, found the following artifacts in Jenkins:\n${job_artifacts}"
Andrew Baraniuke0aef1e2019-10-16 14:50:10 +0300124 global_variables[artifact.key] = ''
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300125 }
126 }
127}
128
129
130/**
131 * Run the workflow or final steps one by one
132 *
133 * @param steps List of steps (Jenkins jobs) to execute
134 * @param global_variables Map where the collected artifact URLs and 'env' objects are stored
135 * @param failed_jobs Map with failed job names and result statuses, to report it later
Dennis Dmitrieve09e0292019-07-30 16:39:27 +0300136 * @param propagate Boolean. If false: allows to collect artifacts after job is finished, even with FAILURE status
137 * If true: immediatelly fails the pipeline. DO NOT USE 'true' with runScenario().
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300138 */
Dennis Dmitrieve09e0292019-07-30 16:39:27 +0300139def runSteps(steps, global_variables, failed_jobs, Boolean propagate = false) {
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300140 for (step in steps) {
141 stage("Running job ${step['job']}") {
142
143 def job_name = step['job']
144 def job_parameters = step['parameters']
145 // Collect job parameters and run the job
Dennis Dmitrieve09e0292019-07-30 16:39:27 +0300146 def job_info = runJob(job_name, job_parameters, global_variables, propagate)
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300147 def job_result = job_info.getResult()
148 def build_url = job_info.getAbsoluteUrl()
149 def build_description = job_info.getDescription()
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300150 def build_id = job_info.getId()
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300151
152 currentBuild.description += "<a href=${build_url}>${job_name}</a>: ${job_result}<br>"
153 // Import the remote build description into the current build
154 if (build_description) { // TODO - add also the job status
155 currentBuild.description += build_description
156 }
157
158 // Store links to the resulting artifacts into 'global_variables'
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300159 storeArtifacts(build_url, step['artifacts'], global_variables, job_name, build_id)
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300160
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300161 // Check job result, in case of SUCCESS, move to next step.
162 // In case job has status NOT_BUILT, fail the build or keep going depending on 'ignore_not_built)' flag
163 // In other cases check flag ignore_failed, if true ignore any statuses and keep going.
164 if (job_result != 'SUCCESS'){
165 def ignoreStepResult = false
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300166 failed_jobs[build_url] = job_result
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300167 switch(job_result) {
168 // In cases when job was waiting too long in queue or internal job logic allows to skip building,
169 // job may have NOT_BUILT status. In that case ignore_not_built flag can be used not to fail scenario.
170 case "NOT_BUILT":
171 ignoreStepResult = step['ignore_not_built'] ?: false
172 break;
173 default:
174 ignoreStepResult = step['ignore_failed'] ?: false
175 }
176 if (!ignoreStepResult) {
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300177 currentBuild.result = job_result
178 error "Job ${build_url} finished with result: ${job_result}"
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300179 } // if (!ignoreStepResult)
180 } // if (job_result != 'SUCCESS')
181 println "Job ${build_url} finished with result: ${job_result}"
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300182 } // stage ("Running job ${step['job']}")
183 } // for (step in scenario['workflow'])
184}
185
186/**
187 * Run the workflow scenario
188 *
189 * @param scenario: Map with scenario steps.
190
191 * There are two keys in the scenario:
192 * workflow: contains steps to run deploy and test jobs
193 * finally: contains steps to run report and cleanup jobs
194 *
195 * Scenario execution example:
196 *
197 * scenario_yaml = """\
198 * workflow:
199 * - job: deploy-kaas
200 * ignore_failed: false
201 * parameters:
202 * KAAS_VERSION:
203 * type: StringParameterValue
204 * use_variable: KAAS_VERSION
205 * artifacts:
206 * KUBECONFIG_ARTIFACT: artifacts/management_kubeconfig
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +0300207 * DEPLOYED_KAAS_VERSION: artifacts/management_version
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300208 *
209 * - job: test-kaas-ui
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300210 * ignore_not_built: false
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300211 * parameters:
212 * KUBECONFIG_ARTIFACT_URL:
213 * type: StringParameterValue
214 * use_variable: KUBECONFIG_ARTIFACT
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +0300215 * KAAS_VERSION:
216 * type: StringParameterValue
217 * get_variable_from_url: DEPLOYED_KAAS_VERSION
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300218 * artifacts:
219 * REPORT_SI_KAAS_UI: artifacts/test_kaas_ui_result.xml
220 *
221 * finally:
222 * - job: testrail-report
223 * ignore_failed: true
224 * parameters:
Dennis Dmitrievce470932019-09-18 18:31:11 +0300225 * KAAS_VERSION:
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300226 * type: StringParameterValue
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +0300227 * get_variable_from_url: DEPLOYED_KAAS_VERSION
Dennis Dmitrievce470932019-09-18 18:31:11 +0300228 * REPORTS_LIST:
229 * type: TextParameterValue
230 * use_template: |
231 * REPORT_SI_KAAS_UI: \$REPORT_SI_KAAS_UI
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300232 * """
233 *
234 * runScenario(scenario)
235 *
236 */
237
vnaumov5a6eb8a2020-03-31 11:16:54 +0200238def runScenario(scenario, slackReportChannel = '') {
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300239
Dennis Dmitriev79f3a2d2019-08-09 16:06:00 +0300240 // Clear description before adding new messages
241 currentBuild.description = ''
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300242 // Collect the parameters for the jobs here
243 global_variables = [:]
244 // List of failed jobs to show at the end
245 failed_jobs = [:]
246
247 try {
248 // Run the 'workflow' jobs
249 runSteps(scenario['workflow'], global_variables, failed_jobs)
250
251 } catch (InterruptedException x) {
252 error "The job was aborted"
253
254 } catch (e) {
255 error("Build failed: " + e.toString())
256
257 } finally {
258 // Run the 'finally' jobs
259 runSteps(scenario['finally'], global_variables, failed_jobs)
260
261 if (failed_jobs) {
sgudz9ac09d22020-01-22 14:31:30 +0200262 statuses = []
263 failed_jobs.each {
sgudz74c8cdd2020-01-23 14:26:32 +0200264 statuses += it.value
sgudz9ac09d22020-01-22 14:31:30 +0200265 }
266 if (statuses.contains('FAILURE')) {
267 currentBuild.result = 'FAILURE'
268 }
269 else if (statuses.contains('UNSTABLE')) {
270 currentBuild.result = 'UNSTABLE'
271 }
272 else {
273 currentBuild.result = 'FAILURE'
274 }
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300275 println "Failed jobs: ${failed_jobs}"
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300276 }
vnaumov5a6eb8a2020-03-31 11:16:54 +0200277
278 if (slackReportChannel) {
279 def slack = new com.mirantis.mcp.SlackNotification()
280 slack.jobResultNotification(currentBuild.result, slackReportChannel, '', null, '', 'slack_webhook_url')
281 }
sgudz9ac09d22020-01-22 14:31:30 +0200282 } // finally
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300283}