2023-12-19 15:16:48 +01:00
package abaputils
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"reflect"
2024-08-16 13:41:23 +02:00
"regexp"
2023-12-19 15:16:48 +01:00
"strings"
"time"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/pkg/errors"
"k8s.io/utils/strings/slices"
)
type SAP_COM_0948 struct {
con ConnectionDetailsHTTP
client piperhttp . Sender
repository Repository
path string
cloneAction string
pullAction string
softwareComponentEntity string
branchEntity string
tagsEntity string
checkoutAction string
actionsEntity string
uuid string
failureMessage string
maxRetries int
retryBaseSleepUnit time . Duration
retryMaxSleepTime time . Duration
retryAllowedErrorCodes [ ] string
}
func ( api * SAP_COM_0948 ) init ( con ConnectionDetailsHTTP , client piperhttp . Sender , repo Repository ) {
api . con = con
api . client = client
api . repository = repo
api . path = "/sap/opu/odata4/sap/a4c_mswc_api/srvd_a2x/sap/manage_software_components/0001"
api . checkoutAction = "/SAP__self.checkout_branch"
api . softwareComponentEntity = "/SoftwareComponents"
api . actionsEntity = "/Actions"
api . branchEntity = "/Branches"
api . cloneAction = "/SAP__self.clone"
api . pullAction = "/SAP__self.pull"
api . tagsEntity = "/Tags"
api . failureMessage = "The action of the Repository / Software Component " + api . repository . Name + " failed"
api . maxRetries = 3
api . setSleepTimeConfig ( 1 * time . Second , 120 * time . Second )
api . retryAllowedErrorCodes = append ( api . retryAllowedErrorCodes , "A4C_A2G/228" )
api . retryAllowedErrorCodes = append ( api . retryAllowedErrorCodes , "A4C_A2G/501" )
}
func ( api * SAP_COM_0948 ) getUUID ( ) string {
return api . uuid
}
2024-04-24 16:01:34 +02:00
// reads the execution log from the ABAP system
func ( api * SAP_COM_0948 ) GetExecutionLog ( ) ( execLog ExecutionLog , err error ) {
connectionDetails := api . con
connectionDetails . URL = api . con . URL + api . path + api . actionsEntity + "/" + api . getUUID ( ) + "/_Execution_log"
resp , err := GetHTTPResponse ( "GET" , connectionDetails , nil , api . client )
if err != nil {
log . SetErrorCategory ( log . ErrorInfrastructure )
2024-08-16 13:41:23 +02:00
_ , err = handleHTTPError ( resp , err , api . failureMessage , connectionDetails )
2024-04-24 16:01:34 +02:00
return execLog , err
}
defer resp . Body . Close ( )
// Parse response
bodyText , _ := io . ReadAll ( resp . Body )
marshallError := json . Unmarshal ( bodyText , & execLog )
if marshallError != nil {
return execLog , errors . Wrap ( marshallError , "Could not parse response from the ABAP Environment system" )
}
if reflect . DeepEqual ( ExecutionLog { } , execLog ) {
log . Entry ( ) . WithField ( "StatusCode" , resp . Status ) . Error ( api . failureMessage )
log . SetErrorCategory ( log . ErrorInfrastructure )
var err = errors . New ( "Request to ABAP System not successful" )
return execLog , err
}
return execLog , nil
}
2023-12-19 15:16:48 +01:00
func ( api * SAP_COM_0948 ) CreateTag ( tag Tag ) error {
if reflect . DeepEqual ( Tag { } , tag ) {
return errors . New ( "No Tag provided" )
}
con := api . con
con . URL = api . con . URL + api . path + api . tagsEntity
requestBodyStruct := CreateTagBody { RepositoryName : api . repository . Name , CommitID : api . repository . CommitID , Tag : tag . TagName , Description : tag . TagDescription }
jsonBody , err := json . Marshal ( & requestBodyStruct )
if err != nil {
return err
}
return api . triggerRequest ( con , jsonBody )
}
func ( api * SAP_COM_0948 ) CheckoutBranch ( ) error {
if api . repository . Name == "" || api . repository . Branch == "" {
return fmt . Errorf ( "Failed to trigger checkout: %w" , errors . New ( "Repository and/or Branch Configuration is empty. Please make sure that you have specified the correct values" ) )
}
checkoutConnectionDetails := api . con
checkoutConnectionDetails . URL = api . con . URL + api . path + api . branchEntity + api . getRepoNameForPath ( ) + api . getBranchNameForPath ( ) + api . checkoutAction
jsonBody := [ ] byte ( ` {
"import_mode" : "" ,
"execution_mode" : ""
} ` )
return api . triggerRequest ( checkoutConnectionDetails , jsonBody )
}
func ( api * SAP_COM_0948 ) parseActionResponse ( resp * http . Response , err error ) ( ActionEntity , error ) {
var body ActionEntity
bodyText , errRead := io . ReadAll ( resp . Body )
if errRead != nil {
return ActionEntity { } , err
}
if err := json . Unmarshal ( bodyText , & body ) ; err != nil {
return ActionEntity { } , err
}
if reflect . DeepEqual ( ActionEntity { } , body ) {
log . Entry ( ) . WithField ( "StatusCode" , resp . Status ) . WithField ( "branchName" , api . repository . Branch ) . Error ( "Could not switch to specified branch" )
err := errors . New ( "Request to ABAP System not successful" )
return ActionEntity { } , err
}
return body , nil
}
func ( api * SAP_COM_0948 ) Pull ( ) error {
// Trigger the Pull of a Repository
if api . repository . Name == "" {
return errors . New ( "An empty string was passed for the parameter 'repositoryName'" )
}
pullConnectionDetails := api . con
pullConnectionDetails . URL = api . con . URL + api . path + api . softwareComponentEntity + api . getRepoNameForPath ( ) + api . pullAction
jsonBody := [ ] byte ( api . repository . GetPullActionRequestBody ( ) )
return api . triggerRequest ( pullConnectionDetails , jsonBody )
}
func ( api * SAP_COM_0948 ) GetLogProtocol ( logOverviewEntry LogResultsV2 , page int ) ( result [ ] LogProtocol , count int , err error ) {
connectionDetails := api . con
connectionDetails . URL = api . con . URL + api . path + api . actionsEntity + "/" + api . getUUID ( ) + "/_Log_Overview" + "/" + fmt . Sprint ( logOverviewEntry . Index ) + "/_Log_Protocol" + api . getLogProtocolQuery ( page )
resp , err := GetHTTPResponse ( "GET" , connectionDetails , nil , api . client )
if err != nil {
log . SetErrorCategory ( log . ErrorInfrastructure )
2024-08-16 13:41:23 +02:00
_ , err = handleHTTPError ( resp , err , api . failureMessage , connectionDetails )
2023-12-19 15:16:48 +01:00
return nil , 0 , err
}
defer resp . Body . Close ( )
// Parse response
var body LogProtocolResultsV4
bodyText , _ := io . ReadAll ( resp . Body )
marshallError := json . Unmarshal ( bodyText , & body )
if marshallError != nil {
return nil , 0 , errors . Wrap ( marshallError , "Could not parse response from the ABAP Environment system" )
}
return body . Results , body . Count , nil
}
func ( api * SAP_COM_0948 ) GetLogOverview ( ) ( result [ ] LogResultsV2 , err error ) {
connectionDetails := api . con
connectionDetails . URL = api . con . URL + api . path + api . actionsEntity + "/" + api . getUUID ( ) + "/_Log_Overview"
resp , err := GetHTTPResponse ( "GET" , connectionDetails , nil , api . client )
if err != nil {
log . SetErrorCategory ( log . ErrorInfrastructure )
2024-08-16 13:41:23 +02:00
_ , err = handleHTTPError ( resp , err , api . failureMessage , connectionDetails )
2023-12-19 15:16:48 +01:00
return nil , err
}
defer resp . Body . Close ( )
// Parse response
var abapResp map [ string ] * json . RawMessage
bodyText , _ := io . ReadAll ( resp . Body )
marshallError := json . Unmarshal ( bodyText , & abapResp )
if marshallError != nil {
return nil , errors . Wrap ( marshallError , "Could not parse response from the ABAP Environment system" )
}
marshallError = json . Unmarshal ( * abapResp [ "value" ] , & result )
if marshallError != nil {
return nil , errors . Wrap ( marshallError , "Could not parse response from the ABAP Environment system" )
}
if reflect . DeepEqual ( LogResultsV2 { } , result ) {
log . Entry ( ) . WithField ( "StatusCode" , resp . Status ) . Error ( api . failureMessage )
log . SetErrorCategory ( log . ErrorInfrastructure )
var err = errors . New ( "Request to ABAP System not successful" )
return nil , err
}
return result , nil
}
func ( api * SAP_COM_0948 ) GetAction ( ) ( string , error ) {
connectionDetails := api . con
connectionDetails . URL = api . con . URL + api . path + api . actionsEntity + "/" + api . getUUID ( )
resp , err := GetHTTPResponse ( "GET" , connectionDetails , nil , api . client )
if err != nil {
log . SetErrorCategory ( log . ErrorInfrastructure )
2024-08-16 13:41:23 +02:00
_ , err = handleHTTPError ( resp , err , api . failureMessage , connectionDetails )
2023-12-19 15:16:48 +01:00
return "E" , err
}
defer resp . Body . Close ( )
// Parse Response
body , parseError := api . parseActionResponse ( resp , err )
if parseError != nil {
return "E" , parseError
}
api . uuid = body . UUID
abapStatusCode := body . Status
log . Entry ( ) . Info ( "Status: " + abapStatusCode + " - " + body . StatusDescription )
return abapStatusCode , nil
}
2024-10-10 13:42:09 +02:00
func ( api * SAP_COM_0948 ) getRepositoryName ( ) string {
return api . repository . Name
}
2024-06-25 10:09:31 +02:00
func ( api * SAP_COM_0948 ) GetRepository ( ) ( bool , string , error , bool ) {
2023-12-19 15:16:48 +01:00
if api . repository . Name == "" {
2024-06-25 10:09:31 +02:00
return false , "" , errors . New ( "An empty string was passed for the parameter 'repositoryName'" ) , false
2023-12-19 15:16:48 +01:00
}
swcConnectionDetails := api . con
swcConnectionDetails . URL = api . con . URL + api . path + api . softwareComponentEntity + api . getRepoNameForPath ( )
resp , err := GetHTTPResponse ( "GET" , swcConnectionDetails , nil , api . client )
if err != nil {
2024-08-16 13:41:23 +02:00
_ , errRepo := handleHTTPError ( resp , err , "Reading the Repository / Software Component failed" , api . con )
2024-06-25 10:09:31 +02:00
return false , "" , errRepo , false
2023-12-19 15:16:48 +01:00
}
defer resp . Body . Close ( )
var body RepositoryEntity
bodyText , errRead := io . ReadAll ( resp . Body )
if errRead != nil {
2024-06-25 10:09:31 +02:00
return false , "" , err , false
2023-12-19 15:16:48 +01:00
}
if err := json . Unmarshal ( bodyText , & body ) ; err != nil {
2024-06-25 10:09:31 +02:00
return false , "" , err , false
2023-12-19 15:16:48 +01:00
}
if reflect . DeepEqual ( RepositoryEntity { } , body ) {
log . Entry ( ) . WithField ( "StatusCode" , resp . Status ) . WithField ( "repositoryName" , api . repository . Name ) . WithField ( "branchName" , api . repository . Branch ) . WithField ( "commitID" , api . repository . CommitID ) . WithField ( "Tag" , api . repository . Tag ) . Error ( "Could not Clone the Repository / Software Component" )
err := errors . New ( "Request to ABAP System not successful" )
2024-06-25 10:09:31 +02:00
return false , "" , err , false
2023-12-19 15:16:48 +01:00
}
if body . AvailOnInst {
2024-06-25 10:09:31 +02:00
return true , body . ActiveBranch , nil , false
2023-12-19 15:16:48 +01:00
}
2024-06-25 10:09:31 +02:00
if body . ByogUrl != "" {
return false , "" , err , true
}
return false , "" , err , false
2023-12-19 15:16:48 +01:00
}
2024-06-25 10:09:31 +02:00
func ( api * SAP_COM_0948 ) UpdateRepoWithBYOGCredentials ( byogAuthMethod string , byogUsername string , byogPassword string ) {
api . repository . ByogAuthMethod = byogAuthMethod
api . repository . ByogUsername = byogUsername
api . repository . ByogPassword = byogPassword
api . repository . IsByog = true
}
2023-12-19 15:16:48 +01:00
func ( api * SAP_COM_0948 ) Clone ( ) error {
// Trigger the Clone of a Repository
if api . repository . Name == "" {
return errors . New ( "An empty string was passed for the parameter 'repositoryName'" )
}
cloneConnectionDetails := api . con
cloneConnectionDetails . URL = api . con . URL + api . path + api . softwareComponentEntity + api . getRepoNameForPath ( ) + api . cloneAction
2024-06-25 10:09:31 +02:00
body , err := api . repository . GetCloneRequestBody ( )
if err != nil {
return errors . Wrap ( err , "Failed to clone repository" )
}
2023-12-19 15:16:48 +01:00
2024-06-25 10:09:31 +02:00
return api . triggerRequest ( cloneConnectionDetails , [ ] byte ( body ) )
2023-12-19 15:16:48 +01:00
}
2024-10-10 13:42:09 +02:00
func ( api * SAP_COM_0948 ) GetLogArchive ( ) ( result [ ] byte , err error ) {
connectionDetails := api . con
connectionDetails . URL = api . con . URL + api . path + "/LogArchive/" + api . getUUID ( ) + "/download"
resp , err := GetHTTPResponse ( "GET" , connectionDetails , nil , api . client )
if err != nil {
log . SetErrorCategory ( log . ErrorInfrastructure )
_ , err = handleHTTPError ( resp , err , api . failureMessage , connectionDetails )
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
fmt . Println ( "Error: HTTP Status" , resp . StatusCode )
return nil , resp . Request . Context ( ) . Err ( )
}
body , err := io . ReadAll ( resp . Body )
return body , err
}
2023-12-19 15:16:48 +01:00
func ( api * SAP_COM_0948 ) triggerRequest ( cloneConnectionDetails ConnectionDetailsHTTP , jsonBody [ ] byte ) error {
var err error
var body ActionEntity
var resp * http . Response
var errorCode string
for i := 0 ; i <= api . maxRetries ; i ++ {
if i > 0 {
sleepTime , err := api . getSleepTime ( i + 5 )
if err != nil {
// reached max retry duration
break
}
log . Entry ( ) . Infof ( "Retrying in %s" , sleepTime . String ( ) )
time . Sleep ( sleepTime )
}
resp , err = GetHTTPResponse ( "POST" , cloneConnectionDetails , jsonBody , api . client )
if err != nil {
2024-08-16 13:41:23 +02:00
errorCode , err = handleHTTPError ( resp , err , "Triggering the action failed" , api . con )
2023-12-19 15:16:48 +01:00
if slices . Contains ( api . retryAllowedErrorCodes , errorCode ) {
// Error Code allows for retry
continue
} else {
break
}
}
defer resp . Body . Close ( )
log . Entry ( ) . WithField ( "StatusCode" , resp . Status ) . WithField ( "repositoryName" , api . repository . Name ) . WithField ( "branchName" , api . repository . Branch ) . WithField ( "commitID" , api . repository . CommitID ) . WithField ( "Tag" , api . repository . Tag ) . Info ( "Triggered action of Repository / Software Component" )
body , err = api . parseActionResponse ( resp , err )
break
}
api . uuid = body . UUID
return err
}
// initialRequest implements SoftwareComponentApiInterface.
func ( api * SAP_COM_0948 ) initialRequest ( ) error {
// Configuring the HTTP Client and CookieJar
cookieJar , errorCookieJar := cookiejar . New ( nil )
if errorCookieJar != nil {
return errors . Wrap ( errorCookieJar , "Could not create a Cookie Jar" )
}
api . client . SetOptions ( piperhttp . ClientOptions {
MaxRequestDuration : 180 * time . Second ,
CookieJar : cookieJar ,
Username : api . con . User ,
Password : api . con . Password ,
2024-05-10 08:58:46 +02:00
TrustedCerts : api . con . CertificateNames ,
2023-12-19 15:16:48 +01:00
} )
2024-02-02 20:07:31 +01:00
// HEAD request to the root is not sufficient, as an unauthorized called is allowed to do so
// Therefore, the request goes to the "Actions" entity without actually fetching data
2023-12-19 15:16:48 +01:00
headConnection := api . con
headConnection . XCsrfToken = "fetch"
2024-02-02 20:07:31 +01:00
headConnection . URL = api . con . URL + api . path + api . actionsEntity + "?$top=0"
2023-12-19 15:16:48 +01:00
// Loging into the ABAP System - getting the x-csrf-token and cookies
2024-01-18 20:28:24 +01:00
resp , err := GetHTTPResponse ( "GET" , headConnection , nil , api . client )
2023-12-19 15:16:48 +01:00
if err != nil {
2024-08-16 13:41:23 +02:00
_ , err = handleHTTPError ( resp , err , "Authentication on the ABAP system failed" , api . con )
2023-12-19 15:16:48 +01:00
return err
}
defer resp . Body . Close ( )
log . Entry ( ) . WithField ( "StatusCode" , resp . Status ) . WithField ( "ABAP Endpoint" , api . con ) . Debug ( "Authentication on the ABAP system successful" )
api . con . XCsrfToken = resp . Header . Get ( "X-Csrf-Token" )
return nil
}
// getSleepTime Should return the Fibonacci numbers in the define time unit up to the defined maximum duration
func ( api * SAP_COM_0948 ) getSleepTime ( n int ) ( time . Duration , error ) {
if n == 0 {
return 0 , nil
} else if n == 1 {
return 1 * api . retryBaseSleepUnit , nil
} else if n < 0 {
return 0 , errors . New ( "Negative numbers are not allowed" )
}
var result , i int
prev := 0
next := 1
for i = 2 ; i <= n ; i ++ {
result = prev + next
prev = next
next = result
}
sleepTime := time . Duration ( result ) * api . retryBaseSleepUnit
if sleepTime > api . retryMaxSleepTime {
return 0 , errors . New ( "Exceeded max sleep time" )
}
return sleepTime , nil
}
// setSleepTimeConfig sets the time unit (seconds, nanoseconds) and the maximum sleep duration
func ( api * SAP_COM_0948 ) setSleepTimeConfig ( timeUnit time . Duration , maxSleepTime time . Duration ) {
api . retryBaseSleepUnit = timeUnit
api . retryMaxSleepTime = maxSleepTime
}
func ( api * SAP_COM_0948 ) getRepoNameForPath ( ) string {
return "/" + strings . ReplaceAll ( api . repository . Name , "/" , "%2F" )
}
func ( api * SAP_COM_0948 ) getBranchNameForPath ( ) string {
return "/" + api . repository . Branch
}
func ( api * SAP_COM_0948 ) getLogProtocolQuery ( page int ) string {
skip := page * numberOfEntriesPerPage
top := numberOfEntriesPerPage
return fmt . Sprintf ( "?$skip=%s&$top=%s&$count=true" , fmt . Sprint ( skip ) , fmt . Sprint ( top ) )
}
2024-05-08 14:25:13 +02:00
// ConvertTime formats an ISO 8601 timestamp string from format 2024-05-02T09:25:40Z into a UNIX timestamp and returns it
func ( api * SAP_COM_0948 ) ConvertTime ( logTimeStamp string ) time . Time {
t , error := time . Parse ( time . RFC3339 , logTimeStamp )
if error != nil {
return time . Unix ( 0 , 0 ) . UTC ( )
}
return t
}
2024-08-16 13:41:23 +02:00
func handleHTTPError ( resp * http . Response , err error , message string , connectionDetails ConnectionDetailsHTTP ) ( string , error ) {
var errorText string
var errorCode string
var parsingError error
if resp == nil {
// Response is nil in case of a timeout
log . Entry ( ) . WithError ( err ) . WithField ( "ABAP Endpoint" , connectionDetails . URL ) . Error ( "Request failed" )
match , _ := regexp . MatchString ( ".*EOF$" , err . Error ( ) )
if match {
AddDefaultDashedLine ( 1 )
log . Entry ( ) . Infof ( "%s" , "A connection could not be established to the ABAP system. The typical root cause is the network configuration (firewall, IP allowlist, etc.)" )
AddDefaultDashedLine ( 1 )
}
log . Entry ( ) . Infof ( "Error message: %s," , err . Error ( ) )
} else {
defer resp . Body . Close ( )
log . Entry ( ) . WithField ( "StatusCode" , resp . Status ) . WithField ( "User" , connectionDetails . User ) . WithField ( "URL" , connectionDetails . URL ) . Error ( message )
errorText , errorCode , parsingError = getErrorDetailsFromResponse ( resp )
if parsingError != nil {
return "" , err
}
abapError := errors . New ( fmt . Sprintf ( "%s - %s" , errorCode , errorText ) )
err = errors . Wrap ( abapError , err . Error ( ) )
}
return errorCode , err
}
func getErrorDetailsFromResponse ( resp * http . Response ) ( errorString string , errorCode string , err error ) {
// Include the error message of the ABAP Environment system, if available
var abapErrorResponse AbapErrorODataV4
bodyText , readError := io . ReadAll ( resp . Body )
if readError != nil {
return "" , "" , readError
}
var abapResp map [ string ] * json . RawMessage
errUnmarshal := json . Unmarshal ( bodyText , & abapResp )
if errUnmarshal != nil {
return "" , "" , errUnmarshal
}
if _ , ok := abapResp [ "error" ] ; ok {
json . Unmarshal ( * abapResp [ "error" ] , & abapErrorResponse )
if ( AbapErrorODataV4 { } ) != abapErrorResponse {
log . Entry ( ) . WithField ( "ErrorCode" , abapErrorResponse . Code ) . Debug ( abapErrorResponse . Message )
return abapErrorResponse . Message , abapErrorResponse . Code , nil
}
}
return "" , "" , errors . New ( "Could not parse the JSON error response" )
}