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 }