1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-14 11:03:09 +02:00
sap-jenkins-library/pkg/abaputils/sap_com_0948.go
2024-02-02 19:07:31 +00:00

379 lines
12 KiB
Go

package abaputils
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"reflect"
"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
}
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)
_, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails)
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)
_, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails)
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)
_, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails)
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
}
func (api *SAP_COM_0948) GetRepository() (bool, string, error) {
if api.repository.Name == "" {
return false, "", errors.New("An empty string was passed for the parameter 'repositoryName'")
}
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 {
_, errRepo := HandleHTTPError(resp, err, "Reading the Repository / Software Component failed", api.con)
return false, "", errRepo
}
defer resp.Body.Close()
var body RepositoryEntity
bodyText, errRead := io.ReadAll(resp.Body)
if errRead != nil {
return false, "", err
}
if err := json.Unmarshal(bodyText, &body); err != nil {
return false, "", err
}
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")
return false, "", err
}
if body.AvailOnInst {
return true, body.ActiveBranch, nil
}
return false, "", err
}
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
body := []byte(api.repository.GetCloneRequestBody())
return api.triggerRequest(cloneConnectionDetails, body)
}
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 {
errorCode, err = HandleHTTPError(resp, err, "Triggering the action failed", api.con)
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,
})
// 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
headConnection := api.con
headConnection.XCsrfToken = "fetch"
headConnection.URL = api.con.URL + api.path + api.actionsEntity + "?$top=0"
// Loging into the ABAP System - getting the x-csrf-token and cookies
resp, err := GetHTTPResponse("GET", headConnection, nil, api.client)
if err != nil {
_, err = HandleHTTPError(resp, err, "Authentication on the ABAP system failed", api.con)
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))
}