github.com/SAP/jenkins-library@v1.362.0/cmd/abapEnvironmentPushATCSystemConfig.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/http/cookiejar"
    10  	"os"
    11  	"path/filepath"
    12  	"reflect"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/SAP/jenkins-library/pkg/abaputils"
    18  	"github.com/SAP/jenkins-library/pkg/command"
    19  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    20  	"github.com/SAP/jenkins-library/pkg/log"
    21  	"github.com/SAP/jenkins-library/pkg/telemetry"
    22  	"github.com/pkg/errors"
    23  )
    24  
    25  func abapEnvironmentPushATCSystemConfig(config abapEnvironmentPushATCSystemConfigOptions, telemetryData *telemetry.CustomData) {
    26  	// for command execution use Command
    27  	c := command.Command{}
    28  	// reroute command output to logging framework
    29  	c.Stdout(log.Writer())
    30  	c.Stderr(log.Writer())
    31  
    32  	var autils = abaputils.AbapUtils{
    33  		Exec: &c,
    34  	}
    35  
    36  	client := piperhttp.Client{}
    37  
    38  	err := runAbapEnvironmentPushATCSystemConfig(&config, telemetryData, &autils, &client)
    39  	if err != nil {
    40  		log.Entry().WithError(err).Fatal("step execution failed")
    41  	}
    42  }
    43  
    44  func runAbapEnvironmentPushATCSystemConfig(config *abapEnvironmentPushATCSystemConfigOptions, telemetryData *telemetry.CustomData, autils abaputils.Communication, client piperhttp.Sender) error {
    45  
    46  	subOptions := convertATCSysOptions(config)
    47  
    48  	// Determine the host, user and password, either via the input parameters or via a cloud foundry service key.
    49  	connectionDetails, err := autils.GetAbapCommunicationArrangementInfo(subOptions, "/sap/opu/odata4/sap/satc_ci_cf_api/srvd_a2x/sap/satc_ci_cf_sv_api/0001")
    50  	if err != nil {
    51  		return errors.Errorf("Parameters for the ABAP Connection not available: %v", err)
    52  	}
    53  
    54  	cookieJar, err := cookiejar.New(nil)
    55  	if err != nil {
    56  		return errors.Errorf("could not create a Cookie Jar: %v", err)
    57  	}
    58  	clientOptions := piperhttp.ClientOptions{
    59  		MaxRequestDuration: 180 * time.Second,
    60  		CookieJar:          cookieJar,
    61  		Username:           connectionDetails.User,
    62  		Password:           connectionDetails.Password,
    63  	}
    64  	client.SetOptions(clientOptions)
    65  
    66  	if connectionDetails.XCsrfToken, err = fetchXcsrfTokenFromHead(connectionDetails, client); err != nil {
    67  		return err
    68  	}
    69  
    70  	return pushATCSystemConfig(config, connectionDetails, client)
    71  
    72  }
    73  
    74  func pushATCSystemConfig(config *abapEnvironmentPushATCSystemConfigOptions, connectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) error {
    75  
    76  	//check, if given ATC System configuration File
    77  	parsedConfigurationJson, atcSystemConfiguartionJsonFile, err := checkATCSystemConfigurationFile(config)
    78  	if err != nil {
    79  		return err
    80  	}
    81  	//check, if ATC configuration with given name already exists in Backend
    82  	configDoesExist, configName, configUUID, configLastChangedBackend, err := checkConfigExistsInBackend(config, atcSystemConfiguartionJsonFile, connectionDetails, client)
    83  	if err != nil {
    84  		return err
    85  	}
    86  	if !configDoesExist {
    87  		//regular push of configuration
    88  		configUUID = ""
    89  		return handlePushConfiguration(config, configUUID, configDoesExist, atcSystemConfiguartionJsonFile, connectionDetails, client)
    90  	}
    91  	if !parsedConfigurationJson.LastChangedAt.IsZero() {
    92  		if configLastChangedBackend.Before(parsedConfigurationJson.LastChangedAt) && !config.PatchIfExisting {
    93  			//config exists, is not recent but must NOT be patched
    94  			log.Entry().Info("pushing ATC System Configuration skipped. Reason: ATC System Configuration with name " + configName + " exists and is outdated (Backend: " + configLastChangedBackend.Local().String() + " vs. File: " + parsedConfigurationJson.LastChangedAt.Local().String() + ") but should not be overwritten (check step configuration parameter).")
    95  			return nil
    96  		}
    97  		if configLastChangedBackend.After(parsedConfigurationJson.LastChangedAt) || configLastChangedBackend == parsedConfigurationJson.LastChangedAt {
    98  			//configuration exists and is most recent
    99  			log.Entry().Info("pushing ATC System Configuration skipped. Reason: ATC System Configuration with name " + configName + " exists and is most recent (Backend: " + configLastChangedBackend.Local().String() + " vs. File: " + parsedConfigurationJson.LastChangedAt.Local().String() + "). Therefore no update needed.")
   100  			return nil
   101  		}
   102  	}
   103  	if configLastChangedBackend.Before(parsedConfigurationJson.LastChangedAt) || parsedConfigurationJson.LastChangedAt.IsZero() {
   104  		if config.PatchIfExisting {
   105  			//configuration exists and is older than current config (or does not provide information about lastChanged) and should be patched
   106  			return handlePushConfiguration(config, configUUID, configDoesExist, atcSystemConfiguartionJsonFile, connectionDetails, client)
   107  		} else {
   108  			//config exists, is not recent but must NOT be patched
   109  			log.Entry().Info("pushing ATC System Configuration skipped. Reason: ATC System Configuration with name " + configName + " exists but should not be overwritten (check step configuration parameter).")
   110  			return nil
   111  		}
   112  	}
   113  
   114  	return nil
   115  }
   116  
   117  func checkATCSystemConfigurationFile(config *abapEnvironmentPushATCSystemConfigOptions) (parsedConfigJsonWithExpand, []byte, error) {
   118  	var parsedConfigurationJson parsedConfigJsonWithExpand
   119  	var emptyConfigurationJson parsedConfigJsonWithExpand
   120  	var atcSystemConfiguartionJsonFile []byte
   121  
   122  	parsedConfigurationJson, atcSystemConfiguartionJsonFile, err := readATCSystemConfigurationFile(config)
   123  	if err != nil {
   124  		return parsedConfigurationJson, atcSystemConfiguartionJsonFile, err
   125  	}
   126  
   127  	//check if parsedConfigurationJson is not initial or Configuration Name not supplied
   128  	if reflect.DeepEqual(parsedConfigurationJson, emptyConfigurationJson) ||
   129  		parsedConfigurationJson.ConfName == "" {
   130  		return parsedConfigurationJson, atcSystemConfiguartionJsonFile, errors.Errorf("pushing ATC System Configuration failed. Reason: Configured File does not contain required ATC System Configuration attributes (File: " + config.AtcSystemConfigFilePath + ")")
   131  	}
   132  
   133  	return parsedConfigurationJson, atcSystemConfiguartionJsonFile, nil
   134  }
   135  
   136  func readATCSystemConfigurationFile(config *abapEnvironmentPushATCSystemConfigOptions) (parsedConfigJsonWithExpand, []byte, error) {
   137  	var parsedConfigurationJson parsedConfigJsonWithExpand
   138  	var emptyConfigurationJson parsedConfigJsonWithExpand
   139  	var atcSystemConfiguartionJsonFile []byte
   140  	var filename string
   141  
   142  	filelocation, err := filepath.Glob(config.AtcSystemConfigFilePath)
   143  	if err != nil {
   144  		return parsedConfigurationJson, atcSystemConfiguartionJsonFile, err
   145  	}
   146  
   147  	if len(filelocation) == 0 {
   148  		return parsedConfigurationJson, atcSystemConfiguartionJsonFile, errors.Errorf("pushing ATC System Configuration failed. Reason: Configured Filelocation is empty (File: " + config.AtcSystemConfigFilePath + ")")
   149  	}
   150  
   151  	filename, err = filepath.Abs(filelocation[0])
   152  	if err != nil {
   153  		return parsedConfigurationJson, atcSystemConfiguartionJsonFile, err
   154  	}
   155  	atcSystemConfiguartionJsonFile, err = os.ReadFile(filename)
   156  	if err != nil {
   157  		return parsedConfigurationJson, atcSystemConfiguartionJsonFile, err
   158  	}
   159  	if len(atcSystemConfiguartionJsonFile) == 0 {
   160  		return parsedConfigurationJson, atcSystemConfiguartionJsonFile, errors.Errorf("pushing ATC System Configuration failed. Reason: Configured File is empty (File: " + config.AtcSystemConfigFilePath + ")")
   161  	}
   162  
   163  	err = json.Unmarshal(atcSystemConfiguartionJsonFile, &parsedConfigurationJson)
   164  	if err != nil {
   165  		return emptyConfigurationJson, atcSystemConfiguartionJsonFile, errors.Errorf("pushing ATC System Configuration failed. Unmarshal Error of ATC Configuration File ("+config.AtcSystemConfigFilePath+"): %v", err)
   166  	}
   167  
   168  	return parsedConfigurationJson, atcSystemConfiguartionJsonFile, err
   169  }
   170  
   171  func handlePushConfiguration(config *abapEnvironmentPushATCSystemConfigOptions, confUUID string, configDoesExist bool, atcSystemConfiguartionJsonFile []byte, connectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) error {
   172  	var err error
   173  	if configDoesExist {
   174  		err = doPatchATCSystemConfig(config, confUUID, atcSystemConfiguartionJsonFile, connectionDetails, client)
   175  		if err != nil {
   176  			return err
   177  		}
   178  		log.Entry().Info("ATC System Configuration successfully pushed from file " + config.AtcSystemConfigFilePath + " and patched in system")
   179  	}
   180  	if !configDoesExist {
   181  		err = doPushATCSystemConfig(config, atcSystemConfiguartionJsonFile, connectionDetails, client)
   182  		if err != nil {
   183  			return err
   184  		}
   185  		log.Entry().Info("ATC System Configuration successfully pushed from file " + config.AtcSystemConfigFilePath + " and created in system")
   186  	}
   187  
   188  	return nil
   189  
   190  }
   191  
   192  func fetchXcsrfTokenFromHead(connectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) (string, error) {
   193  
   194  	log.Entry().WithField("ABAP Endpoint: ", connectionDetails.URL).Debug("Fetching Xcrsf-Token")
   195  	uriConnectionDetails := connectionDetails
   196  	uriConnectionDetails.URL = ""
   197  	connectionDetails.XCsrfToken = "fetch"
   198  
   199  	// Loging into the ABAP System - getting the x-csrf-token and cookies
   200  	resp, err := abaputils.GetHTTPResponse("HEAD", connectionDetails, nil, client)
   201  	if err != nil {
   202  		_, err = abaputils.HandleHTTPError(resp, err, "authentication on the ABAP system failed", connectionDetails)
   203  		return connectionDetails.XCsrfToken, errors.Errorf("X-Csrf-Token fetch failed for Service ATC System Configuration: %v", err)
   204  	}
   205  	defer resp.Body.Close()
   206  
   207  	log.Entry().WithField("StatusCode", resp.Status).WithField("ABAP Endpoint", connectionDetails.URL).Debug("Authentication on the ABAP system successful")
   208  	uriConnectionDetails.XCsrfToken = resp.Header.Get("X-Csrf-Token")
   209  	connectionDetails.XCsrfToken = uriConnectionDetails.XCsrfToken
   210  
   211  	return connectionDetails.XCsrfToken, err
   212  }
   213  
   214  func doPatchATCSystemConfig(config *abapEnvironmentPushATCSystemConfigOptions, confUUID string, atcSystemConfiguartionJsonFile []byte, connectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) error {
   215  
   216  	batchATCSystemConfigFile, err := buildATCSystemConfigBatchRequest(confUUID, atcSystemConfiguartionJsonFile)
   217  	if err != nil {
   218  		return err
   219  	}
   220  	return doBatchATCSystemConfig(config, batchATCSystemConfigFile, connectionDetails, client)
   221  
   222  }
   223  
   224  func buildATCSystemConfigBatchRequest(confUUID string, atcSystemConfiguartionJsonFile []byte) (string, error) {
   225  
   226  	var batchRequestString string
   227  
   228  	//splitting json into configuration base and configuration properties & build a batch request for oData - patch config & patch priorities
   229  	//first remove expansion to priorities to get only "base" Configuration
   230  	configBaseJsonBody, err := buildParsedATCSystemConfigBaseJsonBody(confUUID, bytes.NewBuffer(atcSystemConfiguartionJsonFile).String())
   231  	if err != nil {
   232  		return batchRequestString, err
   233  	}
   234  
   235  	var parsedConfigPriorities parsedConfigPriorities
   236  	err = json.Unmarshal(atcSystemConfiguartionJsonFile, &parsedConfigPriorities)
   237  	if err != nil {
   238  		return batchRequestString, err
   239  	}
   240  
   241  	//build the Batch request string
   242  	contentID := 1
   243  
   244  	batchRequestString = addBeginOfBatch(batchRequestString)
   245  	//now adding opening Changeset as at least config base is to be patched
   246  	batchRequestString = addChangesetBegin(batchRequestString, contentID)
   247  
   248  	if err != nil {
   249  		return batchRequestString, err
   250  	}
   251  	batchRequestString = addPatchConfigBaseChangeset(batchRequestString, confUUID, configBaseJsonBody)
   252  
   253  	if len(parsedConfigPriorities.Priorities) > 0 {
   254  		// in case Priorities need patches too
   255  		var priority priorityJson
   256  		for i, priorityLine := range parsedConfigPriorities.Priorities {
   257  
   258  			//for each line, add content id
   259  			contentID += 1
   260  			priority.Priority = priorityLine.Priority
   261  			priorityJsonBody, err := json.Marshal(&priority)
   262  			if err != nil {
   263  				log.Entry().Errorf("problem with marshall of single priority in line "+strconv.Itoa(i), err)
   264  				continue
   265  			}
   266  			batchRequestString = addChangesetBegin(batchRequestString, contentID)
   267  
   268  			//now PATCH command for priority
   269  			batchRequestString = addPatchSinglePriorityChangeset(batchRequestString, confUUID, priorityLine.Test, priorityLine.MessageId, string(priorityJsonBody))
   270  
   271  		}
   272  	}
   273  
   274  	//at the end, add closing inner and outer boundary tags
   275  	batchRequestString = addEndOfBatch(batchRequestString)
   276  
   277  	log.Entry().Info("Batch Request String: " + batchRequestString)
   278  
   279  	return batchRequestString, nil
   280  
   281  }
   282  
   283  func buildParsedATCSystemConfigBaseJsonBody(confUUID string, atcSystemConfiguartionJsonFile string) (string, error) {
   284  
   285  	var i interface{}
   286  	var outputString string = ``
   287  
   288  	if err := json.Unmarshal([]byte(atcSystemConfiguartionJsonFile), &i); err != nil {
   289  		return outputString, errors.Errorf("problem with unmarshall input "+atcSystemConfiguartionJsonFile+": %v", err)
   290  	}
   291  	if m, ok := i.(map[string]interface{}); ok {
   292  		delete(m, "_priorities")
   293  	}
   294  
   295  	if output, err := json.Marshal(i); err != nil {
   296  		return outputString, errors.Errorf("problem with marshall output "+atcSystemConfiguartionJsonFile+": %v", err)
   297  	} else {
   298  		output = output[1:] // remove leading '{'
   299  		outputString = string(output)
   300  		//injecting the configuration ID
   301  		confIDString := `{"conf_id":"` + confUUID + `",`
   302  		outputString = confIDString + outputString
   303  
   304  		return outputString, err
   305  	}
   306  
   307  }
   308  
   309  func addPatchConfigBaseChangeset(inputString string, confUUID string, configBaseJsonBody string) string {
   310  
   311  	entityIdString := `(root_id='1',conf_id=` + confUUID + `)`
   312  	newString := addCommandEntityChangeset("PATCH", "configuration", entityIdString, inputString, configBaseJsonBody)
   313  
   314  	return newString
   315  }
   316  
   317  func addPatchSinglePriorityChangeset(inputString string, confUUID string, test string, messageId string, priorityJsonBody string) string {
   318  
   319  	entityIdString := `(root_id='1',conf_id=` + confUUID + `,test='` + test + `',message_id='` + messageId + `')`
   320  	newString := addCommandEntityChangeset("PATCH", "priority", entityIdString, inputString, priorityJsonBody)
   321  
   322  	return newString
   323  }
   324  
   325  func addChangesetBegin(inputString string, contentID int) string {
   326  
   327  	newString := inputString + `
   328  --changeset
   329  Content-Type: application/http
   330  Content-Transfer-Encoding: binary
   331  Content-ID: ` + strconv.Itoa(contentID) + `
   332  `
   333  	return newString
   334  }
   335  
   336  func addBeginOfBatch(inputString string) string {
   337  	//Starting always with outer boundary - followed by mandatory Contenttype and boundary for changeset
   338  	newString := inputString + `
   339  --request-separator
   340  Content-Type: multipart/mixed;boundary=changeset
   341  `
   342  	return newString
   343  }
   344  
   345  func addEndOfBatch(inputString string) string {
   346  	//Starting always with outer boundary - followed by mandatory Contenttype and boundary for changeset
   347  	newString := inputString + `
   348  --changeset--
   349  
   350  --request-separator--`
   351  
   352  	return newString
   353  }
   354  
   355  func addCommandEntityChangeset(command string, entity string, entityIdString string, inputString string, jsonBody string) string {
   356  
   357  	newString := inputString + `
   358  ` + command + ` ` + entity + entityIdString + ` HTTP/1.1
   359  Content-Type: application/json
   360  
   361  `
   362  	if len(jsonBody) > 0 {
   363  		newString += jsonBody + `
   364  `
   365  	}
   366  
   367  	return newString
   368  
   369  }
   370  
   371  func doPushATCSystemConfig(config *abapEnvironmentPushATCSystemConfigOptions, atcSystemConfiguartionJsonFile []byte, connectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) error {
   372  	abapEndpoint := connectionDetails.URL
   373  	connectionDetails.URL = abapEndpoint + "/configuration"
   374  
   375  	resp, err := abaputils.GetHTTPResponse("POST", connectionDetails, atcSystemConfiguartionJsonFile, client)
   376  	return HandleHttpResponse(resp, err, "Post Request for Creating ATC System Configuration", connectionDetails)
   377  }
   378  
   379  func doBatchATCSystemConfig(config *abapEnvironmentPushATCSystemConfigOptions, batchRequestBodyFile string, connectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) error {
   380  	abapEndpoint := connectionDetails.URL
   381  	connectionDetails.URL = abapEndpoint + "/$batch"
   382  
   383  	header := make(map[string][]string)
   384  	header["X-Csrf-Token"] = []string{connectionDetails.XCsrfToken}
   385  	header["Content-Type"] = []string{"multipart/mixed;boundary=request-separator"}
   386  
   387  	batchRequestBodyFileByte := []byte(batchRequestBodyFile)
   388  	resp, err := client.SendRequest("POST", connectionDetails.URL, bytes.NewBuffer(batchRequestBodyFileByte), header, nil)
   389  	return HandleHttpResponse(resp, err, "Batch Request for Patching ATC System Configuration", connectionDetails)
   390  }
   391  
   392  func checkConfigExistsInBackend(config *abapEnvironmentPushATCSystemConfigOptions, atcSystemConfiguartionJsonFile []byte, connectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) (bool, string, string, time.Time, error) {
   393  	var configName string
   394  	var configUUID string
   395  	var configLastChangedAt time.Time
   396  
   397  	//extract Configuration Name from atcSystemConfiguartionJsonFile
   398  	var parsedConfigurationJson parsedConfigJsonWithExpand
   399  	err := json.Unmarshal(atcSystemConfiguartionJsonFile, &parsedConfigurationJson)
   400  	if err != nil {
   401  		return false, configName, configUUID, configLastChangedAt, err
   402  	}
   403  
   404  	//call a get on config with filter on given name
   405  	configName = parsedConfigurationJson.ConfName
   406  	abapEndpoint := connectionDetails.URL
   407  	connectionDetails.URL = abapEndpoint + "/configuration" + "?$filter=conf_name%20eq%20" + "'" + configName + "'"
   408  	if err != nil {
   409  		return false, configName, configUUID, configLastChangedAt, err
   410  	}
   411  	resp, err := abaputils.GetHTTPResponse("GET", connectionDetails, nil, client)
   412  	if err != nil {
   413  		return false, configName, configUUID, configLastChangedAt, err
   414  	}
   415  	var body []byte
   416  	body, err = io.ReadAll(resp.Body)
   417  	if err != nil {
   418  		return false, configName, configUUID, configLastChangedAt, err
   419  	}
   420  
   421  	var parsedoDataResponse parsedOdataResp
   422  	if err = json.Unmarshal(body, &parsedoDataResponse); err != nil {
   423  		return false, configName, configUUID, configLastChangedAt, errors.New("GET Request for check existence of ATC System Configuration - Unexpected Response - Problem with Unmarshal body: " + string(body))
   424  	}
   425  	if len(parsedoDataResponse.Value) > 0 {
   426  		configUUID = parsedoDataResponse.Value[0].ConfUUID
   427  		configLastChangedAt = parsedoDataResponse.Value[0].LastChangedAt
   428  		log.Entry().Info("ATC System Configuration " + configName + " does exist and last changed at " + configLastChangedAt.Local().String())
   429  		return true, configName, configUUID, configLastChangedAt, nil
   430  	} else {
   431  		//response value is empty, so NOT found entity with this name!
   432  		log.Entry().Info("ATC System Configuration " + configName + " does not exist!")
   433  		return false, configName, "", configLastChangedAt, nil
   434  	}
   435  }
   436  
   437  func HandleHttpResponse(resp *http.Response, err error, message string, connectionDetails abaputils.ConnectionDetailsHTTP) error {
   438  
   439  	var bodyText []byte
   440  	var readError error
   441  
   442  	if resp == nil {
   443  		// Response is nil in case of a timeout
   444  		log.Entry().WithError(err).WithField("ABAP Endpoint", connectionDetails.URL).Error("Request failed")
   445  	} else {
   446  		log.Entry().WithField("StatusCode", resp.Status).Info(message)
   447  		bodyText, readError = io.ReadAll(resp.Body)
   448  		if readError != nil {
   449  			defer resp.Body.Close()
   450  			return readError
   451  		}
   452  		log.Entry().Infof("Response body: %s", bodyText)
   453  
   454  		errorDetails, parsingError := getErrorDetailsFromBody(resp, bodyText)
   455  		if parsingError == nil &&
   456  			errorDetails != "" {
   457  			err = errors.New(errorDetails)
   458  		}
   459  	}
   460  	defer resp.Body.Close()
   461  	return err
   462  
   463  }
   464  
   465  func getErrorDetailsFromBody(resp *http.Response, bodyText []byte) (errorString string, err error) {
   466  
   467  	// Include the error message of the ABAP Environment system, if available
   468  	var abapErrorResponse AbapError
   469  	var abapResp map[string]*json.RawMessage
   470  
   471  	//errors could also be reported inside an e.g. BATCH request wich returned with status code 200!!!
   472  	contentType := resp.Header.Get("Content-type")
   473  	if len(bodyText) != 0 &&
   474  		strings.Contains(contentType, "multipart/mixed") {
   475  		//scan for inner errors! (by now count as error only RespCode starting with 4 or 5)
   476  		if strings.Contains(string(bodyText), "HTTP/1.1 4") ||
   477  			strings.Contains(string(bodyText), "HTTP/1.1 5") {
   478  			errorString = fmt.Sprintf("Outer Response Code: %v - but at least one Inner response returned StatusCode 4* or 5*. Please check Log for details.", resp.StatusCode)
   479  		} else {
   480  			log.Entry().Info("no Inner Response Errors")
   481  		}
   482  		if errorString != "" {
   483  			return errorString, nil
   484  		}
   485  	}
   486  	if len(bodyText) != 0 &&
   487  		strings.Contains(contentType, "application/json") {
   488  		errUnmarshal := json.Unmarshal(bodyText, &abapResp)
   489  		if errUnmarshal != nil {
   490  			return errorString, errUnmarshal
   491  		}
   492  		if _, ok := abapResp["error"]; ok {
   493  			if err := json.Unmarshal(*abapResp["error"], &abapErrorResponse); err != nil {
   494  				return errorString, err
   495  			}
   496  			if (AbapError{}) != abapErrorResponse {
   497  				log.Entry().WithField("ErrorCode", abapErrorResponse.Code).Error(abapErrorResponse.Message.Value)
   498  				errorString = fmt.Sprintf("%s - %s", abapErrorResponse.Code, abapErrorResponse.Message.Value)
   499  				return errorString, nil
   500  			}
   501  		}
   502  	}
   503  
   504  	return errorString, errors.New("Could not parse the JSON error response. stringified body " + string(bodyText))
   505  
   506  }
   507  
   508  func convertATCSysOptions(options *abapEnvironmentPushATCSystemConfigOptions) abaputils.AbapEnvironmentOptions {
   509  	subOptions := abaputils.AbapEnvironmentOptions{}
   510  
   511  	subOptions.CfAPIEndpoint = options.CfAPIEndpoint
   512  	subOptions.CfServiceInstance = options.CfServiceInstance
   513  	subOptions.CfServiceKeyName = options.CfServiceKeyName
   514  	subOptions.CfOrg = options.CfOrg
   515  	subOptions.CfSpace = options.CfSpace
   516  	subOptions.Host = options.Host
   517  	subOptions.Password = options.Password
   518  	subOptions.Username = options.Username
   519  
   520  	return subOptions
   521  }
   522  
   523  type parsedOdataResp struct {
   524  	Value []parsedConfigJsonWithExpand `json:"value"`
   525  }
   526  
   527  type parsedConfigJsonWithExpand struct {
   528  	ConfName      string                 `json:"conf_name"`
   529  	ConfUUID      string                 `json:"conf_id"`
   530  	LastChangedAt time.Time              `json:"last_changed_at"`
   531  	Priorities    []parsedConfigPriority `json:"_priorities"`
   532  }
   533  
   534  type parsedConfigPriorities struct {
   535  	Priorities []parsedConfigPriority `json:"_priorities"`
   536  }
   537  
   538  type parsedConfigPriority struct {
   539  	Test      string      `json:"test"`
   540  	MessageId string      `json:"message_id"`
   541  	Priority  json.Number `json:"priority"`
   542  }
   543  
   544  type priorityJson struct {
   545  	Priority json.Number `json:"priority"`
   546  }
   547  
   548  // AbapError contains the error code and the error message for ABAP errors
   549  type AbapError struct {
   550  	Code    string           `json:"code"`
   551  	Message AbapErrorMessage `json:"message"`
   552  }
   553  
   554  // AbapErrorMessage contains the lanuage and value fields for ABAP errors
   555  type AbapErrorMessage struct {
   556  	Lang  string `json:"lang"`
   557  	Value string `json:"value"`
   558  }