| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 1 | package 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 Dmitriev | ce47093 | 2019-09-18 18:31:11 +0300 | [diff] [blame] | 26 |  *                          or | 
| Dennis Dmitriev | cae9bca | 2019-09-19 16:10:03 +0300 | [diff] [blame] | 27 |  *                          {'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 Dmitriev | ce47093 | 2019-09-18 18:31:11 +0300 | [diff] [blame] | 29 |  *                          {'PARAM_NAME': {'type': <job parameter $class name>, 'use_template': <a GString multiline template with variables from global_variables>}, ...} | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 30 |  * @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 Dmitriev | e09e029 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 32 |  * @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 Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 35 |  */ | 
| Dennis Dmitriev | e09e029 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 36 | def runJob(job_name, job_parameters, global_variables, Boolean propagate = false) { | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 37 |     def parameters = [] | 
| Dennis Dmitriev | cae9bca | 2019-09-19 16:10:03 +0300 | [diff] [blame] | 38 |     def http = new com.mirantis.mk.Http() | 
| Dennis Dmitriev | ce47093 | 2019-09-18 18:31:11 +0300 | [diff] [blame] | 39 |     def engine = new groovy.text.GStringTemplateEngine() | 
 | 40 |     def template | 
| Dennis Dmitriev | cae9bca | 2019-09-19 16:10:03 +0300 | [diff] [blame] | 41 |     def base = [:] | 
 | 42 |     base["url"] = '' | 
 | 43 |     def variable_content | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 44 |  | 
 | 45 |     // Collect required parameters from 'global_variables' or 'env' | 
 | 46 |     for (param in job_parameters) { | 
| Dennis Dmitriev | ce47093 | 2019-09-18 18:31:11 +0300 | [diff] [blame] | 47 |         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 Dmitriev | cae9bca | 2019-09-19 16:10:03 +0300 | [diff] [blame] | 53 |         } 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 Baraniuk | e0aef1e | 2019-10-16 14:50:10 +0300 | [diff] [blame] | 57 |             if (global_variables[param.value.get_variable_from_url]) { | 
| Dennis Dmitriev | 3782836 | 2019-11-11 18:06:49 +0200 | [diff] [blame] | 58 |                 variable_content = http.restGet(base, global_variables[param.value.get_variable_from_url]).trim() | 
| Andrew Baraniuk | e0aef1e | 2019-10-16 14:50:10 +0300 | [diff] [blame] | 59 |                 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 Dmitriev | ce47093 | 2019-09-18 18:31:11 +0300 | [diff] [blame] | 64 |         } 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 Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 68 |         } | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 69 |     } | 
 | 70 |  | 
 | 71 |     // Build the job | 
| Dennis Dmitriev | e09e029 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 72 |     def job_info = build job: "${job_name}", parameters: parameters, propagate: propagate | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 73 |     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 |  */ | 
 | 90 | def storeArtifacts(build_url, step_artifacts, global_variables) { | 
 | 91 |     def http = new com.mirantis.mk.Http() | 
 | 92 |     def base = [:] | 
 | 93 |     base["url"] = build_url | 
 | 94 |     def job_config = http.restGet(base, "/api/json/") | 
 | 95 |     def job_artifacts = job_config['artifacts'] | 
 | 96 |     for (artifact in step_artifacts) { | 
 | 97 |         def job_artifact = job_artifacts.findAll { item -> artifact.value == item['fileName'] || artifact.value == item['relativePath'] } | 
 | 98 |         if (job_artifact.size() == 1) { | 
 | 99 |             // Store artifact URL | 
 | 100 |             def artifact_url = "${build_url}artifact/${job_artifact[0]['relativePath']}" | 
 | 101 |             global_variables[artifact.key] = artifact_url | 
 | 102 |             println "Artifact URL ${artifact_url} stored to ${artifact.key}" | 
 | 103 |         } else if (job_artifact.size() > 1) { | 
 | 104 |             // Error: too many artifacts with the same name, fail the job | 
 | 105 |             error "Multiple artifacts ${artifact.value} for ${artifact.key} found in the build results ${build_url}, expected one:\n${job_artifact}" | 
 | 106 |         } else { | 
 | 107 |             // Warning: no artifact with expected name | 
 | 108 |             println "Artifact ${artifact.value} for ${artifact.key} not found in the build results ${build_url}, found the following artifacts:\n${job_artifacts}" | 
| Andrew Baraniuk | e0aef1e | 2019-10-16 14:50:10 +0300 | [diff] [blame] | 109 |             global_variables[artifact.key] = '' | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 110 |         } | 
 | 111 |     } | 
 | 112 | } | 
 | 113 |  | 
 | 114 |  | 
 | 115 | /** | 
 | 116 |  * Run the workflow or final steps one by one | 
 | 117 |  * | 
 | 118 |  * @param steps                   List of steps (Jenkins jobs) to execute | 
 | 119 |  * @param global_variables        Map where the collected artifact URLs and 'env' objects are stored | 
 | 120 |  * @param failed_jobs             Map with failed job names and result statuses, to report it later | 
| Dennis Dmitriev | e09e029 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 121 |  * @param propagate               Boolean. If false: allows to collect artifacts after job is finished, even with FAILURE status | 
 | 122 |  *                                If true: immediatelly fails the pipeline. DO NOT USE 'true' with runScenario(). | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 123 |  */ | 
