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"`
}