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