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  }