github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/src/com/sap/piper/cm/ChangeManagement.groovy (about) 1 package com.sap.piper.cm 2 3 import com.sap.piper.GitUtils 4 5 import groovy.json.JsonSlurper 6 import hudson.AbortException 7 8 9 public class ChangeManagement implements Serializable { 10 11 private script 12 private GitUtils gitUtils 13 14 public ChangeManagement(def script, GitUtils gitUtils = null) { 15 this.script = script 16 this.gitUtils = gitUtils ?: new GitUtils() 17 } 18 19 String getChangeDocumentId( 20 String from = 'origin/master', 21 String to = 'HEAD', 22 String label = 'ChangeDocument\\s?:', 23 String format = '%b' 24 ) { 25 26 return getLabeledItem('ChangeDocumentId', from, to, label, format) 27 } 28 29 String getTransportRequestId( 30 String from = 'origin/master', 31 String to = 'HEAD', 32 String label = 'TransportRequest\\s?:', 33 String format = '%b' 34 ) { 35 36 return getLabeledItem('TransportRequestId', from, to, label, format) 37 } 38 39 private String getLabeledItem( 40 String name, 41 String from, 42 String to, 43 String label, 44 String format 45 ) { 46 47 if( ! gitUtils.insideWorkTree() ) { 48 throw new ChangeManagementException("Cannot retrieve ${name}. Not in a git work tree. ${name} is extracted from git commit messages.") 49 } 50 51 def items = gitUtils.extractLogLines(".*${label}.*", from, to, format) 52 .collect { line -> line?.replaceAll(label,'')?.trim() } 53 .unique() 54 55 items.retainAll { line -> line != null && ! line.isEmpty() } 56 57 if( items.size() == 0 ) { 58 throw new ChangeManagementException("Cannot retrieve ${name} from git commits. ${name} retrieved from git commit messages via pattern '${label}'.") 59 } else if (items.size() > 1) { 60 throw new ChangeManagementException("Multiple ${name}s found: ${items}. ${name} retrieved from git commit messages via pattern '${label}'.") 61 } 62 63 return items[0] 64 } 65 66 boolean isChangeInDevelopment(Map docker, String changeId, String endpoint, String credentialsId, String clientOpts = '') { 67 int rc = executeWithCredentials(BackendType.SOLMAN, docker, endpoint, credentialsId, 'is-change-in-development', ['-cID', "'${changeId}'", '--return-code'], 68 false, 69 clientOpts) as int 70 71 if (rc == 0) { 72 return true 73 } else if (rc == 3) { 74 return false 75 } else { 76 throw new ChangeManagementException("Cannot retrieve status for change document '${changeId}'. Does this change exist? Return code from cmclient: ${rc}.") 77 } 78 } 79 80 String createTransportRequestCTS(Map docker, String transportType, String targetSystemId, String description, String endpoint, String credentialsId, String clientOpts = '') { 81 try { 82 def transportRequest = executeWithCredentials(BackendType.CTS, docker, endpoint, credentialsId, 'create-transport', 83 ['-tt', transportType, '-ts', targetSystemId, '-d', "\"${description}\""], 84 true, 85 clientOpts) 86 return (transportRequest as String)?.trim() 87 }catch(AbortException e) { 88 throw new ChangeManagementException("Cannot create a transport request. $e.message.") 89 } 90 } 91 92 String createTransportRequestSOLMAN(Map docker, String changeId, String developmentSystemId, String endpoint, String credentialsId, String clientOpts = '') { 93 94 try { 95 def transportRequest = executeWithCredentials(BackendType.SOLMAN, docker, endpoint, credentialsId, 'create-transport', ['-cID', changeId, '-dID', developmentSystemId], 96 true, 97 clientOpts) 98 return (transportRequest as String)?.trim() 99 }catch(AbortException e) { 100 throw new ChangeManagementException("Cannot create a transport request for change id '$changeId'. $e.message.") 101 } 102 } 103 104 String createTransportRequestRFC( 105 Map docker, 106 String endpoint, 107 String developmentInstance, 108 String developmentClient, 109 String credentialsId, 110 String description, 111 boolean verbose) { 112 113 def command = 'cts createTransportRequest' 114 def args = [ 115 TRANSPORT_DESCRIPTION: description, 116 ABAP_DEVELOPMENT_INSTANCE: developmentInstance, 117 ABAP_DEVELOPMENT_CLIENT: developmentClient, 118 VERBOSE: verbose, 119 ] 120 121 try { 122 123 def transportRequestId = executeWithCredentials( 124 BackendType.RFC, 125 docker, 126 endpoint, 127 credentialsId, 128 command, 129 args, 130 true) 131 132 return new JsonSlurper().parseText(transportRequestId).REQUESTID 133 134 } catch(AbortException ex) { 135 throw new ChangeManagementException( 136 "Cannot create transport request: ${ex.getMessage()}", ex) 137 } 138 } 139 140 void uploadFileToTransportRequestSOLMAN( 141 Map docker, 142 String changeId, 143 String transportRequestId, 144 String applicationId, 145 String filePath, 146 String endpoint, 147 String credentialsId, 148 String cmclientOpts = '') { 149 150 def args = [ 151 '-cID', changeId, 152 '-tID', transportRequestId, 153 applicationId, "\"$filePath\"" 154 ] 155 156 int rc = executeWithCredentials( 157 BackendType.SOLMAN, 158 docker, 159 endpoint, 160 credentialsId, 161 'upload-file-to-transport', 162 args, 163 false, 164 cmclientOpts) as int 165 166 if(rc != 0) { 167 throw new ChangeManagementException( 168 "Cannot upload file into transport request. Return code from cm client: $rc.") 169 } 170 } 171 172 void uploadFileToTransportRequestCTS( 173 Map docker, 174 String transportRequestId, 175 String endpoint, 176 String client, 177 String applicationName, 178 String description, 179 String abapPackage, // "package" would be better, but this is a keyword 180 String osDeployUser, 181 def deployToolDependencies, 182 def npmInstallOpts, 183 String deployConfigFile, 184 String credentialsId) { 185 186 def script = this.script 187 188 def desc = description ?: 'Deployed with Piper based on SAP Fiori tools' 189 190 if (deployToolDependencies in List) { 191 deployToolDependencies = deployToolDependencies.join(' ') 192 } 193 194 if (npmInstallOpts in List) { 195 npmInstallOpts = npmInstallOpts.join(' ') 196 } 197 198 deployToolDependencies = deployToolDependencies.trim() 199 200 /* 201 In case the configuration has been adjusted so that no deployToolDependencies are provided 202 we assume an image is used, which already contains all dependencies. 203 In this case we don't invoke npm install and we run the image with the standard user 204 already, since there is no need for being root. Hence we don't need to switch user also 205 in the script. 206 */ 207 boolean noInstall = deployToolDependencies.isEmpty() 208 209 Iterable cmd = ['#!/bin/bash -e'] 210 211 if (! noInstall) { 212 cmd << "npm install --global ${npmInstallOpts} ${deployToolDependencies}" 213 cmd << "su ${osDeployUser}" 214 } else { 215 script.echo "[INFO] no deploy dependencies provided. Skipping npm install call. Assuming docker image '${docker?.image}' already contains the dependencies for performing the deployment." 216 } 217 218 Iterable params = [] 219 220 boolean useConfigFile = true, noConfig = false 221 222 if (!deployConfigFile) { 223 useConfigFile = false 224 noConfig = !script.fileExists('ui5-deploy.yaml') 225 } else { 226 if (script.fileExists(deployConfigFile)) { 227 // in this case we will use the config file 228 useConfigFile = true 229 } else { 230 if (deployConfigFile == 'ui5-deploy.yaml') { 231 // in this case this is most likely provided by the piper default config and 232 // it was not explicitly configured. Hence we assume not having a config file 233 useConfigFile = false 234 noConfig = true 235 } else { 236 script.error("Configured deploy config file '${deployConfigFile}' does not exists.") 237 } 238 } 239 } 240 241 if (noConfig) { 242 params += ['--noConfig'] // no config file, but we will provide our parameters 243 } 244 245 if (useConfigFile) { 246 params += ['-c', "\"" + deployConfigFile + "\""] 247 } 248 249 // 250 // All the parameters below encapsulated in an if statement might also be provided in a config file. 251 // In case they are empty we don't add to the command line and we trust in the config file. 252 // In case they are finally missing the fiori deploy toolset will tell us. 253 // 254 255 if (transportRequestId) { 256 params += ['-t', transportRequestId] 257 } 258 259 if (endpoint) { 260 params += ['-u', endpoint] 261 } 262 263 if (abapPackage) { 264 params += ['-p', abapPackage] 265 } 266 267 if (applicationName) { 268 params += ['-n' , applicationName] 269 } 270 271 if (client) { 272 params += ['-l', client] 273 } 274 275 params += ['-e', "\"" + desc + "\""] 276 277 params += ['-f'] // failfast --> provide return code != 0 in case of any failure 278 279 params += ['-y'] // autoconfirm --> no need to press 'y' key in order to confirm the params and trigger the deployment 280 281 // Here we provide the names of the environment variable holding username and password. Below we set these values. 282 params += ['--username', 'ABAP_USER', '--password', 'ABAP_PASSWORD'] 283 284 def fioriDeployCmd = "fiori deploy ${params.join(' ')}" 285 script.echo "Executing deploy command: '${fioriDeployCmd}'" 286 cmd << fioriDeployCmd 287 288 script.withCredentials([script.usernamePassword( 289 credentialsId: credentialsId, 290 passwordVariable: 'password', 291 usernameVariable: 'username')]) { 292 293 /* 294 After installing the deploy toolset we switch the user. Since we do not `su` with option `-l` the 295 environment variables are preserved. Hence the environment variables for user and password are 296 still available after the user switch. 297 */ 298 def dockerEnvVars = docker.envVars ?: [:] 299 dockerEnvVars += [ABAP_USER: script.username, ABAP_PASSWORD: script.password] 300 301 def dockerOptions = docker.options ?: [] 302 if (!noInstall) { 303 // when we install globally we need to be root, after preparing that we can su node` in the bash script. 304 // in case there is already a u provided the latest (... what we set here wins). 305 dockerOptions += ['-u', '0'] 306 } 307 308 script.dockerExecute( 309 script: script, 310 dockerImage: docker.image, 311 dockerOptions: dockerOptions, 312 dockerEnvVars: dockerEnvVars, 313 dockerPullImage: docker.pullImage) { 314 315 script.sh script: cmd.join('\n') 316 } 317 } 318 } 319 320 void uploadFileToTransportRequestRFC( 321 Map docker, 322 String transportRequestId, 323 String applicationName, 324 String filePath, 325 String endpoint, 326 String credentialsId, 327 String developmentInstance, 328 String developmentClient, 329 String applicationDescription, 330 String abapPackage, 331 String codePage, 332 boolean acceptUnixStyleEndOfLine, 333 boolean failOnWarning, 334 boolean verbose) { 335 336 def args = [ 337 ABAP_DEVELOPMENT_INSTANCE: developmentInstance, 338 ABAP_DEVELOPMENT_CLIENT: developmentClient, 339 ABAP_APPLICATION_NAME: applicationName, 340 ABAP_APPLICATION_DESC: applicationDescription, 341 ABAP_PACKAGE: abapPackage, 342 ZIP_FILE_URL: filePath, 343 CODE_PAGE: codePage, 344 ABAP_ACCEPT_UNIX_STYLE_EOL: acceptUnixStyleEndOfLine ? 'X' : '-', 345 FAIL_UPLOAD_ON_WARNING: Boolean.toString(failOnWarning), 346 VERBOSE: Boolean.toString(verbose), 347 ] 348 349 int rc = executeWithCredentials( 350 BackendType.RFC, 351 docker, 352 endpoint, 353 credentialsId, 354 "cts uploadToABAP:${transportRequestId}", 355 args, 356 false) as int 357 358 if(rc != 0) { 359 throw new ChangeManagementException( 360 "Cannot upload file into transport request. Return code from rfc client: $rc.") 361 } 362 } 363 364 def executeWithCredentials( 365 BackendType type, 366 Map docker, 367 String endpoint, 368 String credentialsId, 369 String command, 370 def args, 371 boolean returnStdout = false, 372 String clientOpts = '') { 373 374 def script = this.script 375 376 docker = docker ?: [:] 377 378 script.withCredentials([script.usernamePassword( 379 credentialsId: credentialsId, 380 passwordVariable: 'password', 381 usernameVariable: 'username')]) { 382 383 Map shArgs = [:] 384 385 if(returnStdout) 386 shArgs.put('returnStdout', true) 387 else 388 shArgs.put('returnStatus', true) 389 390 Map dockerEnvVars = docker.envVars ?: [:] 391 392 def result = 1 393 394 switch(type) { 395 396 case BackendType.RFC: 397 398 if(! (args in Map)) { 399 throw new IllegalArgumentException("args expected as Map for backend types ${[BackendType.RFC]}") 400 } 401 402 shArgs.script = command 403 404 args = args.plus([ 405 ABAP_DEVELOPMENT_SERVER: endpoint, 406 ABAP_DEVELOPMENT_USER: script.username, 407 ABAP_DEVELOPMENT_PASSWORD: script.password, 408 ]) 409 410 dockerEnvVars += args 411 412 break 413 414 case BackendType.SOLMAN: 415 case BackendType.CTS: 416 417 if(! (args in Collection)) 418 throw new IllegalArgumentException("args expected as Collection for backend types ${[BackendType.SOLMAN, BackendType.CTS]}") 419 420 shArgs.script = getCMCommandLine(type, endpoint, script.username, script.password, 421 command, args, 422 clientOpts) 423 424 break 425 } 426 427 // user and password are masked by withCredentials 428 script.echo """[INFO] Executing command line: "${shArgs.script}".""" 429 430 script.dockerExecute( 431 script: script, 432 dockerImage: docker.image, 433 dockerOptions: docker.options, 434 dockerEnvVars: dockerEnvVars, 435 dockerPullImage: docker.pullImage, 436 dockerVolumeBind: docker.volumeBind ?: [:]) { 437 438 result = script.sh(shArgs) 439 440 } 441 442 return result 443 } 444 } 445 446 void releaseTransportRequestSOLMAN( 447 Map docker, 448 String changeId, 449 String transportRequestId, 450 String endpoint, 451 String credentialsId, 452 String clientOpts = '') { 453 454 def cmd = 'release-transport' 455 def args = [ 456 '-cID', 457 changeId, 458 '-tID', 459 transportRequestId, 460 ] 461 462 int rc = executeWithCredentials( 463 BackendType.SOLMAN, 464 docker, 465 endpoint, 466 credentialsId, 467 cmd, 468 args, 469 false, 470 clientOpts) as int 471 472 if(rc != 0) { 473 throw new ChangeManagementException("Cannot release Transport Request '$transportRequestId'. Return code from cmclient: $rc.") 474 } 475 } 476 477 void releaseTransportRequestCTS( 478 Map docker, 479 String transportRequestId, 480 String endpoint, 481 String credentialsId, 482 String clientOpts = '') { 483 484 def cmd = 'export-transport' 485 def args = [ 486 '-tID', 487 transportRequestId, 488 ] 489 490 int rc = executeWithCredentials( 491 BackendType.CTS, 492 docker, 493 endpoint, 494 credentialsId, 495 cmd, 496 args, 497 false) as int 498 499 if(rc != 0) { 500 throw new ChangeManagementException("Cannot release Transport Request '$transportRequestId'. Return code from cmclient: $rc.") 501 } 502 } 503 504 void releaseTransportRequestRFC( 505 Map docker, 506 String transportRequestId, 507 String endpoint, 508 String developmentInstance, 509 String developmentClient, 510 String credentialsId, 511 boolean verbose) { 512 513 def cmd = "cts releaseTransport:${transportRequestId}" 514 def args = [ 515 ABAP_DEVELOPMENT_INSTANCE: developmentInstance, 516 ABAP_DEVELOPMENT_CLIENT: developmentClient, 517 VERBOSE: verbose, 518 ] 519 520 int rc = executeWithCredentials( 521 BackendType.RFC, 522 docker, 523 endpoint, 524 credentialsId, 525 cmd, 526 args, 527 false) as int 528 529 if(rc != 0) { 530 throw new ChangeManagementException("Cannot release Transport Request '$transportRequestId'. Return code from rfcclient: $rc.") 531 } 532 533 } 534 535 String getCMCommandLine(BackendType type, 536 String endpoint, 537 String username, 538 String password, 539 String command, 540 List<String> args, 541 String clientOpts = '') { 542 String cmCommandLine = '#!/bin/bash' 543 if(clientOpts) { 544 cmCommandLine += """ 545 export CMCLIENT_OPTS="${clientOpts}" """ 546 } 547 cmCommandLine += """ 548 cmclient -e '$endpoint' \ 549 -u '$username' \ 550 -p '$password' \ 551 -t ${type} \ 552 ${command} ${(args as Iterable).join(' ')} 553 """ 554 return cmCommandLine 555 } 556 }