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  }