| Dennis Dmitriev | e09e029 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 124 | def runSteps(steps, global_variables, failed_jobs, Boolean propagate = false) { | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 125 |     for (step in steps) { | 
 | 126 |         stage("Running job ${step['job']}") { | 
 | 127 |  | 
 | 128 |             def job_name = step['job'] | 
 | 129 |             def job_parameters = step['parameters'] | 
 | 130 |             // Collect job parameters and run the job | 
| Dennis Dmitriev | e09e029 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 131 |             def job_info = runJob(job_name, job_parameters, global_variables, propagate) | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 132 |             def job_result = job_info.getResult() | 
 | 133 |             def build_url = job_info.getAbsoluteUrl() | 
 | 134 |             def build_description = job_info.getDescription() | 
 | 135 |  | 
 | 136 |             currentBuild.description += "<a href=${build_url}>${job_name}</a>: ${job_result}<br>" | 
 | 137 |             // Import the remote build description into the current build | 
 | 138 |             if (build_description) { // TODO -  add also the job status | 
 | 139 |                 currentBuild.description += build_description | 
 | 140 |             } | 
 | 141 |  | 
 | 142 |             // Store links to the resulting artifacts into 'global_variables' | 
 | 143 |             storeArtifacts(build_url, step['artifacts'], global_variables) | 
 | 144 |  | 
 | 145 |             // Job failed, fail the build or keep going depending on 'ignore_failed' flag | 
 | 146 |             if (job_result != "SUCCESS") { | 
 | 147 |                 def job_ignore_failed = step['ignore_failed'] ?: false | 
 | 148 |                 failed_jobs[build_url] = job_result | 
 | 149 |                 if (job_ignore_failed) { | 
 | 150 |                     println "Job ${build_url} finished with result: ${job_result}" | 
 | 151 |                 } else { | 
 | 152 |                     currentBuild.result = job_result | 
 | 153 |                     error "Job ${build_url} finished with result: ${job_result}" | 
 | 154 |                 } | 
 | 155 |             } // if (job_result == "SUCCESS") | 
 | 156 |         } // stage ("Running job ${step['job']}") | 
 | 157 |     } // for (step in scenario['workflow']) | 
 | 158 | } | 
 | 159 |  | 
 | 160 | /** | 
 | 161 |  * Run the workflow scenario | 
 | 162 |  * | 
 | 163 |  * @param scenario: Map with scenario steps. | 
 | 164 |  | 
 | 165 |  * There are two keys in the scenario: | 
 | 166 |  *   workflow: contains steps to run deploy and test jobs | 
 | 167 |  *   finally: contains steps to run report and cleanup jobs | 
 | 168 |  * | 
 | 169 |  * Scenario execution example: | 
 | 170 |  * | 
 | 171 |  *     scenario_yaml = """\ | 
 | 172 |  *     workflow: | 
 | 173 |  *     - job: deploy-kaas | 
 | 174 |  *       ignore_failed: false | 
 | 175 |  *       parameters: | 
 | 176 |  *         KAAS_VERSION: | 
 | 177 |  *           type: StringParameterValue | 
 | 178 |  *           use_variable: KAAS_VERSION | 
 | 179 |  *       artifacts: | 
 | 180 |  *         KUBECONFIG_ARTIFACT: artifacts/management_kubeconfig | 
| Dennis Dmitriev | cae9bca | 2019-09-19 16:10:03 +0300 | [diff] [blame] | 181 |  *         DEPLOYED_KAAS_VERSION: artifacts/management_version | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 182 |  * | 
 | 183 |  *     - job: test-kaas-ui | 
 | 184 |  *       ignore_failed: false | 
 | 185 |  *       parameters: | 
 | 186 |  *         KUBECONFIG_ARTIFACT_URL: | 
 | 187 |  *           type: StringParameterValue | 
 | 188 |  *           use_variable: KUBECONFIG_ARTIFACT | 
| Dennis Dmitriev | cae9bca | 2019-09-19 16:10:03 +0300 | [diff] [blame] | 189 |  *         KAAS_VERSION: | 
 | 190 |  *           type: StringParameterValue | 
 | 191 |  *           get_variable_from_url: DEPLOYED_KAAS_VERSION | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 192 |  *       artifacts: | 
 | 193 |  *         REPORT_SI_KAAS_UI: artifacts/test_kaas_ui_result.xml | 
 | 194 |  * | 
 | 195 |  *     finally: | 
 | 196 |  *     - job: testrail-report | 
 | 197 |  *       ignore_failed: true | 
 | 198 |  *       parameters: | 
| Dennis Dmitriev | ce47093 | 2019-09-18 18:31:11 +0300 | [diff] [blame] | 199 |  *         KAAS_VERSION: | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 200 |  *           type: StringParameterValue | 
| Dennis Dmitriev | cae9bca | 2019-09-19 16:10:03 +0300 | [diff] [blame] | 201 |  *           get_variable_from_url: DEPLOYED_KAAS_VERSION | 
| Dennis Dmitriev | ce47093 | 2019-09-18 18:31:11 +0300 | [diff] [blame] | 202 |  *         REPORTS_LIST: | 
 | 203 |  *           type: TextParameterValue | 
 | 204 |  *           use_template: | | 
 | 205 |  *             REPORT_SI_KAAS_UI: \$REPORT_SI_KAAS_UI | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 206 |  *     """ | 
 | 207 |  * | 
 | 208 |  *     runScenario(scenario) | 
 | 209 |  * | 
 | 210 |  */ | 
 | 211 |  | 
 | 212 | def runScenario(scenario) { | 
 | 213 |  | 
| Dennis Dmitriev | 79f3a2d | 2019-08-09 16:06:00 +0300 | [diff] [blame] | 214 |     // Clear description before adding new messages | 
 | 215 |     currentBuild.description = '' | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 216 |     // Collect the parameters for the jobs here | 
 | 217 |     global_variables = [:] | 
 | 218 |     // List of failed jobs to show at the end | 
 | 219 |     failed_jobs = [:] | 
 | 220 |  | 
 | 221 |     try { | 
 | 222 |         // Run the 'workflow' jobs | 
 | 223 |         runSteps(scenario['workflow'], global_variables, failed_jobs) | 
 | 224 |  | 
 | 225 |     } catch (InterruptedException x) { | 
 | 226 |         error "The job was aborted" | 
 | 227 |  | 
 | 228 |     } catch (e) { | 
 | 229 |         error("Build failed: " + e.toString()) | 
 | 230 |  | 
 | 231 |     } finally { | 
 | 232 |         // Run the 'finally' jobs | 
 | 233 |         runSteps(scenario['finally'], global_variables, failed_jobs) | 
 | 234 |  | 
 | 235 |         if (failed_jobs) { | 
| sgudz | 9ac09d2 | 2020-01-22 14:31:30 +0200 | [diff] [blame] | 236 |             statuses = [] | 
 | 237 |             failed_jobs.each { | 
| sgudz | 74c8cdd | 2020-01-23 14:26:32 +0200 | [diff] [blame] | 238 |                 statuses += it.value | 
| sgudz | 9ac09d2 | 2020-01-22 14:31:30 +0200 | [diff] [blame] | 239 |                 } | 
 | 240 |             if (statuses.contains('FAILURE')) { | 
 | 241 |                 currentBuild.result = 'FAILURE' | 
 | 242 |             } | 
 | 243 |             else if (statuses.contains('UNSTABLE')) { | 
 | 244 |                 currentBuild.result = 'UNSTABLE' | 
 | 245 |             } | 
 | 246 |             else { | 
 | 247 |                 currentBuild.result = 'FAILURE' | 
 | 248 |             } | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 249 |             println "Failed jobs: ${failed_jobs}" | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 250 |         } | 
| sgudz | 9ac09d2 | 2020-01-22 14:31:30 +0200 | [diff] [blame] | 251 |     } // finally | 
| Dennis Dmitriev | 5d8a153 | 2019-07-30 16:39:27 +0300 | [diff] [blame] | 252 | } |