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  }