package cmd import ( "bytes" "encoding/json" "encoding/xml" "fmt" "io/ioutil" "net/http" "net/http/cookiejar" "path/filepath" "strconv" "time" "github.com/SAP/jenkins-library/pkg/abaputils" "github.com/SAP/jenkins-library/pkg/command" piperhttp "github.com/SAP/jenkins-library/pkg/http" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/piperutils" "github.com/SAP/jenkins-library/pkg/telemetry" "github.com/ghodss/yaml" "github.com/pkg/errors" ) func abapEnvironmentRunATCCheck(options abapEnvironmentRunATCCheckOptions, telemetryData *telemetry.CustomData) { // Mapping for options subOptions := abaputils.AbapEnvironmentOptions{} subOptions.CfAPIEndpoint = options.CfAPIEndpoint subOptions.CfServiceInstance = options.CfServiceInstance subOptions.CfServiceKeyName = options.CfServiceKeyName subOptions.CfOrg = options.CfOrg subOptions.CfSpace = options.CfSpace subOptions.Host = options.Host subOptions.Password = options.Password subOptions.Username = options.Username c := &command.Command{} c.Stdout(log.Entry().Writer()) c.Stderr(log.Entry().Writer()) var autils = abaputils.AbapUtils{ Exec: c, } var err error client := piperhttp.Client{} cookieJar, _ := cookiejar.New(nil) clientOptions := piperhttp.ClientOptions{ CookieJar: cookieJar, } client.SetOptions(clientOptions) var details abaputils.ConnectionDetailsHTTP var abapEndpoint string //If Host flag is empty read ABAP endpoint from Service Key instead. Otherwise take ABAP system endpoint from config instead if err == nil { details, err = autils.GetAbapCommunicationArrangementInfo(subOptions, "") } var resp *http.Response //Fetch Xcrsf-Token if err == nil { abapEndpoint = details.URL credentialsOptions := piperhttp.ClientOptions{ Username: details.User, Password: details.Password, CookieJar: cookieJar, } client.SetOptions(credentialsOptions) details.XCsrfToken, err = fetchXcsrfToken("GET", details, nil, &client) } if err == nil { resp, err = triggerATCrun(options, details, &client, abapEndpoint) } if err == nil { err = handleATCresults(resp, details, &client, abapEndpoint) } if err != nil { log.Entry().WithError(err).Fatal("step execution failed") } log.Entry().Info("ATC run completed succesfully. The respective run results are listed above.") } func handleATCresults(resp *http.Response, details abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, abapEndpoint string) error { var err error location := resp.Header.Get("Location") details.URL = abapEndpoint + location location, err = pollATCRun(details, nil, client) if err == nil { details.URL = abapEndpoint + location resp, err = getResultATCRun("GET", details, nil, client) } //Parse response var body []byte if err == nil { body, err = ioutil.ReadAll(resp.Body) } if err == nil { defer resp.Body.Close() err = parseATCResult(body) } if err != nil { return fmt.Errorf("Handling ATC result failed: %w", err) } return nil } func triggerATCrun(config abapEnvironmentRunATCCheckOptions, details abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, abapEndpoint string) (*http.Response, error) { var atcConfigyamlFile []byte filelocation, err := filepath.Glob(config.AtcConfig) //Parse YAML ATC run configuration as body for ATC run trigger if err == nil { filename, _ := filepath.Abs(filelocation[0]) atcConfigyamlFile, err = ioutil.ReadFile(filename) } var ATCConfig ATCconfig if err == nil { var result []byte result, err = yaml.YAMLToJSON(atcConfigyamlFile) json.Unmarshal(result, &ATCConfig) } var packageString string var softwareComponentString string if err == nil { packageString, softwareComponentString, err = buildATCCheckBody(ATCConfig) } //Trigger ATC run var resp *http.Response var bodyString = `` + softwareComponentString + packageString + `` var body = []byte(bodyString) if err == nil { details.URL = abapEndpoint + "/sap/bc/adt/api/atc/runs?clientWait=false" resp, err = runATC("POST", details, body, client) } if err != nil { return resp, fmt.Errorf("Triggering ATC run failed: %w", err) } return resp, nil } func buildATCCheckBody(ATCConfig ATCconfig) (string, string, error) { if len(ATCConfig.Objects.Package) == 0 && len(ATCConfig.Objects.SoftwareComponent) == 0 { 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")) } var packageString string var softwareComponentString string //Build Package XML body if len(ATCConfig.Objects.Package) != 0 { packageString += "" for _, s := range ATCConfig.Objects.Package { packageString += `` } packageString += "" } //Build SC XML body if len(ATCConfig.Objects.SoftwareComponent) != 0 { softwareComponentString += "" for _, s := range ATCConfig.Objects.SoftwareComponent { softwareComponentString += `` } softwareComponentString += "" } return packageString, softwareComponentString, nil } func parseATCResult(body []byte) error { if len(body) == 0 { return fmt.Errorf("Parsing ATC result failed: %w", errors.New("Body is empty, can't parse empty body")) } parsedXML := new(Result) xml.Unmarshal([]byte(body), &parsedXML) err := ioutil.WriteFile("ATCResults.xml", body, 0644) if err == nil { var reports []piperutils.Path reports = append(reports, piperutils.Path{Target: "ATCResults.xml", Name: "ATC Results", Mandatory: true}) piperutils.PersistReportsAndLinks("abapEnvironmentRunATCCheck", "", reports, nil) for _, s := range parsedXML.Files { for _, t := range s.ATCErrors { log.Entry().Error("Error in file " + s.Key + ": " + t.Key) } } } if err != nil { return fmt.Errorf("Writing results to XML file failed: %w", err) } return nil } func runATC(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { log.Entry().WithField("ABAP endpoint: ", details.URL).Info("Triggering ATC run") header := make(map[string][]string) header["X-Csrf-Token"] = []string{details.XCsrfToken} header["Content-Type"] = []string{"application/vnd.sap.atc.run.parameters.v1+xml; charset=utf-8;"} req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) if err != nil { return req, fmt.Errorf("Triggering ATC run failed: %w", err) } defer req.Body.Close() return req, err } func fetchXcsrfToken(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (string, error) { log.Entry().WithField("ABAP Endpoint: ", details.URL).Info("Fetching Xcrsf-Token") details.URL += "/sap/bc/adt/api/atc/runs/00000000000000000000000000000000" details.XCsrfToken = "fetch" header := make(map[string][]string) header["X-Csrf-Token"] = []string{details.XCsrfToken} header["Accept"] = []string{"application/vnd.sap.atc.run.v1+xml"} req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) if err != nil { return "", fmt.Errorf("Fetching Xcsrf-Token failed: %w", err) } defer req.Body.Close() token := req.Header.Get("X-Csrf-Token") return token, err } func pollATCRun(details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (string, error) { log.Entry().WithField("ABAP endpoint", details.URL).Info("Polling ATC run status") for { resp, err := getHTTPResponseATCRun("GET", details, nil, client) if err != nil { return "", fmt.Errorf("Getting HTTP response failed: %w", err) } bodyText, err := ioutil.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("Reading response body failed: %w", err) } x := new(Run) xml.Unmarshal(bodyText, &x) log.Entry().WithField("StatusCode", resp.StatusCode).Info("Status: " + x.Status) if x.Status == "Not Created" { return "", err } if x.Status == "Completed" { return x.Link[0].Key, err } if x.Status == "" { 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")) } time.Sleep(5 * time.Second) } } func getHTTPResponseATCRun(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { log.Entry().WithField("ABAP Endpoint: ", details.URL).Info("Polling ATC run status") header := make(map[string][]string) header["Accept"] = []string{"application/vnd.sap.atc.run.v1+xml"} req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) if err != nil { return req, fmt.Errorf("Getting ATC run status failed: %w", err) } return req, err } func getResultATCRun(requestType string, details abaputils.ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { log.Entry().WithField("ABAP Endpoint: ", details.URL).Info("Getting ATC results") header := make(map[string][]string) header["x-csrf-token"] = []string{details.XCsrfToken} header["Accept"] = []string{"application/vnd.sap.atc.checkstyle.v1+xml"} req, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) if err != nil { return req, fmt.Errorf("Getting ATC run results failed: %w", err) } return req, err } //ATCconfig object for parsing yaml config of software components and packages type ATCconfig struct { Objects ATCObjects `json:"atcobjects"` } //ATCObjects in form of packages and software components to be checked type ATCObjects struct { Package []Package `json:"package"` SoftwareComponent []SoftwareComponent `json:"softwarecomponent"` } //Package for ATC run to be checked type Package struct { Name string `json:"name"` IncludeSubpackages bool `json:"includesubpackage"` } //SoftwareComponent for ATC run to be checked type SoftwareComponent struct { Name string `json:"name"` } //Run Object for parsing XML type Run struct { XMLName xml.Name `xml:"run"` Status string `xml:"status,attr"` Link []Link `xml:"link"` } //Link of XML object type Link struct { Key string `xml:"href,attr"` Value string `xml:",chardata"` } //Result from ATC check for all files that were checked type Result struct { XMLName xml.Name `xml:"checkstyle"` Files []File `xml:"file"` } //File that contains ATC check with error for checked file type File struct { Key string `xml:"name,attr"` Value string `xml:",chardata"` ATCErrors []ATCError `xml:"error"` } //ATCError with message type ATCError struct { Key string `xml:"message,attr"` Value string `xml:",chardata"` }