github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/cmd/abapEnvironmentRunATCCheck.go (about) 1 package cmd 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "encoding/xml" 7 "io" 8 "net/http" 9 "net/http/cookiejar" 10 "os" 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/piperutils" 21 "github.com/SAP/jenkins-library/pkg/telemetry" 22 "github.com/pkg/errors" 23 ) 24 25 func abapEnvironmentRunATCCheck(options abapEnvironmentRunATCCheckOptions, telemetryData *telemetry.CustomData) { 26 // Mapping for options 27 subOptions := convertATCOptions(&options) 28 29 c := &command.Command{} 30 c.Stdout(log.Entry().Writer()) 31 c.Stderr(log.Entry().Writer()) 32 33 autils := abaputils.AbapUtils{ 34 Exec: c, 35 } 36 var err error 37 38 client := piperhttp.Client{} 39 fileUtils := piperutils.Files{} 40 cookieJar, _ := cookiejar.New(nil) 41 clientOptions := piperhttp.ClientOptions{ 42 CookieJar: cookieJar, 43 } 44 client.SetOptions(clientOptions) 45 46 var details abaputils.ConnectionDetailsHTTP 47 // If Host flag is empty read ABAP endpoint from Service Key instead. Otherwise take ABAP system endpoint from config instead 48 if err == nil { 49 details, err = autils.GetAbapCommunicationArrangementInfo(subOptions, "") 50 } 51 var resp *http.Response 52 // Fetch Xcrsf-Token 53 if err == nil { 54 credentialsOptions := piperhttp.ClientOptions{ 55 Username: details.User, 56 Password: details.Password, 57 CookieJar: cookieJar, 58 } 59 client.SetOptions(credentialsOptions) 60 details.XCsrfToken, err = fetchXcsrfToken("GET", details, nil, &client) 61 } 62 if err == nil { 63 resp, err = triggerATCRun(options, details, &client) 64 } 65 if err == nil { 66 if err = fetchAndPersistATCResults(resp, details, &client, &fileUtils, options.AtcResultsFileName, options.GenerateHTML, options.FailOnSeverity); err != nil { 67 log.Entry().WithError(err).Fatal("step execution failed") 68 } 69 } 70 71 log.Entry().Info("ATC run completed successfully. If there are any results from the respective run they will be listed in the logs above as well as being saved in the output .xml file") 72 } 73 74 func fetchAndPersistATCResults(resp *http.Response, details abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, utils piperutils.FileUtils, atcResultFileName string, generateHTML bool, failOnSeverityLevel string) error { 75 var err error 76 var failStep bool 77 abapEndpoint := details.URL 78 location := resp.Header.Get("Location") 79 details.URL = abapEndpoint + location 80 location, err = pollATCRun(details, nil, client) 81 if err == nil { 82 details.URL = abapEndpoint + location 83 resp, err = getResultATCRun("GET", details, nil, client) 84 } 85 // Parse response 86 var body []byte 87 if err == nil { 88 body, err = io.ReadAll(resp.Body) 89 } 90 if err == nil { 91 defer resp.Body.Close() 92 err, failStep = logAndPersistAndEvaluateATCResults(utils, body, atcResultFileName, generateHTML, failOnSeverityLevel) 93 } 94 if err != nil { 95 return errors.Errorf("Handling ATC result failed: %v", err) 96 } 97 if failStep { 98 return errors.Errorf("Step execution failed due to at least one ATC finding with severity equal to or higher than the failOnSeverity parameter of this step (see config.yml)") 99 } 100 return nil 101 } 102 103 func triggerATCRun(config abapEnvironmentRunATCCheckOptions, details abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) (*http.Response, error) { 104 bodyString, err := buildATCRequestBody(config) 105 if err != nil { 106 return nil, err 107 } 108 var resp *http.Response 109 abapEndpoint := details.URL 110 111 log.Entry().Infof("Request Body: %s", bodyString) 112 body := []byte(bodyString) 113 details.URL = abapEndpoint + "/sap/bc/adt/api/atc/runs?clientWait=false" 114 resp, err = runATC("POST", details, body, client) 115 return resp, err 116 } 117 118 func buildATCRequestBody(config abapEnvironmentRunATCCheckOptions) (bodyString string, err error) { 119 atcConfig, err := resolveATCConfiguration(config) 120 if err != nil { 121 return "", err 122 } 123 124 // Create string for the run parameters 125 variant := "ABAP_CLOUD_DEVELOPMENT_DEFAULT" 126 if atcConfig.CheckVariant != "" { 127 variant = atcConfig.CheckVariant 128 } 129 log.Entry().Infof("ATC Check Variant: %s", variant) 130 runParameters := ` checkVariant="` + variant + `"` 131 if atcConfig.Configuration != "" { 132 runParameters += ` configuration="` + atcConfig.Configuration + `"` 133 } 134 135 var objectSetString string 136 // check if OSL Objectset is present 137 if !reflect.DeepEqual(abaputils.ObjectSet{}, atcConfig.ObjectSet) { 138 objectSetString = abaputils.BuildOSLString(atcConfig.ObjectSet) 139 } 140 // if initial - check if ATC Object set is present 141 if objectSetString == "" && (len(atcConfig.Objects.Package) != 0 || len(atcConfig.Objects.SoftwareComponent) != 0) { 142 objectSetString, err = getATCObjectSet(atcConfig) 143 } 144 145 if objectSetString == "" { 146 return objectSetString, errors.Errorf("Error while parsing ATC test run object set config. No object set has been provided. Please configure the objects you want to be checked for the respective test run") 147 } 148 149 bodyString = `<?xml version="1.0" encoding="UTF-8"?><atc:runparameters xmlns:atc="http://www.sap.com/adt/atc" xmlns:obj="http://www.sap.com/adt/objectset"` + runParameters + `>` + objectSetString + `</atc:runparameters>` 150 return bodyString, err 151 } 152 153 func resolveATCConfiguration(config abapEnvironmentRunATCCheckOptions) (atcConfig ATCConfiguration, err error) { 154 if config.AtcConfig != "" { 155 // Configuration defaults to ATC Config 156 log.Entry().Infof("ATC Configuration: %s", config.AtcConfig) 157 atcConfigFile, err := abaputils.ReadConfigFile(config.AtcConfig) 158 if err != nil { 159 return atcConfig, err 160 } 161 if err := json.Unmarshal(atcConfigFile, &atcConfig); err != nil { 162 log.Entry().WithError(err).Warning("failed to unmarschal json") 163 } 164 return atcConfig, nil 165 166 } else if config.Repositories != "" { 167 // Fallback / EasyMode is the Repositories configuration 168 log.Entry().Infof("ATC Configuration derived from: %s", config.Repositories) 169 repositories, err := abaputils.GetRepositories((&abaputils.RepositoriesConfig{Repositories: config.Repositories}), false) 170 if err != nil { 171 return atcConfig, err 172 } 173 for _, repository := range repositories { 174 atcConfig.Objects.SoftwareComponent = append(atcConfig.Objects.SoftwareComponent, SoftwareComponent{Name: repository.Name}) 175 } 176 return atcConfig, nil 177 } else { 178 // Fail if no configuration is provided 179 return atcConfig, errors.New("No configuration provided - please provide either an ATC configuration file or a repository configuration file") 180 } 181 } 182 183 func getATCObjectSet(ATCConfig ATCConfiguration) (objectSet string, err error) { 184 objectSet += `<obj:objectSet>` 185 186 // Build SC XML body 187 if len(ATCConfig.Objects.SoftwareComponent) != 0 { 188 objectSet += "<obj:softwarecomponents>" 189 for _, s := range ATCConfig.Objects.SoftwareComponent { 190 objectSet += `<obj:softwarecomponent value="` + s.Name + `"/>` 191 } 192 objectSet += "</obj:softwarecomponents>" 193 } 194 195 // Build Package XML body 196 if len(ATCConfig.Objects.Package) != 0 { 197 objectSet += "<obj:packages>" 198 for _, s := range ATCConfig.Objects.Package { 199 objectSet += `<obj:package value="` + s.Name + `" includeSubpackages="` + strconv.FormatBool(s.IncludeSubpackages) + `"/>` 200 } 201 objectSet += "</obj:packages>" 202 } 203 204 objectSet += `</obj:objectSet>` 205 206 return objectSet, nil 207 } 208 209 func logAndPersistAndEvaluateATCResults(utils piperutils.FileUtils, body []byte, atcResultFileName string, generateHTML bool, failOnSeverityLevel string) (error, bool) { 210 var failStep bool 211 if len(body) == 0 { 212 return errors.Errorf("Parsing ATC result failed: %v", errors.New("Body is empty, can't parse empty body")), failStep 213 } 214 215 responseBody := string(body) 216 log.Entry().Debugf("Response body: %s", responseBody) 217 if strings.HasPrefix(responseBody, "<html>") { 218 return errors.New("The Software Component could not be checked. Please make sure the respective Software Component has been cloned successfully on the system"), failStep 219 } 220 221 parsedXML := new(Result) 222 if err := xml.Unmarshal([]byte(body), &parsedXML); err != nil { 223 log.Entry().WithError(err).Warning("failed to unmarschal xml response") 224 } 225 if len(parsedXML.Files) == 0 { 226 log.Entry().Info("There were no results from this run, most likely the checked Software Components are empty or contain no ATC findings") 227 } 228 229 err := os.WriteFile(atcResultFileName, body, 0o644) 230 if err == nil { 231 log.Entry().Infof("Writing %s file was successful", atcResultFileName) 232 var reports []piperutils.Path 233 reports = append(reports, piperutils.Path{Target: atcResultFileName, Name: "ATC Results", Mandatory: true}) 234 for _, s := range parsedXML.Files { 235 for _, t := range s.ATCErrors { 236 log.Entry().Infof("%s in file '%s': %s in line %s found by %s", t.Severity, s.Key, t.Message, t.Line, t.Source) 237 if !failStep { 238 failStep = checkStepFailing(t.Severity, failOnSeverityLevel) 239 } 240 } 241 } 242 if generateHTML { 243 htmlString := generateHTMLDocument(parsedXML) 244 htmlStringByte := []byte(htmlString) 245 atcResultHTMLFileName := strings.Trim(atcResultFileName, ".xml") + ".html" 246 err = os.WriteFile(atcResultHTMLFileName, htmlStringByte, 0o644) 247 if err == nil { 248 log.Entry().Info("Writing " + atcResultHTMLFileName + " file was successful") 249 reports = append(reports, piperutils.Path{Target: atcResultFileName, Name: "ATC Results HTML file", Mandatory: true}) 250 } 251 } 252 piperutils.PersistReportsAndLinks("abapEnvironmentRunATCCheck", "", utils, reports, nil) 253 } 254 if err != nil { 255 return errors.Errorf("Writing results failed: %v", err), failStep 256 } 257 return nil, failStep 258 } 259 func checkStepFailing(severity string, failOnSeverityLevel string) bool { 260 switch failOnSeverityLevel { 261 case "error": 262 switch severity { 263 case "error": 264 return true 265 case "warning": 266 return false 267 case "info": 268 return false 269 default: 270 return false 271 } 272 case "warning": 273 switch severity { 274 case "error": 275 return true 276 case "warning": 277 return true 278 case "info": 279 return false 280 default: 281 return false 282 } 283 case "info": 284 switch severity { 285 case "error": 286 return true 287 case "warning": 288 return true 289 case "info": 290 return true 291 default: 292 return false 293 } 294 default: 295 return false 296 } 297 } 298 299 func runATC(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { 300 log.Entry().WithField("ABAP endpoint: ", details.URL).Info("triggering ATC run") 301 302 header := make(map[string][]string) 303 header["X-Csrf-Token"] = []string{details.XCsrfToken} 304 header["Content-Type"] = []string{"application/vnd.sap.atc.run.parameters.v1+xml; charset=utf-8;"} 305 306 resp, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 307 _ = logResponseBody(resp) 308 if err != nil || (resp != nil && resp.StatusCode == 400) { // send request does not seem to produce error with StatusCode 400!!! 309 err = abaputils.HandleHTTPError(resp, err, "triggering ATC run failed with Status: "+resp.Status, details) 310 log.SetErrorCategory(log.ErrorService) 311 return resp, errors.Errorf("triggering ATC run failed: %v", err) 312 } 313 defer resp.Body.Close() 314 return resp, err 315 } 316 317 func logResponseBody(resp *http.Response) error { 318 var bodyText []byte 319 var readError error 320 if resp != nil { 321 bodyText, readError = io.ReadAll(resp.Body) 322 if readError != nil { 323 return readError 324 } 325 log.Entry().Infof("Response body: %s", bodyText) 326 } 327 return nil 328 } 329 330 func fetchXcsrfToken(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (string, error) { 331 log.Entry().WithField("ABAP Endpoint: ", details.URL).Debug("Fetching Xcrsf-Token") 332 333 details.URL += "/sap/bc/adt/api/atc/runs/00000000000000000000000000000000" 334 details.XCsrfToken = "fetch" 335 header := make(map[string][]string) 336 header["X-Csrf-Token"] = []string{details.XCsrfToken} 337 header["Accept"] = []string{"application/vnd.sap.atc.run.v1+xml"} 338 req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 339 if err != nil { 340 log.SetErrorCategory(log.ErrorInfrastructure) 341 return "", errors.Errorf("Fetching Xcsrf-Token failed: %v", err) 342 } 343 defer req.Body.Close() 344 345 token := req.Header.Get("X-Csrf-Token") 346 return token, err 347 } 348 349 func pollATCRun(details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (string, error) { 350 log.Entry().WithField("ABAP endpoint", details.URL).Info("Polling ATC run status") 351 352 for { 353 resp, err := getHTTPResponseATCRun("GET", details, nil, client) 354 if err != nil { 355 return "", errors.Errorf("Getting HTTP response failed: %v", err) 356 } 357 bodyText, err := io.ReadAll(resp.Body) 358 if err != nil { 359 return "", errors.Errorf("Reading response body failed: %v", err) 360 } 361 362 x := new(Run) 363 if err := xml.Unmarshal(bodyText, &x); err != nil { 364 log.Entry().WithError(err).Warning("failed to unmarschal xml response") 365 } 366 log.Entry().WithField("StatusCode", resp.StatusCode).Info("Status: " + x.Status) 367 368 if x.Status == "Not Created" { 369 return "", err 370 } 371 if x.Status == "Completed" { 372 return x.Link[0].Key, err 373 } 374 if x.Status == "" { 375 return "", errors.Errorf("Could not get any response from ATC poll: %v", errors.New("Status from ATC run is empty. Either it's not an ABAP system or ATC run hasn't started")) 376 } 377 time.Sleep(5 * time.Second) 378 } 379 } 380 381 func getHTTPResponseATCRun(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { 382 header := make(map[string][]string) 383 header["Accept"] = []string{"application/vnd.sap.atc.run.v1+xml"} 384 385 resp, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 386 if err != nil { 387 return resp, errors.Errorf("Getting ATC run status failed: %v", err) 388 } 389 return resp, err 390 } 391 392 func getResultATCRun(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { 393 log.Entry().WithField("ABAP Endpoint: ", details.URL).Info("Getting ATC results") 394 395 header := make(map[string][]string) 396 header["x-csrf-token"] = []string{details.XCsrfToken} 397 header["Accept"] = []string{"application/vnd.sap.atc.checkstyle.v1+xml"} 398 399 resp, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 400 if err != nil { 401 return resp, errors.Errorf("Getting ATC run results failed: %v", err) 402 } 403 return resp, err 404 } 405 406 func convertATCOptions(options *abapEnvironmentRunATCCheckOptions) abaputils.AbapEnvironmentOptions { 407 subOptions := abaputils.AbapEnvironmentOptions{} 408 409 subOptions.CfAPIEndpoint = options.CfAPIEndpoint 410 subOptions.CfServiceInstance = options.CfServiceInstance 411 subOptions.CfServiceKeyName = options.CfServiceKeyName 412 subOptions.CfOrg = options.CfOrg 413 subOptions.CfSpace = options.CfSpace 414 subOptions.Host = options.Host 415 subOptions.Password = options.Password 416 subOptions.Username = options.Username 417 418 return subOptions 419 } 420 421 func generateHTMLDocument(parsedXML *Result) (htmlDocumentString string) { 422 htmlDocumentString = `<!DOCTYPE html><html lang="en" xmlns="http://www.w3.org/1999/xhtml"><head><title>ATC Results</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><style>table,th,td {border: 1px solid black;border-collapse:collapse;}th,td{padding: 5px;text-align:left;font-size:medium;}</style></head><body><h1 style="text-align:left;font-size:large">ATC Results</h1><table style="width:100%"><tr><th>Severity</th><th>File</th><th>Message</th><th>Line</th><th>Checked by</th></tr>` 423 var htmlDocumentStringError, htmlDocumentStringWarning, htmlDocumentStringInfo, htmlDocumentStringDefault string 424 for _, s := range parsedXML.Files { 425 for _, t := range s.ATCErrors { 426 var trBackgroundColor string 427 if t.Severity == "error" { 428 trBackgroundColor = "rgba(227,85,0)" 429 htmlDocumentStringError += `<tr style="background-color: ` + trBackgroundColor + `">` + `<td>` + t.Severity + `</td>` + `<td>` + s.Key + `</td>` + `<td>` + t.Message + `</td>` + `<td style="text-align:center">` + t.Line + `</td>` + `<td>` + t.Source + `</td>` + `</tr>` 430 } 431 if t.Severity == "warning" { 432 trBackgroundColor = "rgba(255,175,0, 0.75)" 433 htmlDocumentStringWarning += `<tr style="background-color: ` + trBackgroundColor + `">` + `<td>` + t.Severity + `</td>` + `<td>` + s.Key + `</td>` + `<td>` + t.Message + `</td>` + `<td style="text-align:center">` + t.Line + `</td>` + `<td>` + t.Source + `</td>` + `</tr>` 434 } 435 if t.Severity == "info" { 436 trBackgroundColor = "rgba(255,175,0, 0.2)" 437 htmlDocumentStringInfo += `<tr style="background-color: ` + trBackgroundColor + `">` + `<td>` + t.Severity + `</td>` + `<td>` + s.Key + `</td>` + `<td>` + t.Message + `</td>` + `<td style="text-align:center">` + t.Line + `</td>` + `<td>` + t.Source + `</td>` + `</tr>` 438 } 439 if t.Severity != "info" && t.Severity != "warning" && t.Severity != "error" { 440 trBackgroundColor = "rgba(255,175,0, 0)" 441 htmlDocumentStringDefault += `<tr style="background-color: ` + trBackgroundColor + `">` + `<td>` + t.Severity + `</td>` + `<td>` + s.Key + `</td>` + `<td>` + t.Message + `</td>` + `<td style="text-align:center">` + t.Line + `</td>` + `<td>` + t.Source + `</td>` + `</tr>` 442 } 443 } 444 } 445 htmlDocumentString += htmlDocumentStringError + htmlDocumentStringWarning + htmlDocumentStringInfo + htmlDocumentStringDefault + `</table></body></html>` 446 447 return htmlDocumentString 448 } 449 450 // ATCConfiguration object for parsing yaml config of software components and packages 451 type ATCConfiguration struct { 452 CheckVariant string `json:"checkvariant,omitempty"` 453 Configuration string `json:"configuration,omitempty"` 454 Objects ATCObjects `json:"atcobjects"` 455 ObjectSet abaputils.ObjectSet `json:"objectset,omitempty"` 456 } 457 458 // ATCObjects in form of packages and software components to be checked 459 type ATCObjects struct { 460 Package []Package `json:"package"` 461 SoftwareComponent []SoftwareComponent `json:"softwarecomponent"` 462 } 463 464 // Package for ATC run to be checked 465 type Package struct { 466 Name string `json:"name"` 467 IncludeSubpackages bool `json:"includesubpackage"` 468 } 469 470 // SoftwareComponent for ATC run to be checked 471 type SoftwareComponent struct { 472 Name string `json:"name"` 473 } 474 475 // Run Object for parsing XML 476 type Run struct { 477 XMLName xml.Name `xml:"run"` 478 Status string `xml:"status,attr"` 479 Link []Link `xml:"link"` 480 } 481 482 // Link of XML object 483 type Link struct { 484 Key string `xml:"href,attr"` 485 Value string `xml:",chardata"` 486 } 487 488 // Result from ATC check for all files that were checked 489 type Result struct { 490 XMLName xml.Name `xml:"checkstyle"` 491 Files []File `xml:"file"` 492 } 493 494 // File that contains ATC check with error for checked file 495 type File struct { 496 Key string `xml:"name,attr"` 497 Value string `xml:",chardata"` 498 ATCErrors []ATCError `xml:"error"` 499 } 500 501 // ATCError with message 502 type ATCError struct { 503 Text string `xml:",chardata"` 504 Message string `xml:"message,attr"` 505 Source string `xml:"source,attr"` 506 Line string `xml:"line,attr"` 507 Severity string `xml:"severity,attr"` 508 }