2020-07-23 10:26:50 +02:00
package abaputils
import (
2020-08-04 17:52:28 +02:00
"bytes"
2020-07-23 10:26:50 +02:00
"encoding/json"
2020-08-13 09:48:40 +02:00
"fmt"
2020-08-04 17:52:28 +02:00
"io"
"io/ioutil"
"net/http"
2020-07-23 10:26:50 +02:00
"regexp"
2020-08-04 17:52:28 +02:00
"strconv"
"strings"
"time"
2020-07-23 10:26:50 +02:00
"github.com/SAP/jenkins-library/pkg/cloudfoundry"
"github.com/SAP/jenkins-library/pkg/command"
2020-08-04 17:52:28 +02:00
piperhttp "github.com/SAP/jenkins-library/pkg/http"
2020-07-23 10:26:50 +02:00
"github.com/SAP/jenkins-library/pkg/log"
"github.com/pkg/errors"
)
2020-08-21 16:02:46 +02:00
// AbapUtils Struct
2020-07-31 14:43:23 +02:00
type AbapUtils struct {
2020-08-07 11:09:58 +02:00
Exec command . ExecRunner
Intervall time . Duration
2020-07-31 14:43:23 +02:00
}
/ *
Communication for defining function used for communication
* /
type Communication interface {
GetAbapCommunicationArrangementInfo ( options AbapEnvironmentOptions , oDataURL string ) ( ConnectionDetailsHTTP , error )
2020-08-07 11:09:58 +02:00
GetPollIntervall ( ) time . Duration
2020-07-31 14:43:23 +02:00
}
2020-07-23 10:26:50 +02:00
2020-07-31 14:43:23 +02:00
// GetAbapCommunicationArrangementInfo function fetches the communcation arrangement information in SAP CP ABAP Environment
func ( abaputils * AbapUtils ) GetAbapCommunicationArrangementInfo ( options AbapEnvironmentOptions , oDataURL string ) ( ConnectionDetailsHTTP , error ) {
c := abaputils . Exec
2020-07-23 10:26:50 +02:00
var connectionDetails ConnectionDetailsHTTP
var error error
if options . Host != "" {
// Host, User and Password are directly provided -> check for host schema (double https)
match , err := regexp . MatchString ( ` ^(https|HTTPS):\/\/.* ` , options . Host )
if err != nil {
2021-08-04 17:31:16 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-07-23 10:26:50 +02:00
return connectionDetails , errors . Wrap ( err , "Schema validation for host parameter failed. Check for https." )
}
var hostOdataURL = options . Host + oDataURL
if match {
connectionDetails . URL = hostOdataURL
} else {
connectionDetails . URL = "https://" + hostOdataURL
}
connectionDetails . User = options . Username
connectionDetails . Password = options . Password
} else {
if options . CfAPIEndpoint == "" || options . CfOrg == "" || options . CfSpace == "" || options . CfServiceInstance == "" || options . CfServiceKeyName == "" {
var err = errors . New ( "Parameters missing. Please provide EITHER the Host of the ABAP server OR the Cloud Foundry ApiEndpoint, Organization, Space, Service Instance and a corresponding Service Key for the Communication Scenario SAP_COM_0510" )
2021-08-04 17:31:16 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-07-23 10:26:50 +02:00
return connectionDetails , err
}
// Url, User and Password should be read from a cf service key
var abapServiceKey , error = ReadServiceKeyAbapEnvironment ( options , c )
if error != nil {
return connectionDetails , errors . Wrap ( error , "Read service key failed" )
}
connectionDetails . URL = abapServiceKey . URL + oDataURL
connectionDetails . User = abapServiceKey . Abap . Username
connectionDetails . Password = abapServiceKey . Abap . Password
}
return connectionDetails , error
}
2020-08-04 17:52:28 +02:00
// ReadServiceKeyAbapEnvironment from Cloud Foundry and returns it. Depending on user/developer requirements if he wants to perform further Cloud Foundry actions
2020-07-23 10:26:50 +02:00
func ReadServiceKeyAbapEnvironment ( options AbapEnvironmentOptions , c command . ExecRunner ) ( AbapServiceKey , error ) {
var abapServiceKey AbapServiceKey
var serviceKeyJSON string
var err error
cfconfig := cloudfoundry . ServiceKeyOptions {
CfAPIEndpoint : options . CfAPIEndpoint ,
CfOrg : options . CfOrg ,
CfSpace : options . CfSpace ,
CfServiceInstance : options . CfServiceInstance ,
CfServiceKeyName : options . CfServiceKeyName ,
Username : options . Username ,
Password : options . Password ,
}
cf := cloudfoundry . CFUtils { Exec : c }
serviceKeyJSON , err = cf . ReadServiceKey ( cfconfig )
if err != nil {
// Executing cfReadServiceKeyScript failed
return abapServiceKey , err
}
// parse
json . Unmarshal ( [ ] byte ( serviceKeyJSON ) , & abapServiceKey )
if abapServiceKey == ( AbapServiceKey { } ) {
2021-08-04 17:31:16 +02:00
log . SetErrorCategory ( log . ErrorInfrastructure )
2021-01-26 21:23:59 +02:00
return abapServiceKey , errors . New ( "Parsing the service key failed. Service key is empty" )
2020-07-23 10:26:50 +02:00
}
log . Entry ( ) . Info ( "Service Key read successfully" )
return abapServiceKey , nil
}
2020-08-07 11:09:58 +02:00
/ *
GetPollIntervall returns the specified intervall from AbapUtils or a default value of 10 seconds
* /
func ( abaputils * AbapUtils ) GetPollIntervall ( ) time . Duration {
if abaputils . Intervall != 0 {
return abaputils . Intervall
}
return 10 * time . Second
}
// GetHTTPResponse wraps the SendRequest function of piperhttp
2020-08-04 17:52:28 +02:00
func GetHTTPResponse ( requestType string , connectionDetails ConnectionDetailsHTTP , body [ ] byte , client piperhttp . Sender ) ( * http . Response , error ) {
header := make ( map [ string ] [ ] string )
header [ "Content-Type" ] = [ ] string { "application/json" }
header [ "Accept" ] = [ ] string { "application/json" }
header [ "x-csrf-token" ] = [ ] string { connectionDetails . XCsrfToken }
2020-10-08 11:08:58 +02:00
httpResponse , err := client . SendRequest ( requestType , connectionDetails . URL , bytes . NewBuffer ( body ) , header , nil )
return httpResponse , err
2020-08-04 17:52:28 +02:00
}
// HandleHTTPError handles ABAP error messages which can occur when using OData services
2020-08-26 16:45:09 +02:00
//
// The point of this function is to enrich the error received from a HTTP Request (which is passed as a parameter to this function).
// Further error details may be present in the response body of the HTTP response.
2020-09-24 07:41:06 +02:00
// If the response body is parseable, the included details are wrapped around the original error from the HTTP repsponse.
2020-08-26 16:45:09 +02:00
// If this is not possible, the original error is returned.
2020-08-04 17:52:28 +02:00
func HandleHTTPError ( resp * http . Response , err error , message string , connectionDetails ConnectionDetailsHTTP ) error {
if resp == nil {
// Response is nil in case of a timeout
log . Entry ( ) . WithError ( err ) . WithField ( "ABAP Endpoint" , connectionDetails . URL ) . Error ( "Request failed" )
} else {
2020-08-26 16:45:09 +02:00
defer resp . Body . Close ( )
2020-08-04 17:52:28 +02:00
log . Entry ( ) . WithField ( "StatusCode" , resp . Status ) . Error ( message )
2020-08-26 16:45:09 +02:00
errorDetails , parsingError := getErrorDetailsFromResponse ( resp )
if parsingError != nil {
return err
2020-08-04 17:52:28 +02:00
}
2020-08-26 16:45:09 +02:00
abapError := errors . New ( errorDetails )
err = errors . Wrap ( abapError , err . Error ( ) )
}
return err
}
func getErrorDetailsFromResponse ( resp * http . Response ) ( errorString string , err error ) {
// Include the error message of the ABAP Environment system, if available
var abapErrorResponse AbapError
bodyText , readError := ioutil . ReadAll ( resp . Body )
if readError != nil {
return errorString , readError
}
var abapResp map [ string ] * json . RawMessage
errUnmarshal := json . Unmarshal ( bodyText , & abapResp )
if errUnmarshal != nil {
return errorString , errUnmarshal
}
if _ , ok := abapResp [ "error" ] ; ok {
2020-08-04 17:52:28 +02:00
json . Unmarshal ( * abapResp [ "error" ] , & abapErrorResponse )
if ( AbapError { } ) != abapErrorResponse {
log . Entry ( ) . WithField ( "ErrorCode" , abapErrorResponse . Code ) . Error ( abapErrorResponse . Message . Value )
2020-08-26 16:45:09 +02:00
errorString = fmt . Sprintf ( "%s - %s" , abapErrorResponse . Code , abapErrorResponse . Message . Value )
return errorString , nil
2020-08-04 17:52:28 +02:00
}
}
2020-08-26 16:45:09 +02:00
return errorString , errors . New ( "Could not parse the JSON error response" )
2020-08-04 17:52:28 +02:00
}
// ConvertTime formats an ABAP timestamp string from format /Date(1585576807000+0000)/ into a UNIX timestamp and returns it
func ConvertTime ( logTimeStamp string ) time . Time {
seconds := strings . TrimPrefix ( strings . TrimSuffix ( logTimeStamp , "000+0000)/" ) , "/Date(" )
n , error := strconv . ParseInt ( seconds , 10 , 64 )
if error != nil {
return time . Unix ( 0 , 0 ) . UTC ( )
}
t := time . Unix ( n , 0 ) . UTC ( )
return t
2020-07-23 10:26:50 +02:00
}
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Structs for specific steps *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
// AbapEnvironmentPullGitRepoOptions struct for the PullGitRepo piper step
type AbapEnvironmentPullGitRepoOptions struct {
AbapEnvOptions AbapEnvironmentOptions
RepositoryNames [ ] string ` json:"repositoryNames,omitempty" `
}
2020-08-04 17:52:28 +02:00
// AbapEnvironmentCheckoutBranchOptions struct for the CheckoutBranch piper step
type AbapEnvironmentCheckoutBranchOptions struct {
AbapEnvOptions AbapEnvironmentOptions
RepositoryName string ` json:"repositoryName,omitempty" `
}
2020-07-23 10:26:50 +02:00
// AbapEnvironmentRunATCCheckOptions struct for the RunATCCheck piper step
type AbapEnvironmentRunATCCheckOptions struct {
AbapEnvOptions AbapEnvironmentOptions
AtcConfig string ` json:"atcConfig,omitempty" `
}
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Structs for ABAP in general *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
//AbapEnvironmentOptions contains cloud foundry fields and the host parameter for connections to ABAP Environment instances
type AbapEnvironmentOptions struct {
Username string ` json:"username,omitempty" `
Password string ` json:"password,omitempty" `
Host string ` json:"host,omitempty" `
CfAPIEndpoint string ` json:"cfApiEndpoint,omitempty" `
CfOrg string ` json:"cfOrg,omitempty" `
CfSpace string ` json:"cfSpace,omitempty" `
CfServiceInstance string ` json:"cfServiceInstance,omitempty" `
CfServiceKeyName string ` json:"cfServiceKeyName,omitempty" `
}
// AbapMetadata contains the URI of metadata files
type AbapMetadata struct {
URI string ` json:"uri" `
}
// ConnectionDetailsHTTP contains fields for HTTP connections including the XCSRF token
type ConnectionDetailsHTTP struct {
User string ` json:"user" `
Password string ` json:"password" `
URL string ` json:"url" `
XCsrfToken string ` json:"xcsrftoken" `
}
// AbapError contains the error code and the error message for ABAP errors
type AbapError struct {
Code string ` json:"code" `
Message AbapErrorMessage ` json:"message" `
}
// AbapErrorMessage contains the lanuage and value fields for ABAP errors
type AbapErrorMessage struct {
Lang string ` json:"lang" `
Value string ` json:"value" `
}
// AbapServiceKey contains information about an ABAP service key
type AbapServiceKey struct {
SapCloudService string ` json:"sap.cloud.service" `
URL string ` json:"url" `
SystemID string ` json:"systemid" `
Abap AbapConnection ` json:"abap" `
Binding AbapBinding ` json:"binding" `
PreserveHostHeader bool ` json:"preserve_host_header" `
}
// AbapConnection contains information about the ABAP connection for the ABAP endpoint
type AbapConnection struct {
Username string ` json:"username" `
Password string ` json:"password" `
CommunicationScenarioID string ` json:"communication_scenario_id" `
CommunicationArrangementID string ` json:"communication_arrangement_id" `
CommunicationSystemID string ` json:"communication_system_id" `
CommunicationInboundUserID string ` json:"communication_inbound_user_id" `
CommunicationInboundUserAuthMode string ` json:"communication_inbound_user_auth_mode" `
}
// AbapBinding contains information about service binding in Cloud Foundry
type AbapBinding struct {
ID string ` json:"id" `
Type string ` json:"type" `
Version string ` json:"version" `
Env string ` json:"env" `
}
2020-07-31 14:43:23 +02:00
2020-08-04 17:52:28 +02:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Testing with a client mock *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
// ClientMock contains information about the client mock
type ClientMock struct {
Token string
Body string
BodyList [ ] string
StatusCode int
Error error
}
// SetOptions sets clientOptions for a client mock
func ( c * ClientMock ) SetOptions ( opts piperhttp . ClientOptions ) { }
// SendRequest sets a HTTP response for a client mock
func ( c * ClientMock ) SendRequest ( method , url string , bdy io . Reader , hdr http . Header , cookies [ ] * http . Cookie ) ( * http . Response , error ) {
var body [ ] byte
if c . Body != "" {
body = [ ] byte ( c . Body )
} else {
bodyString := c . BodyList [ len ( c . BodyList ) - 1 ]
c . BodyList = c . BodyList [ : len ( c . BodyList ) - 1 ]
body = [ ] byte ( bodyString )
}
header := http . Header { }
header . Set ( "X-Csrf-Token" , c . Token )
return & http . Response {
StatusCode : c . StatusCode ,
Header : header ,
Body : ioutil . NopCloser ( bytes . NewReader ( body ) ) ,
} , c . Error
}
2020-07-31 14:43:23 +02:00
// AUtilsMock mock
type AUtilsMock struct {
ReturnedConnectionDetailsHTTP ConnectionDetailsHTTP
ReturnedError error
}
// GetAbapCommunicationArrangementInfo mock
func ( autils * AUtilsMock ) GetAbapCommunicationArrangementInfo ( options AbapEnvironmentOptions , oDataURL string ) ( ConnectionDetailsHTTP , error ) {
return autils . ReturnedConnectionDetailsHTTP , autils . ReturnedError
}
2020-08-07 11:09:58 +02:00
// GetPollIntervall mock
func ( autils * AUtilsMock ) GetPollIntervall ( ) time . Duration {
return 1 * time . Microsecond
}
2020-07-31 14:43:23 +02:00
// Cleanup to reset AUtilsMock
func ( autils * AUtilsMock ) Cleanup ( ) {
autils . ReturnedConnectionDetailsHTTP = ConnectionDetailsHTTP { }
autils . ReturnedError = nil
}