blob: aa38c0c14dd28aceafd5e7b3d76f489ce2ecd1fa [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 = [:]
Dennis Dmitriev334eecd2020-04-30 14:32:45 +0300171 def step_parameters = step['parameters'] ?: [:]
Dennis Dmitriev5f014d82020-04-29 00:00:34 +0300172 if (step['inherit_parent_params'] ?: false) {
173 // add parameters from the current job for the child job
174 job_parameters << getJobDefaultParameters(env.JOB_NAME)
175 }
176 // add parameters from the workflow for the child job
Dennis Dmitriev334eecd2020-04-30 14:32:45 +0300177 job_parameters << step_parameters
Dennis Dmitriev5f014d82020-04-29 00:00:34 +0300178
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300179 // Collect job parameters and run the job
Dennis Dmitrieve09e0292019-07-30 16:39:27 +0300180 def job_info = runJob(job_name, job_parameters, global_variables, propagate)
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300181 def job_result = job_info.getResult()
182 def build_url = job_info.getAbsoluteUrl()
183 def build_description = job_info.getDescription()
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300184 def build_id = job_info.getId()
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300185
186 currentBuild.description += "<a href=${build_url}>${job_name}</a>: ${job_result}<br>"
187 // Import the remote build description into the current build
188 if (build_description) { // TODO - add also the job status
189 currentBuild.description += build_description
190 }
191
192 // Store links to the resulting artifacts into 'global_variables'
Dmitry Tyzhnenkof446e412020-04-06 13:24:54 +0300193 storeArtifacts(build_url, step['artifacts'], global_variables, job_name, build_id)
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300194
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300195 // Check job result, in case of SUCCESS, move to next step.
Mykyta Karpin0bd8bc62020-04-29 12:27:14 +0300196 // 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 +0300197 // In other cases check flag ignore_failed, if true ignore any statuses and keep going.
198 if (job_result != 'SUCCESS'){
199 def ignoreStepResult = false
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300200 switch(job_result) {
201 // In cases when job was waiting too long in queue or internal job logic allows to skip building,
202 // job may have NOT_BUILT status. In that case ignore_not_built flag can be used not to fail scenario.
203 case "NOT_BUILT":
204 ignoreStepResult = step['ignore_not_built'] ?: false
205 break;
206 default:
207 ignoreStepResult = step['ignore_failed'] ?: false
Mykyta Karpin0bd8bc62020-04-29 12:27:14 +0300208 failed_jobs[build_url] = job_result
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300209 }
210 if (!ignoreStepResult) {
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300211 currentBuild.result = job_result
212 error "Job ${build_url} finished with result: ${job_result}"
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300213 } // if (!ignoreStepResult)
214 } // if (job_result != 'SUCCESS')
215 println "Job ${build_url} finished with result: ${job_result}"
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300216 } // stage ("Running job ${step['job']}")
217 } // for (step in scenario['workflow'])
218}
219
220/**
221 * Run the workflow scenario
222 *
223 * @param scenario: Map with scenario steps.
224
225 * There are two keys in the scenario:
226 * workflow: contains steps to run deploy and test jobs
227 * finally: contains steps to run report and cleanup jobs
228 *
229 * Scenario execution example:
230 *
231 * scenario_yaml = """\
232 * workflow:
233 * - job: deploy-kaas
234 * ignore_failed: false
235 * parameters:
236 * KAAS_VERSION:
237 * type: StringParameterValue
238 * use_variable: KAAS_VERSION
239 * artifacts:
240 * KUBECONFIG_ARTIFACT: artifacts/management_kubeconfig
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +0300241 * DEPLOYED_KAAS_VERSION: artifacts/management_version
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300242 *
Dennis Dmitriev5f014d82020-04-29 00:00:34 +0300243 * - job: create-child
244 * inherit_parent_params: true
245 * ignore_failed: false
246 * parameters:
247 * KUBECONFIG_ARTIFACT_URL:
248 * type: StringParameterValue
249 * use_variable: KUBECONFIG_ARTIFACT
250 * KAAS_VERSION:
251 * type: StringParameterValue
252 * get_variable_from_url: DEPLOYED_KAAS_VERSION
253 *
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300254 * - job: test-kaas-ui
Mykyta Karpina3d775e2020-04-24 14:45:17 +0300255 * ignore_not_built: false
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300256 * parameters:
257 * KUBECONFIG_ARTIFACT_URL:
258 * type: StringParameterValue
259 * use_variable: KUBECONFIG_ARTIFACT
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +0300260 * KAAS_VERSION:
261 * type: StringParameterValue
262 * get_variable_from_url: DEPLOYED_KAAS_VERSION
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300263 * artifacts:
264 * REPORT_SI_KAAS_UI: artifacts/test_kaas_ui_result.xml
265 *
266 * finally:
267 * - job: testrail-report
268 * ignore_failed: true
269 * parameters:
Dennis Dmitrievce470932019-09-18 18:31:11 +0300270 * KAAS_VERSION:
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300271 * type: StringParameterValue
Dennis Dmitrievcae9bca2019-09-19 16:10:03 +0300272 * get_variable_from_url: DEPLOYED_KAAS_VERSION
Dennis Dmitrievce470932019-09-18 18:31:11 +0300273 * REPORTS_LIST:
274 * type: TextParameterValue
275 * use_template: |
276 * REPORT_SI_KAAS_UI: \$REPORT_SI_KAAS_UI
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300277 * """
278 *
279 * runScenario(scenario)
280 *
Dennis Dmitriev5f014d82020-04-29 00:00:34 +0300281 * Scenario workflow keys:
282 *
283 * job: string. Jenkins job name
284 * ignore_failed: bool. if true, keep running the workflow jobs if the job is failed, but fail the workflow at finish
285 * 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
286 * inherit_parent_params: bool. if true, provide all parameters from the parent job to the child job as defaults
287 * 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 +0300288 */
289
vnaumov5a6eb8a2020-03-31 11:16:54 +0200290def runScenario(scenario, slackReportChannel = '') {
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300291
Dennis Dmitriev79f3a2d2019-08-09 16:06:00 +0300292 // Clear description before adding new messages
293 currentBuild.description = ''
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300294 // Collect the parameters for the jobs here
295 global_variables = [:]
296 // List of failed jobs to show at the end
297 failed_jobs = [:]
298
299 try {
300 // Run the 'workflow' jobs
301 runSteps(scenario['workflow'], global_variables, failed_jobs)
302
303 } catch (InterruptedException x) {
304 error "The job was aborted"
305
306 } catch (e) {
307 error("Build failed: " + e.toString())
308
309 } finally {
310 // Run the 'finally' jobs
311 runSteps(scenario['finally'], global_variables, failed_jobs)
312
313 if (failed_jobs) {
sgudz9ac09d22020-01-22 14:31:30 +0200314 statuses = []
315 failed_jobs.each {
sgudz74c8cdd2020-01-23 14:26:32 +0200316 statuses += it.value
sgudz9ac09d22020-01-22 14:31:30 +0200317 }
318 if (statuses.contains('FAILURE')) {
319 currentBuild.result = 'FAILURE'
320 }
321 else if (statuses.contains('UNSTABLE')) {
322 currentBuild.result = 'UNSTABLE'
323 }
324 else {
325 currentBuild.result = 'FAILURE'
326 }
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300327 println "Failed jobs: ${failed_jobs}"
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300328 }
vnaumov5a6eb8a2020-03-31 11:16:54 +0200329
330 if (slackReportChannel) {
331 def slack = new com.mirantis.mcp.SlackNotification()
332 slack.jobResultNotification(currentBuild.result, slackReportChannel, '', null, '', 'slack_webhook_url')
333 }
sgudz9ac09d22020-01-22 14:31:30 +0200334 } // finally
Dennis Dmitriev5d8a1532019-07-30 16:39:27 +0300335}