github.com/xgoffin/jenkins-library@v1.154.0/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 "strconv" 12 "strings" 13 "time" 14 15 "github.com/SAP/jenkins-library/pkg/abaputils" 16 "github.com/SAP/jenkins-library/pkg/command" 17 piperhttp "github.com/SAP/jenkins-library/pkg/http" 18 "github.com/SAP/jenkins-library/pkg/log" 19 "github.com/SAP/jenkins-library/pkg/piperutils" 20 "github.com/SAP/jenkins-library/pkg/telemetry" 21 "github.com/pkg/errors" 22 ) 23 24 func abapEnvironmentRunATCCheck(options abapEnvironmentRunATCCheckOptions, telemetryData *telemetry.CustomData) { 25 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 var autils = abaputils.AbapUtils{ 34 Exec: c, 35 } 36 var err error 37 38 client := piperhttp.Client{} 39 cookieJar, _ := cookiejar.New(nil) 40 clientOptions := piperhttp.ClientOptions{ 41 CookieJar: cookieJar, 42 } 43 client.SetOptions(clientOptions) 44 45 var details abaputils.ConnectionDetailsHTTP 46 //If Host flag is empty read ABAP endpoint from Service Key instead. Otherwise take ABAP system endpoint from config instead 47 if err == nil { 48 details, err = autils.GetAbapCommunicationArrangementInfo(subOptions, "") 49 } 50 var resp *http.Response 51 //Fetch Xcrsf-Token 52 if err == nil { 53 credentialsOptions := piperhttp.ClientOptions{ 54 Username: details.User, 55 Password: details.Password, 56 CookieJar: cookieJar, 57 } 58 client.SetOptions(credentialsOptions) 59 details.XCsrfToken, err = fetchXcsrfToken("GET", details, nil, &client) 60 } 61 if err == nil { 62 resp, err = triggerATCRun(options, details, &client) 63 } 64 if err == nil { 65 err = fetchAndPersistATCResults(resp, details, &client, options.AtcResultsFileName, options.GenerateHTML) 66 } 67 if err != nil { 68 log.Entry().WithError(err).Fatal("step execution failed") 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, atcResultFileName string, generateHTML bool) error { 75 var err error 76 var abapEndpoint string 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 = ioutil.ReadAll(resp.Body) 89 } 90 if err == nil { 91 defer resp.Body.Close() 92 err = logAndPersistATCResult(body, atcResultFileName, generateHTML) 93 } 94 if err != nil { 95 return fmt.Errorf("Handling ATC result failed: %w", err) 96 } 97 return nil 98 } 99 100 func triggerATCRun(config abapEnvironmentRunATCCheckOptions, details abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) (*http.Response, error) { 101 102 bodyString, err := buildATCRequestBody(config) 103 if err != nil { 104 return nil, err 105 } 106 var resp *http.Response 107 abapEndpoint := details.URL 108 109 log.Entry().Infof("Request Body: %s", bodyString) 110 var body = []byte(bodyString) 111 details.URL = abapEndpoint + "/sap/bc/adt/api/atc/runs?clientWait=false" 112 resp, err = runATC("POST", details, body, client) 113 return resp, err 114 } 115 116 func buildATCRequestBody(config abapEnvironmentRunATCCheckOptions) (bodyString string, err error) { 117 118 atcConfig, err := resolveATCConfiguration(config) 119 if err != nil { 120 return "", err 121 } 122 123 // Create string for the run parameters 124 variant := "ABAP_CLOUD_DEVELOPMENT_DEFAULT" 125 if atcConfig.CheckVariant != "" { 126 variant = atcConfig.CheckVariant 127 } 128 log.Entry().Infof("ATC Check Variant: %s", variant) 129 runParameters := ` checkVariant="` + variant + `"` 130 if atcConfig.Configuration != "" { 131 runParameters += ` configuration="` + atcConfig.Configuration + `"` 132 } 133 134 objectSet, err := getATCObjectSet(atcConfig) 135 136 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 + `>` + objectSet + `</atc:runparameters>` 137 return bodyString, err 138 } 139 140 func resolveATCConfiguration(config abapEnvironmentRunATCCheckOptions) (atcConfig ATCConfiguration, err error) { 141 142 if config.AtcConfig != "" { 143 // Configuration defaults to AUnitConfig 144 log.Entry().Infof("ATC Configuration: %s", config.AtcConfig) 145 atcConfigFile, err := abaputils.ReadConfigFile(config.AtcConfig) 146 if err != nil { 147 return atcConfig, err 148 } 149 json.Unmarshal(atcConfigFile, &atcConfig) 150 return atcConfig, err 151 152 } else if config.Repositories != "" { 153 // Fallback / EasyMode is the Repositories configuration 154 log.Entry().Infof("ATC Configuration derived from: %s", config.Repositories) 155 repositories, err := abaputils.GetRepositories((&abaputils.RepositoriesConfig{Repositories: config.Repositories})) 156 if err != nil { 157 return atcConfig, err 158 } 159 for _, repository := range repositories { 160 atcConfig.Objects.SoftwareComponent = append(atcConfig.Objects.SoftwareComponent, SoftwareComponent{Name: repository.Name}) 161 } 162 return atcConfig, nil 163 } else { 164 // Fail if no configuration is provided 165 return atcConfig, errors.New("No configuration provided - please provide either an ATC configuration file or a repository configuration file") 166 } 167 } 168 169 func getATCObjectSet(ATCConfig ATCConfiguration) (objectSet string, err error) { 170 if len(ATCConfig.Objects.Package) == 0 && len(ATCConfig.Objects.SoftwareComponent) == 0 { 171 log.SetErrorCategory(log.ErrorConfiguration) 172 return "", fmt.Errorf("Error while parsing ATC run config. Please provide the packages and/or the software components to be checked! %w", errors.New("No Package or Software Component specified. Please provide either one or both of them")) 173 } 174 175 objectSet += `<obj:objectSet>` 176 177 //Build SC XML body 178 if len(ATCConfig.Objects.SoftwareComponent) != 0 { 179 objectSet += "<obj:softwarecomponents>" 180 for _, s := range ATCConfig.Objects.SoftwareComponent { 181 objectSet += `<obj:softwarecomponent value="` + s.Name + `"/>` 182 } 183 objectSet += "</obj:softwarecomponents>" 184 } 185 186 //Build Package XML body 187 if len(ATCConfig.Objects.Package) != 0 { 188 objectSet += "<obj:packages>" 189 for _, s := range ATCConfig.Objects.Package { 190 objectSet += `<obj:package value="` + s.Name + `" includeSubpackages="` + strconv.FormatBool(s.IncludeSubpackages) + `"/>` 191 } 192 objectSet += "</obj:packages>" 193 } 194 195 objectSet += `</obj:objectSet>` 196 197 return objectSet, nil 198 } 199 200 func logAndPersistATCResult(body []byte, atcResultFileName string, generateHTML bool) (err error) { 201 if len(body) == 0 { 202 return fmt.Errorf("Parsing ATC result failed: %w", errors.New("Body is empty, can't parse empty body")) 203 } 204 205 responseBody := string(body) 206 log.Entry().Debugf("Response body: %s", responseBody) 207 if strings.HasPrefix(responseBody, "<html>") { 208 return errors.New("The Software Component could not be checked. Please make sure the respective Software Component has been cloned successfully on the system") 209 } 210 211 parsedXML := new(Result) 212 xml.Unmarshal([]byte(body), &parsedXML) 213 if len(parsedXML.Files) == 0 { 214 log.Entry().Info("There were no results from this run, most likely the checked Software Components are empty or contain no ATC findings") 215 } 216 217 err = ioutil.WriteFile(atcResultFileName, body, 0644) 218 if err == nil { 219 log.Entry().Infof("Writing %s file was successful", atcResultFileName) 220 var reports []piperutils.Path 221 reports = append(reports, piperutils.Path{Target: atcResultFileName, Name: "ATC Results", Mandatory: true}) 222 for _, s := range parsedXML.Files { 223 for _, t := range s.ATCErrors { 224 log.Entry().Infof("%s in file '%s': %s in line %s found by %s", t.Severity, s.Key, t.Message, t.Line, t.Source) 225 } 226 } 227 if generateHTML == true { 228 htmlString := generateHTMLDocument(parsedXML) 229 htmlStringByte := []byte(htmlString) 230 atcResultHTMLFileName := strings.Trim(atcResultFileName, ".xml") + ".html" 231 err = ioutil.WriteFile(atcResultHTMLFileName, htmlStringByte, 0644) 232 if err == nil { 233 log.Entry().Info("Writing " + atcResultHTMLFileName + " file was successful") 234 reports = append(reports, piperutils.Path{Target: atcResultFileName, Name: "ATC Results HTML file", Mandatory: true}) 235 } 236 } 237 piperutils.PersistReportsAndLinks("abapEnvironmentRunATCCheck", "", reports, nil) 238 } 239 if err != nil { 240 return fmt.Errorf("Writing results failed: %w", err) 241 } 242 return nil 243 } 244 245 func runATC(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { 246 247 log.Entry().WithField("ABAP endpoint: ", details.URL).Info("triggering ATC run") 248 249 header := make(map[string][]string) 250 header["X-Csrf-Token"] = []string{details.XCsrfToken} 251 header["Content-Type"] = []string{"application/vnd.sap.atc.run.parameters.v1+xml; charset=utf-8;"} 252 253 resp, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 254 logResponseBody(resp) 255 if err != nil || (resp != nil && resp.StatusCode == 400) { //send request does not seem to produce error with StatusCode 400!!! 256 err = abaputils.HandleHTTPError(resp, err, "triggering ATC run failed with Status: "+resp.Status, details) 257 log.SetErrorCategory(log.ErrorService) 258 return resp, fmt.Errorf("triggering ATC run failed: %w", err) 259 } 260 defer resp.Body.Close() 261 return resp, err 262 } 263 264 func logResponseBody(resp *http.Response) error { 265 var bodyText []byte 266 var readError error 267 if resp != nil { 268 bodyText, readError = ioutil.ReadAll(resp.Body) 269 if readError != nil { 270 return readError 271 } 272 log.Entry().Infof("Response body: %s", bodyText) 273 } 274 return nil 275 } 276 277 func fetchXcsrfToken(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (string, error) { 278 279 log.Entry().WithField("ABAP Endpoint: ", details.URL).Debug("Fetching Xcrsf-Token") 280 281 details.URL += "/sap/bc/adt/api/atc/runs/00000000000000000000000000000000" 282 details.XCsrfToken = "fetch" 283 header := make(map[string][]string) 284 header["X-Csrf-Token"] = []string{details.XCsrfToken} 285 header["Accept"] = []string{"application/vnd.sap.atc.run.v1+xml"} 286 req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 287 if err != nil { 288 log.SetErrorCategory(log.ErrorInfrastructure) 289 return "", fmt.Errorf("Fetching Xcsrf-Token failed: %w", err) 290 } 291 defer req.Body.Close() 292 293 token := req.Header.Get("X-Csrf-Token") 294 return token, err 295 } 296 297 func pollATCRun(details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (string, error) { 298 299 log.Entry().WithField("ABAP endpoint", details.URL).Info("Polling ATC run status") 300 301 for { 302 resp, err := getHTTPResponseATCRun("GET", details, nil, client) 303 if err != nil { 304 return "", fmt.Errorf("Getting HTTP response failed: %w", err) 305 } 306 bodyText, err := ioutil.ReadAll(resp.Body) 307 if err != nil { 308 return "", fmt.Errorf("Reading response body failed: %w", err) 309 } 310 311 x := new(Run) 312 xml.Unmarshal(bodyText, &x) 313 log.Entry().WithField("StatusCode", resp.StatusCode).Info("Status: " + x.Status) 314 315 if x.Status == "Not Created" { 316 return "", err 317 } 318 if x.Status == "Completed" { 319 return x.Link[0].Key, err 320 } 321 if x.Status == "" { 322 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")) 323 } 324 time.Sleep(5 * time.Second) 325 } 326 } 327 328 func getHTTPResponseATCRun(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { 329 330 header := make(map[string][]string) 331 header["Accept"] = []string{"application/vnd.sap.atc.run.v1+xml"} 332 333 resp, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 334 if err != nil { 335 return resp, fmt.Errorf("Getting ATC run status failed: %w", err) 336 } 337 return resp, err 338 } 339 340 func getResultATCRun(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { 341 342 log.Entry().WithField("ABAP Endpoint: ", details.URL).Info("Getting ATC results") 343 344 header := make(map[string][]string) 345 header["x-csrf-token"] = []string{details.XCsrfToken} 346 header["Accept"] = []string{"application/vnd.sap.atc.checkstyle.v1+xml"} 347 348 resp, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) 349 if err != nil { 350 return resp, fmt.Errorf("Getting ATC run results failed: %w", err) 351 } 352 return resp, err 353 } 354 355 func convertATCOptions(options *abapEnvironmentRunATCCheckOptions) abaputils.AbapEnvironmentOptions { 356 subOptions := abaputils.AbapEnvironmentOptions{} 357 358 subOptions.CfAPIEndpoint = options.CfAPIEndpoint 359 subOptions.CfServiceInstance = options.CfServiceInstance 360 subOptions.CfServiceKeyName = options.CfServiceKeyName 361 subOptions.CfOrg = options.CfOrg 362 subOptions.CfSpace = options.CfSpace 363 subOptions.Host = options.Host 364 subOptions.Password = options.Password 365 subOptions.Username = options.Username 366 367 return subOptions 368 } 369 370 func generateHTMLDocument(parsedXML *Result) (htmlDocumentString string) { 371 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>` 372 var htmlDocumentStringError, htmlDocumentStringWarning, htmlDocumentStringInfo, htmlDocumentStringDefault string 373 for _, s := range parsedXML.Files { 374 for _, t := range s.ATCErrors { 375 var trBackgroundColor string 376 if t.Severity == "error" { 377 trBackgroundColor = "rgba(227,85,0)" 378 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>` 379 } 380 if t.Severity == "warning" { 381 trBackgroundColor = "rgba(255,175,0, 0.75)" 382 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>` 383 } 384 if t.Severity == "info" { 385 trBackgroundColor = "rgba(255,175,0, 0.2)" 386 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>` 387 } 388 if t.Severity != "info" && t.Severity != "warning" && t.Severity != "error" { 389 trBackgroundColor = "rgba(255,175,0, 0)" 390 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>` 391 } 392 } 393 } 394 htmlDocumentString += htmlDocumentStringError + htmlDocumentStringWarning + htmlDocumentStringInfo + htmlDocumentStringDefault + `</table></body></html>` 395 396 return htmlDocumentString 397 } 398 399 //ATCConfiguration object for parsing yaml config of software components and packages 400 type ATCConfiguration struct { 401 CheckVariant string `json:"checkvariant,omitempty"` 402 Configuration string `json:"configuration,omitempty"` 403 Objects ATCObjects `json:"atcobjects"` 404 } 405 406 //ATCObjects in form of packages and software components to be checked 407 type ATCObjects struct { 408 Package []Package `json:"package"` 409 SoftwareComponent []SoftwareComponent `json:"softwarecomponent"` 410 } 411 412 //Package for ATC run to be checked 413 type Package struct { 414 Name string `json:"name"` 415 IncludeSubpackages bool `json:"includesubpackage"` 416 } 417 418 //SoftwareComponent for ATC run to be checked 419 type SoftwareComponent struct { 420 Name string `json:"name"` 421 } 422 423 //Run Object for parsing XML 424 type Run struct { 425 XMLName xml.Name `xml:"run"` 426 Status string `xml:"status,attr"` 427 Link []Link `xml:"link"` 428 } 429 430 //Link of XML object 431 type Link struct { 432 Key string `xml:"href,attr"` 433 Value string `xml:",chardata"` 434 } 435 436 //Result from ATC check for all files that were checked 437 type Result struct { 438 XMLName xml.Name `xml:"checkstyle"` 439 Files []File `xml:"file"` 440 } 441 442 //File that contains ATC check with error for checked file 443 type File struct { 444 Key string `xml:"name,attr"` 445 Value string `xml:",chardata"` 446 ATCErrors []ATCError `xml:"error"` 447 } 448 449 //ATCError with message 450 type ATCError struct { 451 Text string `xml:",chardata"` 452 Message string `xml:"message,attr"` 453 Source string `xml:"source,attr"` 454 Line string `xml:"line,attr"` 455 Severity string `xml:"severity,attr"` 456 }