github.com/jaylevin/jenkins-library@v1.230.4/cmd/abapEnvironmentRunATCCheck.go (about) 1 package cmd 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "encoding/xml" 7 "fmt" 8 "io/ioutil" 9 "net/http" 10 "net/http/cookiejar" 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 27 // Mapping for options 28 subOptions := convertATCOptions(&options) 29 30 c := &command.Command{} 31 c.Stdout(log.Entry().Writer()) 32 c.Stderr(log.Entry().Writer()) 33 34 var autils = abaputils.AbapUtils{ 35 Exec: c, 36 } 37 var err error 38 39 client := piperhttp.Client{} 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 err = fetchAndPersistATCResults(resp, details, &client, options.AtcResultsFileName, options.GenerateHTML) 67 } 68 if err != nil { 69 log.Entry().WithError(err).Fatal("step execution failed") 70 } 71 72 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") 73 } 74 75 func fetchAndPersistATCResults(resp *http.Response, details abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, atcResultFileName string, generateHTML bool) error { 76 var err error 77 var abapEndpoint string 78 abapEndpoint = details.URL 79 location := resp.Header.Get("Location") 80 details.URL = abapEndpoint + location 81 location, err = pollATCRun(details, nil, client) 82 if err == nil { 83 details.URL = abapEndpoint + location 84 resp, err = getResultATCRun("GET", details, nil, client) 85 } 86 //Parse response 87 var body []byte 88 if err == nil { 89 body, err = ioutil.ReadAll(resp.Body) 90 } 91 if err == nil { 92 defer resp.Body.Close() 93 err = logAndPersistATCResult(body, atcResultFileName, generateHTML) 94 } 95 if err != nil { 96 return fmt.Errorf("Handling ATC result failed: %w", err) 97 } 98 return nil 99 } 100 101 func triggerATCRun(config abapEnvironmentRunATCCheckOptions, details abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) (*http.Response, error) { 102 103 bodyString, err := buildATCRequestBody(config) 104 if err != nil { 105 return nil, err 106 } 107 var resp *http.Response 108 abapEndpoint := details.URL 109 110 log.Entry().Infof("Request Body: %s", bodyString) 111 var body = []byte(bodyString) 112 details.URL = abapEndpoint + "/sap/bc/adt/api/atc/runs?clientWait=false" 113 resp, err = runATC("POST", details, body, client) 114 return resp, err 115 } 116 117 func buildATCRequestBody(config abapEnvironmentRunATCCheckOptions) (bodyString string, err error) { 118 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, fmt.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 155 if config.AtcConfig != "" { 156 // Configuration defaults to ATC Config 157 log.Entry().Infof("ATC Configuration: %s", config.AtcConfig) 158 atcConfigFile, err := abaputils.ReadConfigFile(config.AtcConfig) 159 if err != nil { 160 return atcConfig, err 161 } 162 json.Unmarshal(atcConfigFile, &atcConfig) 163 return atcConfig, err 164 165 } else if config.Repositories != "" { 166 // Fallback / EasyMode is the Repositories configuration 167 log.Entry().Infof("ATC Configuration derived from: %s", config.Repositories) 168 repositories, err := abaputils.GetRepositories((&abaputils.RepositoriesConfig{Repositories: config.Repositories})) 169 if err != nil { 170 return atcConfig, err 171 } 172 for _, repository := range repositories { 173 atcConfig.Objects.SoftwareComponent = append(atcConfig.Objects.SoftwareComponent, SoftwareComponent{Name: repository.Name}) 174 } 175 return atcConfig, nil 176 } else { 177 // Fail if no configuration is provided 178 return atcConfig, errors.New("No configuration provided - please provide either an ATC configuration file or a repository configuration file") 179 } 180 } 181 182 func getATCObjectSet(ATCConfig ATCConfiguration) (objectSet string, err error) { 183 objectSet += `<obj:objectSet>` 184 185 //Build SC XML body 186 if len(ATCConfig.Objects.SoftwareComponent) != 0 { 187 objectSet += "<obj:softwarecomponents>" 188 for _, s := range ATCConfig.Objects.SoftwareComponent { 189 objectSet += `<obj:softwarecomponent value="` + s.Name + `"/>` 190 } 191 objectSet += "</obj:softwarecomponents>" 192 } 193 194 //Build Package XML body 195 if len(ATCConfig.Objects.Package) != 0 { 196 objectSet += "<obj:packages>" 197 for _, s := range ATCConfig.Objects.Package { 198 objectSet += `<obj:package value="` + s.Name + `" includeSubpackages="` + strconv.FormatBool(s.IncludeSubpackages) + `"/>` 199 } 200 objectSet += "</obj:packages>" 201 } 202 203 objectSet += `</obj:objectSet>` 204 205 return objectSet, nil 206 } 207 208 func logAndPersistATCResult(body []byte, atcResultFileName string, generateHTML bool) (err error) { 209 if len(body) == 0 { 210 return fmt.Errorf("Parsing ATC result failed: %w", errors.New("Body is empty, can't parse empty body")) 211 } 212 213 responseBody := string(body) 214 log.Entry().Debugf("Response body: %s", responseBody) 215 if strings.HasPrefix(responseBody, "<html>") { 216 return errors.New("The Software Component could not be checked. Please make sure the respective Software Component has been cloned successfully on the system") 217 } 218 219 parsedXML := new(Result) 220 xml.Unmarshal([]byte(body), &parsedXML) 221 if len(parsedXML.Files) == 0 { 222 log.Entry().Info("There were no results from this run, most likely the checked Software Components are empty or contain no ATC findings") 223 } 224 225 err = ioutil.WriteFile(atcResultFileName, body, 0644) 226 if err == nil { 227 log.Entry().Infof("Writing %s file was successful", atcResultFileName) 228 var reports []piperutils.Path 229 reports = append(reports, piperutils.Path{Target: atcResultFileName, Name: "ATC Results", Mandatory: true}) 230 for _, s := range parsedXML.Files { 231 for _, t := range s.ATCErrors { 232 log.Entry().Infof("%s in file '%s': %s in line %s found by %s", t.Severity, s.Key, t.Message, t.Line, t.Source) 233 } 234 } 235 if generateHTML == true { 236 htmlString := generateHTMLDocument(parsedXML) 237 htmlStringByte := []byte(htmlString) 238 atcResultHTMLFileName := strings.Trim(atcResultFileName, ".xml") + ".html" 239 err = ioutil.WriteFile(atcResultHTMLFileName, htmlStringByte, 0644) 240 if err == nil { 241 log.Entry().Info("Writing " + atcResultHTMLFileName + " file was successful") 242 reports = append(reports, piperutils.Path{Target: atcResultFileName, Name: "ATC Results HTML file", Mandatory: true}) 243 } 244 } 245 piperutils.PersistReportsAndLinks("abapEnvironmentRunATCCheck", "", reports, nil) 246 } 247 if err != nil { 248 return fmt.Errorf("Writing results failed: %w", err) 249 } 250 return nil 251 252 } 253 254 func runATC(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { 255 256 log.Entry().WithField("ABAP endpoint: ", details.URL).Info("triggering ATC run") 257 258 header := make(map[string][]string) 259 header["X-Csrf-Token"] = []string{details.XCsrfToken} 260 header["Content-Type"] = []string{"application/vnd.sap.atc.run.parameters.v1+xml; charset=utf-8;"} 261 262 resp, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 263 logResponseBody(resp) 264 if err != nil || (resp != nil && resp.StatusCode == 400) { //send request does not seem to produce error with StatusCode 400!!! 265 err = abaputils.HandleHTTPError(resp, err, "triggering ATC run failed with Status: "+resp.Status, details) 266 log.SetErrorCategory(log.ErrorService) 267 return resp, fmt.Errorf("triggering ATC run failed: %w", err) 268 } 269 defer resp.Body.Close() 270 return resp, err 271 } 272 273 func logResponseBody(resp *http.Response) error { 274 var bodyText []byte 275 var readError error 276 if resp != nil { 277 bodyText, readError = ioutil.ReadAll(resp.Body) 278 if readError != nil { 279 return readError 280 } 281 log.Entry().Infof("Response body: %s", bodyText) 282 } 283 return nil 284 } 285 286 func fetchXcsrfToken(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (string, error) { 287 288 log.Entry().WithField("ABAP Endpoint: ", details.URL).Debug("Fetching Xcrsf-Token") 289 290 details.URL += "/sap/bc/adt/api/atc/runs/00000000000000000000000000000000" 291 details.XCsrfToken = "fetch" 292 header := make(map[string][]string) 293 header["X-Csrf-Token"] = []string{details.XCsrfToken} 294 header["Accept"] = []string{"application/vnd.sap.atc.run.v1+xml"} 295 req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 296 if err != nil { 297 log.SetErrorCategory(log.ErrorInfrastructure) 298 return "", fmt.Errorf("Fetching Xcsrf-Token failed: %w", err) 299 } 300 defer req.Body.Close() 301 302 token := req.Header.Get("X-Csrf-Token") 303 return token, err 304 305 } 306 307 func pollATCRun(details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (string, error) { 308 309 log.Entry().WithField("ABAP endpoint", details.URL).Info("Polling ATC run status") 310 311 for { 312 resp, err := getHTTPResponseATCRun("GET", details, nil, client) 313 if err != nil { 314 return "", fmt.Errorf("Getting HTTP response failed: %w", err) 315 } 316 bodyText, err := ioutil.ReadAll(resp.Body) 317 if err != nil { 318 return "", fmt.Errorf("Reading response body failed: %w", err) 319 } 320 321 x := new(Run) 322 xml.Unmarshal(bodyText, &x) 323 log.Entry().WithField("StatusCode", resp.StatusCode).Info("Status: " + x.Status) 324 325 if x.Status == "Not Created" { 326 return "", err 327 } 328 if x.Status == "Completed" { 329 return x.Link[0].Key, err 330 } 331 if x.Status == "" { 332 return "", fmt.Errorf("Could not get any response from ATC poll: %w", errors.New("Status from ATC run is empty. Either it's not an ABAP system or ATC run hasn't started")) 333 } 334 time.Sleep(5 * time.Second) 335 } 336 } 337 338 func getHTTPResponseATCRun(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { 339 340 header := make(map[string][]string) 341 header["Accept"] = []string{"application/vnd.sap.atc.run.v1+xml"} 342 343 resp, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 344 if err != nil { 345 return resp, fmt.Errorf("Getting ATC run status failed: %w", err) 346 } 347 return resp, err 348 } 349 350 func getResultATCRun(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { 351 352 log.Entry().WithField("ABAP Endpoint: ", details.URL).Info("Getting ATC results") 353 354 header := make(map[string][]string) 355 header["x-csrf-token"] = []string{details.XCsrfToken} 356 header["Accept"] = []string{"application/vnd.sap.atc.checkstyle.v1+xml"} 357 358 resp, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 359 if err != nil { 360 return resp, fmt.Errorf("Getting ATC run results failed: %w", err) 361 } 362 return resp, err 363 } 364 365 func convertATCOptions(options *abapEnvironmentRunATCCheckOptions) abaputils.AbapEnvironmentOptions { 366 subOptions := abaputils.AbapEnvironmentOptions{} 367 368 subOptions.CfAPIEndpoint = options.CfAPIEndpoint 369 subOptions.CfServiceInstance = options.CfServiceInstance 370 subOptions.CfServiceKeyName = options.CfServiceKeyName 371 subOptions.CfOrg = options.CfOrg 372 subOptions.CfSpace = options.CfSpace 373 subOptions.Host = options.Host 374 subOptions.Password = options.Password 375 subOptions.Username = options.Username 376 377 return subOptions 378 } 379 380 func generateHTMLDocument(parsedXML *Result) (htmlDocumentString string) { 381 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>` 382 var htmlDocumentStringError, htmlDocumentStringWarning, htmlDocumentStringInfo, htmlDocumentStringDefault string 383 for _, s := range parsedXML.Files { 384 for _, t := range s.ATCErrors { 385 var trBackgroundColor string 386 if t.Severity == "error" { 387 trBackgroundColor = "rgba(227,85,0)" 388 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>` 389 } 390 if t.Severity == "warning" { 391 trBackgroundColor = "rgba(255,175,0, 0.75)" 392 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>` 393 } 394 if t.Severity == "info" { 395 trBackgroundColor = "rgba(255,175,0, 0.2)" 396 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>` 397 } 398 if t.Severity != "info" && t.Severity != "warning" && t.Severity != "error" { 399 trBackgroundColor = "rgba(255,175,0, 0)" 400 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>` 401 } 402 } 403 } 404 htmlDocumentString += htmlDocumentStringError + htmlDocumentStringWarning + htmlDocumentStringInfo + htmlDocumentStringDefault + `</table></body></html>` 405 406 return htmlDocumentString 407 } 408 409 //ATCConfiguration object for parsing yaml config of software components and packages 410 type ATCConfiguration struct { 411 CheckVariant string `json:"checkvariant,omitempty"` 412 Configuration string `json:"configuration,omitempty"` 413 Objects ATCObjects `json:"atcobjects"` 414 ObjectSet abaputils.ObjectSet `json:"objectset,omitempty"` 415 } 416 417 //ATCObjects in form of packages and software components to be checked 418 type ATCObjects struct { 419 Package []Package `json:"package"` 420 SoftwareComponent []SoftwareComponent `json:"softwarecomponent"` 421 } 422 423 //Package for ATC run to be checked 424 type Package struct { 425 Name string `json:"name"` 426 IncludeSubpackages bool `json:"includesubpackage"` 427 } 428 429 //SoftwareComponent for ATC run to be checked 430 type SoftwareComponent struct { 431 Name string `json:"name"` 432 } 433 434 //Run Object for parsing XML 435 type Run struct { 436 XMLName xml.Name `xml:"run"` 437 Status string `xml:"status,attr"` 438 Link []Link `xml:"link"` 439 } 440 441 //Link of XML object 442 type Link struct { 443 Key string `xml:"href,attr"` 444 Value string `xml:",chardata"` 445 } 446 447 //Result from ATC check for all files that were checked 448 type Result struct { 449 XMLName xml.Name `xml:"checkstyle"` 450 Files []File `xml:"file"` 451 } 452 453 //File that contains ATC check with error for checked file 454 type File struct { 455 Key string `xml:"name,attr"` 456 Value string `xml:",chardata"` 457 ATCErrors []ATCError `xml:"error"` 458 } 459 460 //ATCError with message 461 type ATCError struct { 462 Text string `xml:",chardata"` 463 Message string `xml:"message,attr"` 464 Source string `xml:"source,attr"` 465 Line string `xml:"line,attr"` 466 Severity string `xml:"severity,attr"` 467 }