blob: d13301c50ed549b77eef7e6ee1d988a99222cf8b [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/**
Dennis Dmitriev5f014d82020-04-29 00:00:34 +030021 * Get Jenkins parameter names, values and types from jobName
22 * @param jobName job name
23 * @return Map with parameter names as keys and the following map as values:
24 * [
25 * <str name1>: [type: <str cls1>, use_variable: <str name1>, defaultValue: <cls value1>],
26 * <str name2>: [type: <str cls2>, use_variable: <str name2>, defaultValue: <cls value2>],
27 * ]
28 */
29def getJobDefaultParameters(jobName) {
30 def jenkinsUtils = new com.mirantis.mk.JenkinsUtils()
31 def item = jenkinsUtils.getJobByName(env.JOB_NAME)
32 def parameters = [:]
33 def prop = item.getProperty(ParametersDefinitionProperty.class)
34 if(prop != null) {
35 for(param in prop.getParameterDefinitions()) {
36 def defaultParam = param.getDefaultParameterValue()
37 def cls = defaultParam.getClass().getName()
38 def value = defaultParam.getValue()
39 def name = defaultParam.getName()
40 parameters[name] = [type: cls, use_variable: name, defaultValue: value]
41 }
42 }
43 return parameters
44}
45
46/**
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030047 * Run a Jenkins job using the collected parameters
48 *
49 * @param job_name Name of the running job
50 * @param job_parameters Map that declares which values from global_variables should be used, in the following format:
51 * {'PARAM_NAME': {'type': <job parameter $class name>, 'use_variable': <a key from global_variables>}, ...}
Dennis Dmitrievce470932019-09-18 18:31:11 +030052 * or
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +030053 * {'PARAM_NAME': {'type': <job parameter $class name>, 'get_variable_from_url': <a key from global_variables which contains URL with required content>}, ...}
54 * or
Dennis Dmitrievce470932019-09-18 18:31:11 +030055 * {'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 +030056 * @param global_variables Map that keeps the artifact URLs and used 'env' objects:
57 * {'PARAM1_NAME': <param1 value>, 'PARAM2_NAME': 'http://.../artifacts/param2_value', ...}
Dennis Dmitrieve09e0292019-07-30 16:39:27 +030058 * @param propagate Boolean. If false: allows to collect artifacts after job is finished, even with FAILURE status
59 * If true: immediatelly fails the pipeline. DO NOT USE 'true' if you want to collect artifacts
60 * for 'finally' steps
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030061 */
Dennis Dmitrieve09e0292019-07-30 16:39:27 +030062def runJob(job_name, job_parameters, global_variables, Boolean propagate = false) {
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030063 def parameters = []
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +030064 def http = new com.mirantis.mk.Http()
Dennis Dmitrievce470932019-09-18 18:31:11 +030065 def engine = new groovy.text.GStringTemplateEngine()
66 def template
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +030067 def base = [:]
68 base["url"] = ''
69 def variable_content
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030070
71 // Collect required parameters from 'global_variables' or 'env'
72 for (param in job_parameters) {
Dennis Dmitrievce470932019-09-18 18:31:11 +030073 if (param.value.containsKey('use_variable')) {
74 if (!global_variables[param.value.use_variable]) {
75 global_variables[param.value.use_variable] = env[param.value.use_variable] ?: ''
76 }
77 parameters.add([$class: "${param.value.type}", name: "${param.key}", value: global_variables[param.value.use_variable]])
78 println "${param.key}: <${param.value.type}> ${global_variables[param.value.use_variable]}"
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +030079 } else if (param.value.containsKey('get_variable_from_url')) {
80 if (!global_variables[param.value.get_variable_from_url]) {
81 global_variables[param.value.get_variable_from_url] = env[param.value.get_variable_from_url] ?: ''
82 }
Andrew Baraniuke0aef1e2019-10-16 14:50:10 +030083 if (global_variables[param.value.get_variable_from_url]) {
Dennis Dmitriev37828362019-11-11 18:06:49 +020084 variable_content = http.restGet(base, global_variables[param.value.get_variable_from_url]).trim()
Andrew Baraniuke0aef1e2019-10-16 14:50:10 +030085 parameters.add([$class: "${param.value.type}", name: "${param.key}", value: variable_content])
86 println "${param.key}: <${param.value.type}> ${variable_content}"
87 } else {
88 println "${param.key} is empty, skipping get_variable_from_url"
89 }
Dennis Dmitrievce470932019-09-18 18:31:11 +030090 } else if (param.value.containsKey('use_template')) {
91 template = engine.createTemplate(param.value.use_template).make(global_variables)
92 parameters.add([$class: "${param.value.type}", name: "${param.key}", value: template.toString()])
93 println "${param.key}: <${param.value.type}>\n${template.toString()}"
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030094 }
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030095 }
96
97 // Build the job
Dennis Dmitrieve09e0292019-07-30 16:39:27 +030098 def job_info = build job: "${job_name}", parameters: parameters, propagate: propagate
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +030099 return job_info
100}
101
102/**
103 * Store URLs of the specified artifacts to the global_variables
104 *
105 * @param build_url URL of the completed job
106 * @param step_artifacts Map that contains artifact names in the job, and variable names
107 * where the URLs to that atrifacts should be stored, for example:
108 * {'ARTIFACT1': 'logs.tar.gz', 'ARTIFACT2': 'test_report.xml', ...}
109 * @param global_variables Map that will keep the artifact URLs. Variable 'ARTIFACT1', for example,
110 * be used in next job parameters: {'ARTIFACT1_URL':{ 'use_variable': 'ARTIFACT1', ...}}
111 *
112 * If the artifact with the specified name not found, the parameter ARTIFACT1_URL
113 * will be empty.
114 *
115 */
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300116def storeArtifacts(build_url, step_artifacts, global_variables, job_name, build_num) {
117 def common = new com.mirantis.mk.Common()
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300118 def http = new com.mirantis.mk.Http()
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300119 def baseJenkins = [:]
120 def baseArtifactory = [:]
121 build_url = build_url.replaceAll(~/\/+$/, "")
122 artifactory_url = "https://artifactory.mcp.mirantis.net/api/storage/si-local/jenkins-job-artifacts"
123 baseArtifactory["url"] = artifactory_url + "/${job_name}/${build_num}"
124
125 baseJenkins["url"] = build_url
126 def job_config = http.restGet(baseJenkins, "/api/json/")
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300127 def job_artifacts = job_config['artifacts']
128 for (artifact in step_artifacts) {
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300129 try {
130 artifactoryResp = http.restGet(baseArtifactory, "/${artifact.value}")
131 global_variables[artifact.key] = artifactoryResp.downloadUri
132 println "Artifact URL ${artifactoryResp.downloadUri} stored to ${artifact.key}"
133 continue
134 } catch (Exception e) {
135 common.warningMsg("Can't find an artifact in ${artifactory_url}/${job_name}/${build_num}/${artifact.value} error code ${e.message}")
136 }
137
138 job_artifact = job_artifacts.findAll { item -> artifact.value == item['fileName'] || artifact.value == item['relativePath'] }
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300139 if (job_artifact.size() == 1) {
140 // Store artifact URL
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300141 def artifact_url = "${build_url}/artifact/${job_artifact[0]['relativePath']}"
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300142 global_variables[artifact.key] = artifact_url
143 println "Artifact URL ${artifact_url} stored to ${artifact.key}"
144 } else if (job_artifact.size() > 1) {
145 // Error: too many artifacts with the same name, fail the job
146 error "Multiple artifacts ${artifact.value} for ${artifact.key} found in the build results ${build_url}, expected one:\n${job_artifact}"
147 } else {
148 // Warning: no artifact with expected name
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300149 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 +0300150 global_variables[artifact.key] = ''
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300151 }
152 }
153}
154
155
156/**
157 * Run the workflow or final steps one by one
158 *
159 * @param steps List of steps (Jenkins jobs) to execute
160 * @param global_variables Map where the collected artifact URLs and 'env' objects are stored
161 * @param failed_jobs Map with failed job names and result statuses, to report it later
Dennis Dmitrieve09e0292019-07-30 16:39:27 +0300162 * @param propagate Boolean. If false: allows to collect artifacts after job is finished, even with FAILURE status
163 * If true: immediatelly fails the pipeline. DO NOT USE 'true' with runScenario().
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300164 */
Dennis Dmitrieve09e0292019-07-30 16:39:27 +0300165def runSteps(steps, global_variables, failed_jobs, Boolean propagate = false) {
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300166 for (step in steps) {
167 stage("Running job ${step['job']}") {
168
169 def job_name = step['job']
Dennis Dmitriev5f014d82020-04-29 00:00:34 +0300170 def job_parameters = [:]
171 if (step['inherit_parent_params'] ?: false) {
172 // add parameters from the current job for the child job
173 job_parameters << getJobDefaultParameters(env.JOB_NAME)
174 }
175 // add parameters from the workflow for the child job
176 job_parameters << step['parameters']
177
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300178 // Collect job parameters and run the job
Dennis Dmitrieve09e0292019-07-30 16:39:27 +0300179 def job_info = runJob(job_name, job_parameters, global_variables, propagate)
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300180 def job_result = job_info.getResult()
181 def build_url = job_info.getAbsoluteUrl()
182 def build_description = job_info.getDescription()
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300183 def build_id = job_info.getId()
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300184
185 currentBuild.description += "<a href=${build_url}>${job_name}</a>: ${job_result}<br>"
186 // Import the remote build description into the current build
187 if (build_description) { // TODO - add also the job status
188 currentBuild.description += build_description
189 }
190
191 // Store links to the resulting artifacts into 'global_variables'
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300192 storeArtifacts(build_url, step['artifacts'], global_variables, job_name, build_id)
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300193
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300194 // Check job result, in case of SUCCESS, move to next step.
Mykyta Karpin0bd8bc62020-04-29 12:27:14 +0300195 // In case job has status NOT_BUILT, fail the build or keep going depending on 'ignore_not_built' flag
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300196 // In other cases check flag ignore_failed, if true ignore any statuses and keep going.
197 if (job_result != 'SUCCESS'){
198 def ignoreStepResult = false
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300199 switch(job_result) {
200 // In cases when job was waiting too long in queue or internal job logic allows to skip building,
201 // job may have NOT_BUILT status. In that case ignore_not_built flag can be used not to fail scenario.
202 case "NOT_BUILT":
203 ignoreStepResult = step['ignore_not_built'] ?: false
204 break;
205 default:
206 ignoreStepResult = step['ignore_failed'] ?: false
Mykyta Karpin0bd8bc62020-04-29 12:27:14 +0300207 failed_jobs[build_url] = job_result
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300208 }
209 if (!ignoreStepResult) {
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300210 currentBuild.result = job_result
211 error "Job ${build_url} finished with result: ${job_result}"
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300212 } // if (!ignoreStepResult)
213 } // if (job_result != 'SUCCESS')
214 println "Job ${build_url} finished with result: ${job_result}"
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300215 } // stage ("Running job ${step['job']}")
216 } // for (step in scenario['workflow'])
217}
218
219/**
220 * Run the workflow scenario
221 *
222 * @param scenario: Map with scenario steps.
223
224 * There are two keys in the scenario:
225 * workflow: contains steps to run deploy and test jobs
226 * finally: contains steps to run report and cleanup jobs
227 *
228 * Scenario execution example:
229 *
230 * scenario_yaml = """\
231 * workflow:
232 * - job: deploy-kaas
233 * ignore_failed: false
234 * parameters:
235 * KAAS_VERSION:
236 * type: StringParameterValue
237 * use_variable: KAAS_VERSION
238 * artifacts:
239 * KUBECONFIG_ARTIFACT: artifacts/management_kubeconfig
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +0300240 * DEPLOYED_KAAS_VERSION: artifacts/management_version
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300241 *
Dennis Dmitriev5f014d82020-04-29 00:00:34 +0300242 * - job: create-child
243 * inherit_parent_params: true
244 * ignore_failed: false
245 * parameters:
246 * KUBECONFIG_ARTIFACT_URL:
247 * type: StringParameterValue
248 * use_variable: KUBECONFIG_ARTIFACT
249 * KAAS_VERSION:
250 * type: StringParameterValue
251 * get_variable_from_url: DEPLOYED_KAAS_VERSION
252 *
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300253 * - job: test-kaas-ui
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300254 * ignore_not_built: false
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300255 * parameters:
256 * KUBECONFIG_ARTIFACT_URL:
257 * type: StringParameterValue
258 * use_variable: KUBECONFIG_ARTIFACT
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +0300259 * KAAS_VERSION:
260 * type: StringParameterValue
261 * get_variable_from_url: DEPLOYED_KAAS_VERSION
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300262 * artifacts:
263 * REPORT_SI_KAAS_UI: artifacts/test_kaas_ui_result.xml
264 *
265 * finally:
266 * - job: testrail-report
267 * ignore_failed: true
268 * parameters:
Dennis Dmitrievce470932019-09-18 18:31:11 +0300269 * KAAS_VERSION:
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300270 * type: StringParameterValue
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +0300271 * get_variable_from_url: DEPLOYED_KAAS_VERSION
Dennis Dmitrievce470932019-09-18 18:31:11 +0300272 * REPORTS_LIST:
273 * type: TextParameterValue
274 * use_template: |
275 * REPORT_SI_KAAS_UI: \$REPORT_SI_KAAS_UI
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300276 * """
277 *
278 * runScenario(scenario)
279 *
Dennis Dmitriev5f014d82020-04-29 00:00:34 +0300280 * Scenario workflow keys:
281 *
282 * job: string. Jenkins job name
283 * ignore_failed: bool. if true, keep running the workflow jobs if the job is failed, but fail the workflow at finish
284 * ignore_not_built: bool. if true, keep running the workflow jobs if the job set own status to NOT_BUILT, do not fail the workflow at finish for such jobs
285 * inherit_parent_params: bool. if true, provide all parameters from the parent job to the child job as defaults
286 * parameters: dict. parameters name and type to inherit from parent to child job, or from artifact to child job
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300287 */
288
vnaumov5a6eb8a2020-03-31 11:16:54 +0200289def runScenario(scenario, slackReportChannel = '') {
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300290
Dennis Dmitriev79f3a2d2019-08-09 16:06:00 +0300291 // Clear description before adding new messages
292 currentBuild.description = ''
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300293 // Collect the parameters for the jobs here
294 global_variables = [:]
295 // List of failed jobs to show at the end
296 failed_jobs = [:]
297
298 try {
299 // Run the 'workflow' jobs
300 runSteps(scenario['workflow'], global_variables, failed_jobs)
301
302 } catch (InterruptedException x) {
303 error "The job was aborted"
304
305 } catch (e) {
306 error("Build failed: " + e.toString())
307
308 } finally {
309 // Run the 'finally' jobs
310 runSteps(scenario['finally'], global_variables, failed_jobs)
311
312 if (failed_jobs) {
sgudz9ac09d22020-01-22 14:31:30 +0200313 statuses = []
314 failed_jobs.each {
sgudz74c8cdd2020-01-23 14:26:32 +0200315 statuses += it.value
sgudz9ac09d22020-01-22 14:31:30 +0200316 }
317 if (statuses.contains('FAILURE')) {
318 currentBuild.result = 'FAILURE'
319 }
320 else if (statuses.contains('UNSTABLE')) {
321 currentBuild.result = 'UNSTABLE'
322 }
323 else {
324 currentBuild.result = 'FAILURE'
325 }
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300326 println "Failed jobs: ${failed_jobs}"
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300327 }
vnaumov5a6eb8a2020-03-31 11:16:54 +0200328
329 if (slackReportChannel) {
330 def slack = new com.mirantis.mcp.SlackNotification()
331 slack.jobResultNotification(currentBuild.result, slackReportChannel, '', null, '', 'slack_webhook_url')
332 }
sgudz9ac09d22020-01-22 14:31:30 +0200333 } // finally
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300334}