github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/abaputils/abaputils.go (about)

     1  package abaputils
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/SAP/jenkins-library/pkg/cloudfoundry"
    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/ghodss/yaml"
    21  	"github.com/pkg/errors"
    22  )
    23  
    24  // AbapUtils Struct
    25  type AbapUtils struct {
    26  	Exec      command.ExecRunner
    27  	Intervall time.Duration
    28  }
    29  
    30  /*
    31  Communication for defining function used for communication
    32  */
    33  type Communication interface {
    34  	GetAbapCommunicationArrangementInfo(options AbapEnvironmentOptions, oDataURL string) (ConnectionDetailsHTTP, error)
    35  	GetPollIntervall() time.Duration
    36  }
    37  
    38  // GetAbapCommunicationArrangementInfo function fetches the communcation arrangement information in SAP CP ABAP Environment
    39  func (abaputils *AbapUtils) GetAbapCommunicationArrangementInfo(options AbapEnvironmentOptions, oDataURL string) (ConnectionDetailsHTTP, error) {
    40  	c := abaputils.Exec
    41  	var connectionDetails ConnectionDetailsHTTP
    42  	var error error
    43  
    44  	if options.Host != "" {
    45  		// Host, User and Password are directly provided -> check for host schema (double https)
    46  		match, err := regexp.MatchString(`^(https|HTTPS):\/\/.*`, options.Host)
    47  		if err != nil {
    48  			log.SetErrorCategory(log.ErrorConfiguration)
    49  			return connectionDetails, errors.Wrap(err, "Schema validation for host parameter failed. Check for https.")
    50  		}
    51  		var hostOdataURL = options.Host + oDataURL
    52  		if match {
    53  			connectionDetails.URL = hostOdataURL
    54  			connectionDetails.Host = options.Host
    55  		} else {
    56  			connectionDetails.URL = "https://" + hostOdataURL
    57  			connectionDetails.Host = "https://" + options.Host
    58  		}
    59  		connectionDetails.User = options.Username
    60  		connectionDetails.Password = options.Password
    61  	} else {
    62  		if options.CfAPIEndpoint == "" || options.CfOrg == "" || options.CfSpace == "" || options.CfServiceInstance == "" || options.CfServiceKeyName == "" {
    63  			var err = errors.New("Parameters missing. Please provide EITHER the Host of the ABAP server OR the Cloud Foundry ApiEndpoint, Organization, Space, Service Instance and a corresponding Service Key for the Communication Scenario SAP_COM_0510")
    64  			log.SetErrorCategory(log.ErrorConfiguration)
    65  			return connectionDetails, err
    66  		}
    67  		// Url, User and Password should be read from a cf service key
    68  		var abapServiceKey, error = ReadServiceKeyAbapEnvironment(options, c)
    69  		if error != nil {
    70  			return connectionDetails, errors.Wrap(error, "Read service key failed")
    71  		}
    72  		connectionDetails.Host = abapServiceKey.URL
    73  		connectionDetails.URL = abapServiceKey.URL + oDataURL
    74  		connectionDetails.User = abapServiceKey.Abap.Username
    75  		connectionDetails.Password = abapServiceKey.Abap.Password
    76  	}
    77  	return connectionDetails, error
    78  }
    79  
    80  // ReadServiceKeyAbapEnvironment from Cloud Foundry and returns it. Depending on user/developer requirements if he wants to perform further Cloud Foundry actions
    81  func ReadServiceKeyAbapEnvironment(options AbapEnvironmentOptions, c command.ExecRunner) (AbapServiceKey, error) {
    82  
    83  	var abapServiceKeyV8 AbapServiceKeyV8
    84  	var abapServiceKey AbapServiceKey
    85  	var serviceKeyJSON string
    86  	var err error
    87  
    88  	cfconfig := cloudfoundry.ServiceKeyOptions{
    89  		CfAPIEndpoint:     options.CfAPIEndpoint,
    90  		CfOrg:             options.CfOrg,
    91  		CfSpace:           options.CfSpace,
    92  		CfServiceInstance: options.CfServiceInstance,
    93  		CfServiceKeyName:  options.CfServiceKeyName,
    94  		Username:          options.Username,
    95  		Password:          options.Password,
    96  	}
    97  
    98  	cf := cloudfoundry.CFUtils{Exec: c}
    99  
   100  	serviceKeyJSON, err = cf.ReadServiceKey(cfconfig)
   101  
   102  	if err != nil {
   103  		// Executing cfReadServiceKeyScript failed
   104  		return abapServiceKeyV8.Credentials, err
   105  	}
   106  
   107  	// Depending on the cf cli version, the service key may be returned in a different format. For compatibility reason, both formats are supported
   108  	unmarshalErrorV8 := json.Unmarshal([]byte(serviceKeyJSON), &abapServiceKeyV8)
   109  	if abapServiceKeyV8 == (AbapServiceKeyV8{}) {
   110  		if unmarshalErrorV8 != nil {
   111  			log.Entry().Debug(unmarshalErrorV8.Error())
   112  		}
   113  		log.Entry().Debug("Could not parse the service key in the cf cli v8 format.")
   114  	} else {
   115  		log.Entry().Info("Service Key read successfully")
   116  		return abapServiceKeyV8.Credentials, nil
   117  	}
   118  
   119  	unmarshalError := json.Unmarshal([]byte(serviceKeyJSON), &abapServiceKey)
   120  	if abapServiceKey == (AbapServiceKey{}) {
   121  		if unmarshalError != nil {
   122  			log.Entry().Debug(unmarshalError.Error())
   123  		}
   124  		log.Entry().Debug("Could not parse the service key in the cf cli v7 format.")
   125  	} else {
   126  		log.Entry().Info("Service Key read successfully")
   127  		return abapServiceKey, nil
   128  	}
   129  	log.SetErrorCategory(log.ErrorInfrastructure)
   130  	return abapServiceKeyV8.Credentials, errors.New("Parsing the service key failed for all supported formats. Service key is empty")
   131  }
   132  
   133  /*
   134  GetPollIntervall returns the specified intervall from AbapUtils or a default value of 10 seconds
   135  */
   136  func (abaputils *AbapUtils) GetPollIntervall() time.Duration {
   137  	if abaputils.Intervall != 0 {
   138  		return abaputils.Intervall
   139  	}
   140  	return 10 * time.Second
   141  }
   142  
   143  /*
   144  ReadCOnfigFile reads a file from a specific path and returns the json string as []byte
   145  */
   146  func ReadConfigFile(path string) (file []byte, err error) {
   147  	filelocation, err := filepath.Glob(path)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  	if len(filelocation) == 0 {
   152  		return nil, errors.New("Could not find " + path)
   153  	}
   154  	filename, err := filepath.Abs(filelocation[0])
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  	yamlFile, err := os.ReadFile(filename)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  	var jsonFile []byte
   163  	jsonFile, err = yaml.YAMLToJSON(yamlFile)
   164  	return jsonFile, err
   165  }
   166  
   167  // GetHTTPResponse wraps the SendRequest function of piperhttp
   168  func GetHTTPResponse(requestType string, connectionDetails ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) {
   169  
   170  	header := make(map[string][]string)
   171  	header["Content-Type"] = []string{"application/json"}
   172  	header["Accept"] = []string{"application/json"}
   173  	header["x-csrf-token"] = []string{connectionDetails.XCsrfToken}
   174  
   175  	httpResponse, err := client.SendRequest(requestType, connectionDetails.URL, bytes.NewBuffer(body), header, nil)
   176  	return httpResponse, err
   177  }
   178  
   179  // HandleHTTPError handles ABAP error messages which can occur when using OData services
   180  //
   181  // The point of this function is to enrich the error received from a HTTP Request (which is passed as a parameter to this function).
   182  // Further error details may be present in the response body of the HTTP response.
   183  // If the response body is parseable, the included details are wrapped around the original error from the HTTP repsponse.
   184  // If this is not possible, the original error is returned.
   185  func HandleHTTPError(resp *http.Response, err error, message string, connectionDetails ConnectionDetailsHTTP) error {
   186  	if resp == nil {
   187  		// Response is nil in case of a timeout
   188  		log.Entry().WithError(err).WithField("ABAP Endpoint", connectionDetails.URL).Error("Request failed")
   189  
   190  		match, _ := regexp.MatchString(".*EOF$", err.Error())
   191  		if match {
   192  			AddDefaultDashedLine()
   193  			log.Entry().Infof("%s", "A connection could not be established to the ABAP system. The typical root cause is the network configuration (firewall, IP allowlist, etc.)")
   194  			AddDefaultDashedLine()
   195  		}
   196  
   197  		log.Entry().Infof("Error message: %s,", err.Error())
   198  	} else {
   199  
   200  		defer resp.Body.Close()
   201  
   202  		log.Entry().WithField("StatusCode", resp.Status).WithField("User", connectionDetails.User).WithField("URL", connectionDetails.URL).Error(message)
   203  
   204  		errorText, errorCode, parsingError := GetErrorDetailsFromResponse(resp)
   205  		if parsingError != nil {
   206  			return err
   207  		}
   208  		abapError := errors.New(fmt.Sprintf("%s - %s", errorCode, errorText))
   209  		err = errors.Wrap(abapError, err.Error())
   210  
   211  	}
   212  	return err
   213  }
   214  
   215  func GetErrorDetailsFromResponse(resp *http.Response) (errorString string, errorCode string, err error) {
   216  
   217  	// Include the error message of the ABAP Environment system, if available
   218  	var abapErrorResponse AbapError
   219  	bodyText, readError := io.ReadAll(resp.Body)
   220  	if readError != nil {
   221  		return "", "", readError
   222  	}
   223  	var abapResp map[string]*json.RawMessage
   224  	errUnmarshal := json.Unmarshal(bodyText, &abapResp)
   225  	if errUnmarshal != nil {
   226  		return "", "", errUnmarshal
   227  	}
   228  	if _, ok := abapResp["error"]; ok {
   229  		json.Unmarshal(*abapResp["error"], &abapErrorResponse)
   230  		if (AbapError{}) != abapErrorResponse {
   231  			log.Entry().WithField("ErrorCode", abapErrorResponse.Code).Debug(abapErrorResponse.Message.Value)
   232  			return abapErrorResponse.Message.Value, abapErrorResponse.Code, nil
   233  		}
   234  	}
   235  
   236  	return "", "", errors.New("Could not parse the JSON error response")
   237  
   238  }
   239  
   240  // ConvertTime formats an ABAP timestamp string from format /Date(1585576807000+0000)/ into a UNIX timestamp and returns it
   241  func ConvertTime(logTimeStamp string) time.Time {
   242  	seconds := strings.TrimPrefix(strings.TrimSuffix(logTimeStamp, "000+0000)/"), "/Date(")
   243  	n, error := strconv.ParseInt(seconds, 10, 64)
   244  	if error != nil {
   245  		return time.Unix(0, 0).UTC()
   246  	}
   247  	t := time.Unix(n, 0).UTC()
   248  	return t
   249  }
   250  
   251  // AddDefaultDashedLine adds 25 dashes
   252  func AddDefaultDashedLine() {
   253  	log.Entry().Infof(strings.Repeat("-", 25))
   254  }
   255  
   256  // AddDefaultDebugLine adds 25 dashes in debug
   257  func AddDebugDashedLine() {
   258  	log.Entry().Debugf(strings.Repeat("-", 25))
   259  }
   260  
   261  /*******************************
   262   *	Structs for specific steps *
   263   *******************************/
   264  
   265  // AbapEnvironmentPullGitRepoOptions struct for the PullGitRepo piper step
   266  type AbapEnvironmentPullGitRepoOptions struct {
   267  	AbapEnvOptions  AbapEnvironmentOptions
   268  	RepositoryNames []string `json:"repositoryNames,omitempty"`
   269  }
   270  
   271  // AbapEnvironmentCheckoutBranchOptions struct for the CheckoutBranch piper step
   272  type AbapEnvironmentCheckoutBranchOptions struct {
   273  	AbapEnvOptions AbapEnvironmentOptions
   274  	RepositoryName string `json:"repositoryName,omitempty"`
   275  }
   276  
   277  // AbapEnvironmentRunATCCheckOptions struct for the RunATCCheck piper step
   278  type AbapEnvironmentRunATCCheckOptions struct {
   279  	AbapEnvOptions AbapEnvironmentOptions
   280  	AtcConfig      string `json:"atcConfig,omitempty"`
   281  }
   282  
   283  /********************************
   284   *	Structs for ABAP in general *
   285   ********************************/
   286  
   287  // AbapEnvironmentOptions contains cloud foundry fields and the host parameter for connections to ABAP Environment instances
   288  type AbapEnvironmentOptions struct {
   289  	Username          string `json:"username,omitempty"`
   290  	Password          string `json:"password,omitempty"`
   291  	Host              string `json:"host,omitempty"`
   292  	CfAPIEndpoint     string `json:"cfApiEndpoint,omitempty"`
   293  	CfOrg             string `json:"cfOrg,omitempty"`
   294  	CfSpace           string `json:"cfSpace,omitempty"`
   295  	CfServiceInstance string `json:"cfServiceInstance,omitempty"`
   296  	CfServiceKeyName  string `json:"cfServiceKeyName,omitempty"`
   297  }
   298  
   299  // AbapMetadata contains the URI of metadata files
   300  type AbapMetadata struct {
   301  	URI string `json:"uri"`
   302  }
   303  
   304  // ConnectionDetailsHTTP contains fields for HTTP connections including the XCSRF token
   305  type ConnectionDetailsHTTP struct {
   306  	Host       string
   307  	User       string `json:"user"`
   308  	Password   string `json:"password"`
   309  	URL        string `json:"url"`
   310  	XCsrfToken string `json:"xcsrftoken"`
   311  }
   312  
   313  // AbapError contains the error code and the error message for ABAP errors
   314  type AbapError struct {
   315  	Code    string           `json:"code"`
   316  	Message AbapErrorMessage `json:"message"`
   317  }
   318  
   319  // AbapErrorMessage contains the lanuage and value fields for ABAP errors
   320  type AbapErrorMessage struct {
   321  	Lang  string `json:"lang"`
   322  	Value string `json:"value"`
   323  }
   324  
   325  // AbapServiceKeyV8 contains the new format of an ABAP service key
   326  
   327  type AbapServiceKeyV8 struct {
   328  	Credentials AbapServiceKey `json:"credentials"`
   329  }
   330  
   331  // AbapServiceKey contains information about an ABAP service key
   332  type AbapServiceKey struct {
   333  	SapCloudService    string         `json:"sap.cloud.service"`
   334  	URL                string         `json:"url"`
   335  	SystemID           string         `json:"systemid"`
   336  	Abap               AbapConnection `json:"abap"`
   337  	Binding            AbapBinding    `json:"binding"`
   338  	PreserveHostHeader bool           `json:"preserve_host_header"`
   339  }
   340  
   341  // AbapConnection contains information about the ABAP connection for the ABAP endpoint
   342  type AbapConnection struct {
   343  	Username                         string `json:"username"`
   344  	Password                         string `json:"password"`
   345  	CommunicationScenarioID          string `json:"communication_scenario_id"`
   346  	CommunicationArrangementID       string `json:"communication_arrangement_id"`
   347  	CommunicationSystemID            string `json:"communication_system_id"`
   348  	CommunicationInboundUserID       string `json:"communication_inbound_user_id"`
   349  	CommunicationInboundUserAuthMode string `json:"communication_inbound_user_auth_mode"`
   350  }
   351  
   352  // AbapBinding contains information about service binding in Cloud Foundry
   353  type AbapBinding struct {
   354  	ID      string `json:"id"`
   355  	Type    string `json:"type"`
   356  	Version string `json:"version"`
   357  	Env     string `json:"env"`
   358  }
   359  
   360  /********************************
   361   *	Testing with a client mock  *
   362   ********************************/
   363  
   364  // ClientMock contains information about the client mock
   365  type ClientMock struct {
   366  	Token              string
   367  	Body               string
   368  	BodyList           []string
   369  	StatusCode         int
   370  	Error              error
   371  	NilResponse        bool
   372  	ErrorInsteadOfDump bool
   373  }
   374  
   375  // SetOptions sets clientOptions for a client mock
   376  func (c *ClientMock) SetOptions(opts piperhttp.ClientOptions) {}
   377  
   378  // SendRequest sets a HTTP response for a client mock
   379  func (c *ClientMock) SendRequest(method, url string, bdy io.Reader, hdr http.Header, cookies []*http.Cookie) (*http.Response, error) {
   380  
   381  	if c.NilResponse {
   382  		return nil, c.Error
   383  	}
   384  
   385  	var body []byte
   386  	if c.Body != "" {
   387  		body = []byte(c.Body)
   388  	} else {
   389  		if c.ErrorInsteadOfDump && len(c.BodyList) == 0 {
   390  			return nil, errors.New("No more bodies in the list")
   391  		}
   392  		bodyString := c.BodyList[len(c.BodyList)-1]
   393  		c.BodyList = c.BodyList[:len(c.BodyList)-1]
   394  		body = []byte(bodyString)
   395  	}
   396  	header := http.Header{}
   397  	header.Set("X-Csrf-Token", c.Token)
   398  	return &http.Response{
   399  		StatusCode: c.StatusCode,
   400  		Header:     header,
   401  		Body:       io.NopCloser(bytes.NewReader(body)),
   402  	}, c.Error
   403  }
   404  
   405  // DownloadFile : Empty file download
   406  func (c *ClientMock) DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error {
   407  	return nil
   408  }
   409  
   410  // AUtilsMock mock
   411  type AUtilsMock struct {
   412  	ReturnedConnectionDetailsHTTP ConnectionDetailsHTTP
   413  	ReturnedError                 error
   414  }
   415  
   416  // GetAbapCommunicationArrangementInfo mock
   417  func (autils *AUtilsMock) GetAbapCommunicationArrangementInfo(options AbapEnvironmentOptions, oDataURL string) (ConnectionDetailsHTTP, error) {
   418  	return autils.ReturnedConnectionDetailsHTTP, autils.ReturnedError
   419  }
   420  
   421  // GetPollIntervall mock
   422  func (autils *AUtilsMock) GetPollIntervall() time.Duration {
   423  	return 1 * time.Microsecond
   424  }
   425  
   426  // Cleanup to reset AUtilsMock
   427  func (autils *AUtilsMock) Cleanup() {
   428  	autils.ReturnedConnectionDetailsHTTP = ConnectionDetailsHTTP{}
   429  	autils.ReturnedError = nil
   430  }