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 }