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  }