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