2019-11-07 16:40:45 +02:00
import static com . sap . piper . Prerequisites . checkScript
import com.sap.piper.ConfigurationHelper
import com.sap.piper.GenerateDocumentation
import com.sap.piper.JenkinsUtils
import com.sap.piper.Utils
2019-12-16 23:02:30 +02:00
import com.sap.piper.BashUtils
2019-11-07 16:40:45 +02:00
import groovy.json.JsonSlurper
import hudson.AbortException
import groovy.transform.Field
import java.util.UUID
2019-12-16 23:02:30 +02:00
import java.util.regex.*
2019-11-07 16:40:45 +02:00
@Field def STEP_NAME = getClass ( ) . getName ( )
2019-12-16 23:02:30 +02:00
@Field Set STEP_CONFIG_KEYS = [
2019-11-07 16:40:45 +02:00
/ * *
* Specifies the host address of the SAP Cloud Platform ABAP Environment system
* /
'host' ,
/ * *
2019-12-16 23:02:30 +02:00
* Jenkins CredentialsId containing the communication user and password of the communciation scenario SAP_COM_0510
2019-11-07 16:40:45 +02:00
* /
2019-12-16 23:02:30 +02:00
'credentialsId' ,
2019-11-07 16:40:45 +02:00
/ * *
2019-12-16 23:02:30 +02:00
* Specifies the name of the Repository ( Software Component ) on the SAP Cloud Platform ABAP Environment system
2019-11-07 16:40:45 +02:00
* /
2019-12-16 23:02:30 +02:00
'repositoryName' ,
'cloudFoundry' ,
/ * *
* Cloud Foundry API endpoint .
* @parentConfigKey cloudFoundry
* /
'apiEndpoint' ,
'credentialsId' ,
/ * *
* Cloud Foundry target organization .
* @parentConfigKey cloudFoundry
* /
'org' ,
/ * *
* Cloud Foundry target space .
* @parentConfigKey cloudFoundry
* /
'space' ,
/ * *
* Cloud Foundry service instance , for which the service key will be created .
* @parentConfigKey cloudFoundry
* /
'serviceInstance' ,
/ * *
* Cloud Foundry service key , which will be created .
* @parentConfigKey cloudFoundry
* /
'serviceKey' ,
/** @see dockerExecute */
'dockerImage' ,
/** @see dockerExecute */
'dockerWorkspace'
]
@Field Set GENERAL_CONFIG_KEYS = STEP_CONFIG_KEYS
2019-11-07 16:40:45 +02:00
@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS
2019-12-16 23:02:30 +02:00
@Field Map CONFIG_KEY_COMPATIBILITY = [ cloudFoundry: [ apiEndpoint: 'cfApiEndpoint' , credentialsId: 'cfCredentialsId' , org: 'cfOrg' , space: 'cfSpace' , serviceInstance: 'cfServiceInstance' , serviceKey: 'cfServiceKey' ] ]
2019-11-07 16:40:45 +02:00
/ * *
* Pulls a Repository ( Software Component ) to a SAP Cloud Platform ABAP Environment system .
*
2019-12-16 23:02:30 +02:00
* This is either possible by providing the host and the credentialsId of the communication arrangement or by providing access to a service key for the communication arrangement SAP_COM_0510 on cloud foundry .
*
2019-11-07 16:40:45 +02:00
* ! ! ! note "Git Repository and Software Component"
* In SAP Cloud Platform ABAP Environment Git repositories are wrapped in Software Components ( which are managed in the App "Manage Software Components" )
* Currently , those two names are used synonymous .
* /
@GenerateDocumentation
void call ( Map parameters = [ : ] ) {
handlePipelineStepErrors ( stepName: STEP_NAME , stepParameters: parameters , failOnError: true ) {
def script = checkScript ( this , parameters ) ? : this
Map configuration = ConfigurationHelper . newInstance ( this )
2019-12-16 23:02:30 +02:00
. loadStepDefaults ( )
. mixinGeneralConfig ( script . commonPipelineEnvironment , GENERAL_CONFIG_KEYS , CONFIG_KEY_COMPATIBILITY )
. mixinStepConfig ( script . commonPipelineEnvironment , STEP_CONFIG_KEYS , CONFIG_KEY_COMPATIBILITY )
. mixinStageConfig ( script . commonPipelineEnvironment , parameters . stageName ? : env . STAGE_NAME , STEP_CONFIG_KEYS , CONFIG_KEY_COMPATIBILITY )
. mixin ( parameters , PARAMETER_KEYS , CONFIG_KEY_COMPATIBILITY )
2019-11-07 16:40:45 +02:00
. collectValidationFailures ( )
2019-12-16 23:02:30 +02:00
. withMandatoryProperty ( 'repositoryName' )
2019-11-07 16:40:45 +02:00
. use ( )
2019-12-16 23:02:30 +02:00
String userColonPassword
String urlString
if ( configuration . credentialsId ! = null & & configuration . host ! = null ) {
echo "[${STEP_NAME}] Info: Using configuration: credentialsId: $configuration.credentialsId and host: $configuration.host"
withCredentials ( [ usernamePassword ( credentialsId: configuration . credentialsId , usernameVariable: 'USER' , passwordVariable: 'PASSWORD' ) ] ) {
userColonPassword = "${USER}:${PASSWORD}"
urlString = 'https://' + configuration . host + '/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Pull'
}
} else {
echo "[${STEP_NAME}] Info: Using Cloud Foundry service key $configuration.cloudFoundry.serviceKey for service instance $configuration.cloudFoundry.serviceInstance"
dockerExecute ( script: script , dockerImage: configuration . dockerImage , dockerWorkspace: configuration . dockerWorkspace ) {
String jsonString = getServiceKey ( configuration )
Map responseJson = readJSON ( text : jsonString )
userColonPassword = responseJson . abap . username + ":" + responseJson . abap . password
urlString = responseJson . url + '/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Pull'
}
2019-11-12 18:40:59 +02:00
}
2019-12-16 23:02:30 +02:00
if ( userColonPassword ! = null & & urlString ! = null ) {
String authToken = userColonPassword . bytes . encodeBase64 ( ) . toString ( )
executeAbapEnvironmentPullGitRepo ( configuration , urlString , authToken )
} else {
error "[${STEP_NAME}] Error: Necessary parameters not available"
}
}
}
2019-11-12 18:40:59 +02:00
2019-12-16 23:02:30 +02:00
private String getServiceKey ( Map configuration ) {
String responseFile = "response-${UUID.randomUUID().toString()}.txt"
withCredentials ( [
usernamePassword ( credentialsId: configuration . cloudFoundry . credentialsId , passwordVariable: 'CF_PASSWORD' , usernameVariable: 'CF_USERNAME' )
] ) {
bashScript =
"" " # ! /bin/ bash
set + x
set - e
export HOME = $ { configuration . dockerWorkspace }
cf login - u $ { BashUtils . quoteAndEscape ( CF_USERNAME ) } - p $ { BashUtils . quoteAndEscape ( CF_PASSWORD ) } - a $ { configuration . cloudFoundry . apiEndpoint } - o $ { BashUtils . quoteAndEscape ( configuration . cloudFoundry . org ) } - s $ { BashUtils . quoteAndEscape ( configuration . cloudFoundry . space ) } ;
cf service - key $ { BashUtils . quoteAndEscape ( configuration . cloudFoundry . serviceInstance ) } $ { BashUtils . quoteAndEscape ( configuration . cloudFoundry . serviceKey ) } > \ "${responseFile}\"
"" "
String responseString
2019-11-07 16:40:45 +02:00
try {
2019-12-16 23:02:30 +02:00
def status = sh returnStatus: true , script: bashScript
if ( status ! = 0 ) {
echo "[${STEP_NAME}] Info: Could not get the service key $configuration.cloudFoundry.serviceKey for service instance $configuration.cloudFoundry.serviceInstance"
2019-11-07 16:40:45 +02:00
}
2019-12-16 23:02:30 +02:00
responseString = readFile ( responseFile )
2019-11-07 16:40:45 +02:00
} finally {
2019-12-16 23:02:30 +02:00
sh "cf logout"
sh script : "" " # ! /bin/ bash
rm - f $ { responseFile }
"" "
}
def p = Pattern . compile ( /\{.*\}$/ , Pattern . MULTILINE | Pattern . DOTALL )
def m = responseString = ~ p
String jsonString
if ( m . find ( ) ) {
return m [ 0 ]
} else {
echo "[${STEP_NAME}] Info: Could not parse the service key $configuration.cloudFoundry.serviceKey"
return null
2019-11-07 16:40:45 +02:00
}
}
}
2019-12-16 23:02:30 +02:00
private executeAbapEnvironmentPullGitRepo ( Map configuration , String urlString , String authToken ) {
echo "[${STEP_NAME}] General Parameters: URL = \"${urlString}\", repositoryName = \"${configuration.repositoryName}\""
HeaderFiles headerFiles = new HeaderFiles ( )
try {
String urlPullEntity = triggerPull ( configuration , urlString , authToken , headerFiles )
if ( urlPullEntity ! = null ) {
String finalStatus = pollPullStatus ( urlPullEntity , authToken , headerFiles )
if ( finalStatus ! = 'S' ) {
error "[${STEP_NAME}] Pull Failed"
}
} else {
error "[${STEP_NAME}] Pull Failed"
}
} finally {
workspaceCleanup ( headerFiles )
}
}
2019-11-07 16:40:45 +02:00
2019-12-16 23:02:30 +02:00
private String triggerPull ( Map configuration , String url , String authToken , HeaderFiles headerFiles ) {
2019-11-07 16:40:45 +02:00
String entityUri = null
def xCsrfTokenScript = "" " # ! /bin/ bash
curl - I - X GET $ { url } \
- H 'Authorization: Basic ${authToken}' \
- H 'Accept: application/json' \
- H 'x-csrf-token: fetch' \
- D $ { headerFiles . authFile } \
"" "
sh ( script : xCsrfTokenScript , returnStdout: true )
HttpHeaderProperties headerProperties = new HttpHeaderProperties ( readFile ( headerFiles . authFile ) )
checkRequestStatus ( headerProperties )
def scriptPull = "" " # ! /bin/ bash
curl - X POST \ "${url}\" \
- H 'Authorization: Basic ${authToken}' \
- H 'Accept: application/json' \
- H 'Content-Type: application/json' \
- H 'x-csrf-token: ${headerProperties.xCsrfToken}' \
- - cookie $ { headerFiles . authFile } \
- D $ { headerFiles . postFile } \
- d '{ \"sc_name\": \"${configuration.repositoryName}\" }'
"" "
def response = sh (
script : scriptPull ,
returnStdout: true )
checkRequestStatus ( new HttpHeaderProperties ( readFile ( headerFiles . postFile ) ) )
JsonSlurper slurper = new JsonSlurper ( )
Map responseJson = slurper . parseText ( response )
if ( responseJson . d ! = null ) {
entityUri = responseJson . d . __metadata . uri . toString ( )
echo "[${STEP_NAME}] Pull Status: ${responseJson.d.status_descr.toString()}"
} else {
error "[${STEP_NAME}] Error: ${responseJson?.error?.message?.value?.toString()?:'No message available'}"
}
echo "[${STEP_NAME}] Entity URI: ${entityUri}"
return entityUri
}
private String pollPullStatus ( String url , String authToken , HeaderFiles headerFiles ) {
String status = "R" ;
while ( status = = "R" ) {
Thread . sleep ( 5000 )
def pollScript = "" " # ! /bin/ bash
curl - X GET "${url}" \
- H 'Authorization: Basic ${authToken}' \
- H 'Accept: application/json' \
- D $ { headerFiles . pollFile }
"" "
def pollResponse = sh (
script : pollScript ,
returnStdout: true )
checkRequestStatus ( new HttpHeaderProperties ( readFile ( headerFiles . pollFile ) ) )
JsonSlurper slurper = new JsonSlurper ( )
Map pollResponseJson = slurper . parseText ( pollResponse )
if ( pollResponseJson . d ! = null ) {
status = pollResponseJson . d . status . toString ( )
} else {
error "[${STEP_NAME}] Error: ${pollResponseJson?.error?.message?.value?.toString()?:'No message available'}"
}
echo "[${STEP_NAME}] Pull Status: ${pollResponseJson.d.status_descr.toString()}"
}
return status
}
private void checkRequestStatus ( HttpHeaderProperties httpHeader ) {
if ( httpHeader . statusCode = = 400 ) {
echo "[${STEP_NAME}] Info: ${httpHeader.statusCode} ${httpHeader.statusMessage}"
} else if ( httpHeader . statusCode > 201 ) {
error "[${STEP_NAME}] Error: ${httpHeader.statusCode} ${httpHeader.statusMessage}"
}
}
private void workspaceCleanup ( HeaderFiles headerFiles ) {
String cleanupScript = "" " # ! /bin/ bash
rm - f $ { headerFiles . authFile } $ { headerFiles . postFile } $ { headerFiles . pollFile }
"" "
sh ( script : cleanupScript )
}
public class HttpHeaderProperties {
Integer statusCode
String statusMessage
String xCsrfToken
HttpHeaderProperties ( String header ) {
def statusCodeRegex = header = ~ /(?<=HTTP\/ 1 . [ 0 - 9 ] \ s ) [ 0 - 9 ] { 3 } ( ? = \ s ) /
if ( statusCodeRegex . find ( ) ) {
statusCode = statusCodeRegex [ 0 ] . toInteger ( )
}
def statusMessageRegex = header = ~ /(?<=HTTP\/ 1 . [ 0 - 9 ] \ s [ 0 - 9 ] { 3 } \ s ) . * /
if ( statusMessageRegex . find ( ) ) {
statusMessage = statusMessageRegex [ 0 ]
}
def xCsrfTokenRegex = header = ~ /(?<=x-csrf-token:\s).*/
if ( xCsrfTokenRegex . find ( ) ) {
xCsrfToken = xCsrfTokenRegex [ 0 ]
}
}
}
public class HeaderFiles {
String authFile
String postFile
String pollFile
HeaderFiles ( ) {
String uuid = UUID . randomUUID ( ) . toString ( )
this . authFile = "headerFileAuth-${uuid}.txt"
this . postFile = "headerFilePost-${uuid}.txt"
this . pollFile = "headerFilePoll-${uuid}.txt"
}
}