1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-16 05:16:08 +02:00
sap-jenkins-library/cmd/abapLandscapePortalUpdateAddOnProduct.go
ranliii f1234114be
Abap environment update addon product (#4774)
* new Piper step abapEnvironmentUpdateAddOnProduct

* modified entity json format and some minor function changes

* modified groovy file for pipelineStageIntTests and addonDescriptor to be mandatory in yaml file

* sync with fork branch ranliii/abap-environment-update-addon-product

* added generated file

* fail the step as long as addon update not successful and unit tests

* added docu for the new step

* tried to fix groovy unit test

* tried to fix groovy unit test 2

* for test

* fixed error

* fixed error 2

* tried to fix groovy unit test error

* added groovy unit test for new Piper step

* tried to fix groovy unit test error

* tried to fix groovy unit test error 2

* changes after first review

* remove .DS_Store

* for test

* revert test relevant changes

* try to fix groovy test error

* try to fix groovy error

* 3rd try to fix groovy test error

* rewrite the failed groovy test

* small changes and try with timeout as well as poll interval

* changes for test

* revert test-related changes

* try to fix errors

* Revert "Merge branch 'master' into abap-environment-update-addon-product"

This reverts commit 1ee0bcd80d, reversing
changes made to 3c4a99dfb0.

* try to fix error

* try to fix error 2

* try to fix error 3

* align go.mod with master branch

* revert go.mod to commit 3c4a99d

* for test

* revert test changes

* new unit test

* Revert "Revert "Merge branch 'master' into abap-environment-update-addon-product""

This reverts commit 363c038001.

* go generate after merging master

---------

Co-authored-by: Jk1484 <35270240+Jk1484@users.noreply.github.com>
Co-authored-by: Ran Li <ran.li01@sap.com>
Co-authored-by: tiloKo <70266685+tiloKo@users.noreply.github.com>
2024-02-20 19:39:43 +01:00

475 lines
15 KiB
Go

package cmd
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/SAP/jenkins-library/pkg/abaputils"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/telemetry"
"golang.org/x/exp/slices"
)
const (
StatusComplete = "C"
StatusError = "E"
StatusInProgress = "I"
StatusScheduled = "S"
StatusAborted = "X"
maxRuntimeInMinute = time.Duration(120) * time.Minute
pollIntervalInSecond = time.Duration(30) * time.Second
)
type httpClient interface {
Do(*http.Request) (*http.Response, error)
}
type uaa struct {
CertUrl string `json:"certurl"`
ClientId string `json:"clientid"`
Certificate string `json:"certificate"`
Key string `json:"key"`
}
type serviceKey struct {
Url string `json:"url"`
Uaa uaa `json:"uaa"`
}
type accessTokenResp struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
}
type systemEntity struct {
SystemId string `json:"SystemId"`
SystemNumber string `json:"SystemNumber"`
ZoneId string `json:"zone_id"`
}
type reqEntity struct {
RequestId string `json:"RequestId"`
ZoneId string `json:"zone_id"`
Status string `json:"Status"`
SystemId string `json:"SystemId"`
}
type updateAddOnReq struct {
ProductName string `json:"productName"`
ProductVersion string `json:"productVersion"`
}
type updateAddOnResp struct {
RequestId string `json:"requestId"`
ZoneId string `json:"zoneId"`
Status string `json:"status"`
SystemId string `json:"systemId"`
}
var client, clientToken httpClient
var servKey serviceKey
func abapLandscapePortalUpdateAddOnProduct(config abapLandscapePortalUpdateAddOnProductOptions, telemetryData *telemetry.CustomData) {
client = &http.Client{}
if prepareErr := parseServiceKeyAndPrepareAccessTokenHttpClient(config.LandscapePortalAPIServiceKey, &clientToken, &servKey); prepareErr != nil {
err := fmt.Errorf("Failed to prepare credentials to get access token of LP API. Error: %v\n", prepareErr)
log.Entry().WithError(err).Fatal("step execution failed")
}
// Error situations should be bubbled up until they reach the line below which will then stop execution
// through the log.Entry().Fatal() call leading to an os.Exit(1) in the end.
err := runAbapLandscapePortalUpdateAddOnProduct(&config, client, clientToken, servKey, maxRuntimeInMinute, pollIntervalInSecond)
if err != nil {
log.Entry().WithError(err).Fatal("step execution failed")
}
}
func runAbapLandscapePortalUpdateAddOnProduct(config *abapLandscapePortalUpdateAddOnProductOptions, client httpClient, clientToken httpClient, servKey serviceKey, maxRuntimeInMinute time.Duration, pollIntervalInSecond time.Duration) error {
var systemId, reqId, reqStatus string
var getStatusReq http.Request
var err error
// get system
if getSystemErr := getSystemBySystemNumber(config, client, clientToken, servKey, &systemId); getSystemErr != nil {
err = fmt.Errorf("Failed to get system with systemNumber %v. Error: %v\n", config.AbapSystemNumber, getSystemErr)
return err
}
// update addon in the system
if updateAddOnErr := updateAddOn(config.AddonDescriptorFileName, client, clientToken, servKey, systemId, &reqId); updateAddOnErr != nil {
err = fmt.Errorf("Failed to update addon in the system with systemId %v. Error: %v\n", systemId, updateAddOnErr)
return err
}
// prepare http request to poll status of addon update
if prepareGetStatusHttpRequestErr := prepareGetStatusHttpRequest(clientToken, servKey, reqId, &getStatusReq); prepareGetStatusHttpRequestErr != nil {
err = fmt.Errorf("Failed to prepare http request to poll status of addon update request %v. Error: %v\n", reqId, prepareGetStatusHttpRequestErr)
return err
}
// keep polling request status until it reaches a final status or timeout
if waitToBeFinishedErr := waitToBeFinished(maxRuntimeInMinute, pollIntervalInSecond, client, &getStatusReq, reqId, &reqStatus); waitToBeFinishedErr != nil {
err = fmt.Errorf("Error occurred before a final status can be reached. Error: %v\n", waitToBeFinishedErr)
return err
}
// respond to the final status of addon update
if respondToUpdateAddOnFinalStatusErr := respondToUpdateAddOnFinalStatus(client, clientToken, servKey, reqId, reqStatus); respondToUpdateAddOnFinalStatusErr != nil {
err = fmt.Errorf("The final status of addon update is %v. Error: %v\n", reqStatus, respondToUpdateAddOnFinalStatusErr)
return err
}
return nil
}
// this function is used to parse service key JSON and prepare http client for access token
func parseServiceKeyAndPrepareAccessTokenHttpClient(servKeyJSON string, clientToken *httpClient, servKey *serviceKey) error {
// parse the service key from JSON string to struct
if parseServiceKeyErr := json.Unmarshal([]byte(servKeyJSON), servKey); parseServiceKeyErr != nil {
return parseServiceKeyErr
}
// configure http client with certificate authorization for getLPAPIAccessToken
certSource := servKey.Uaa.Certificate
keySource := servKey.Uaa.Key
certPem := strings.Replace(certSource, `\n`, "\n", -1)
keyPem := strings.Replace(keySource, `\n`, "\n", -1)
certificate, certErr := tls.X509KeyPair([]byte(certPem), []byte(keyPem))
if certErr != nil {
return certErr
}
*clientToken = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{certificate},
},
},
}
return nil
}
// this function is used to get access token of Landscape Portal API
func getLPAPIAccessToken(clientToken httpClient, servKey serviceKey) (string, error) {
authRawURL := servKey.Uaa.CertUrl + "/oauth/token"
// configure request body
reqBody := url.Values{}
reqBody.Set("grant_type", "client_credentials")
reqBody.Set("client_id", servKey.Uaa.ClientId)
encodedReqBody := reqBody.Encode()
// generate http request and configure header
req, reqErr := http.NewRequest(http.MethodPost, authRawURL, strings.NewReader(encodedReqBody))
if reqErr != nil {
return "", reqErr
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, getAccessTokenErr := clientToken.Do(req)
if getAccessTokenErr != nil {
return "", getAccessTokenErr
}
defer resp.Body.Close()
// error case of response status code being non 200
if resp.StatusCode != http.StatusOK {
err := fmt.Errorf("Unexpected response status %v received when getting access token of LP API.\n", resp.Status)
return "", err
}
// read and parse response body
respBody := accessTokenResp{}
if parseRespBodyErr := parseRespBody[accessTokenResp](resp, &respBody); parseRespBodyErr != nil {
return "", parseRespBodyErr
}
return respBody.AccessToken, nil
}
// this function is used to check the existence of integration test system
func getSystemBySystemNumber(config *abapLandscapePortalUpdateAddOnProductOptions, client httpClient, clientToken httpClient, servKey serviceKey, systemId *string) error {
accessToken, getAccessTokenErr := getLPAPIAccessToken(clientToken, servKey)
if getAccessTokenErr != nil {
return getAccessTokenErr
}
// define the raw url of the request and parse it into required form used in http.Request
getSystemRawURL := servKey.Url + "/api/systems/" + config.AbapSystemNumber
getSystemURL, urlParseErr := url.Parse(getSystemRawURL)
if urlParseErr != nil {
return urlParseErr
}
req := http.Request{
Method: http.MethodGet,
URL: getSystemURL,
Header: map[string][]string{
"Authorization": {"Bearer " + accessToken},
"Content-Type": {"application/json"},
"Accept": {"application/json"},
},
}
resp, getSystemErr := client.Do(&req)
if getSystemErr != nil {
return getSystemErr
}
defer resp.Body.Close()
// error case of response status code being non 200
if resp.StatusCode != http.StatusOK {
err := fmt.Errorf("Unexpected response status %v received when getting system with systemNumber %v.\n", resp.Status, config.AbapSystemNumber)
return err
}
// read and parse response body
respBody := systemEntity{}
if parseRespBodyErr := parseRespBody[systemEntity](resp, &respBody); parseRespBodyErr != nil {
return parseRespBodyErr
}
*systemId = respBody.SystemId
fmt.Printf("Successfully got ABAP system with systemNumber %v and systemId %v.\n", respBody.SystemNumber, respBody.SystemId)
return nil
}
// this function is used to define and maintain the request body of querying status of addon update request
func prepareGetStatusHttpRequest(clientToken httpClient, servKey serviceKey, reqId string, getStatusReq *http.Request) error {
accessToken, getAccessTokenErr := getLPAPIAccessToken(clientToken, servKey)
if getAccessTokenErr != nil {
return getAccessTokenErr
}
// define the raw url of the request and parse it into required form used in http.Request
getStatusRawURL := servKey.Url + "/api/requests/" + reqId
getStatusURL, urlParseErr := url.Parse(getStatusRawURL)
if urlParseErr != nil {
return urlParseErr
}
req := http.Request{
Method: http.MethodGet,
URL: getStatusURL,
Header: map[string][]string{
"Authorization": {"Bearer " + accessToken},
"Content-Type": {"application/json"},
"Accept": {"application/json"},
},
}
// store the req in the global variable for later usage
*getStatusReq = req
return nil
}
// this function is used to poll status of addon update request and maintain the status
func pollStatusOfUpdateAddOn(client httpClient, req *http.Request, reqId string, status *string) error {
resp, getStatusErr := client.Do(req)
if getStatusErr != nil {
return getStatusErr
}
defer resp.Body.Close()
// error case of response status code being non 200
if resp.StatusCode != http.StatusOK {
err := fmt.Errorf("Unexpected response status %v received when polling status of request %v.\n", resp.Status, reqId)
return err
}
// read and parse response body
respBody := reqEntity{}
if parseRespBodyErr := parseRespBody[reqEntity](resp, &respBody); parseRespBodyErr != nil {
return parseRespBodyErr
}
*status = respBody.Status
fmt.Printf("Successfully polled status %v of request %v.\n", respBody.Status, respBody.RequestId)
return nil
}
// this function is used to update addon
func updateAddOn(addOnFileName string, client httpClient, clientToken httpClient, servKey serviceKey, systemId string, reqId *string) error {
accessToken, getAccessTokenErr := getLPAPIAccessToken(clientToken, servKey)
if getAccessTokenErr != nil {
return getAccessTokenErr
}
// read productName and productVersion from addon.yml
addOnDescriptor, readAddOnErr := abaputils.ReadAddonDescriptor(addOnFileName)
if readAddOnErr != nil {
return readAddOnErr
}
// define the raw url of the request and parse it into required form used in http.Request
updateAddOnRawURL := servKey.Url + "/api/systems/" + systemId + "/deployProduct"
// define the request body as a struct
reqBody := updateAddOnReq{
ProductName: addOnDescriptor.AddonProduct,
ProductVersion: addOnDescriptor.AddonVersionYAML,
}
// encode the request body to JSON
var reqBuff bytes.Buffer
json.NewEncoder(&reqBuff).Encode(reqBody)
req, reqErr := http.NewRequest(http.MethodPost, updateAddOnRawURL, &reqBuff)
if reqErr != nil {
return reqErr
}
req.Header = map[string][]string{
"Authorization": {"Bearer " + accessToken},
"Content-Type": {"application/json"},
"Accept": {"application/json"},
}
resp, updateAddOnErr := client.Do(req)
if updateAddOnErr != nil {
return updateAddOnErr
}
defer resp.Body.Close()
// error case of response status code being non 200
if resp.StatusCode != http.StatusOK {
err := fmt.Errorf("Unexpected response status %v received when updating addon in system with systemId %v.\n", resp.Status, systemId)
return err
}
// read and parse response body
respBody := updateAddOnResp{}
if parseRespBodyErr := parseRespBody[updateAddOnResp](resp, &respBody); parseRespBodyErr != nil {
return parseRespBodyErr
}
*reqId = respBody.RequestId
fmt.Printf("Successfully triggered addon update in system with systemId %v, the returned request id is %v.\n", systemId, respBody.RequestId)
return nil
}
// this function is used to cancel addon update
func cancelUpdateAddOn(client httpClient, clientToken httpClient, servKey serviceKey, reqId string) error {
accessToken, getAccessTokenErr := getLPAPIAccessToken(clientToken, servKey)
if getAccessTokenErr != nil {
return getAccessTokenErr
}
// define the raw url of the request and parse it into required form used in http.Request
cancelUpdateAddOnRawURL := servKey.Url + "/api/requests/" + reqId
cancelUpdateAddOnURL, urlParseErr := url.Parse(cancelUpdateAddOnRawURL)
if urlParseErr != nil {
return urlParseErr
}
req := http.Request{
Method: http.MethodDelete,
URL: cancelUpdateAddOnURL,
Header: map[string][]string{
"Authorization": {"Bearer " + accessToken},
"Content-Type": {"application/json"},
"Accept": {"application/json"},
},
}
resp, cancelUpdateAddOnErr := client.Do(&req)
if cancelUpdateAddOnErr != nil {
return cancelUpdateAddOnErr
}
defer resp.Body.Close()
// error case of response status code being non 204
if resp.StatusCode != http.StatusNoContent {
err := fmt.Errorf("Unexpected response status %v received when canceling addon update request %v.\n", resp.Status, reqId)
return err
}
fmt.Printf("Successfully canceled addon update request %v.\n", reqId)
return nil
}
// this function is used to respond to a final status of addon update
func respondToUpdateAddOnFinalStatus(client httpClient, clientToken httpClient, servKey serviceKey, reqId string, status string) error {
switch status {
case StatusComplete:
fmt.Println("Addon update succeeded.")
case StatusError:
fmt.Println("Addon update failed and will be canceled.")
if cancelUpdateAddOnErr := cancelUpdateAddOn(client, clientToken, servKey, reqId); cancelUpdateAddOnErr != nil {
err := fmt.Errorf("Failed to cancel addon update. Error: %v\n", cancelUpdateAddOnErr)
return err
}
err := fmt.Errorf("Addon update failed.\n")
return err
case StatusAborted:
fmt.Println("Addon update was aborted.")
err := fmt.Errorf("Addon update was aborted.\n")
return err
}
return nil
}
// this function is used to parse response body of http request
func parseRespBody[T comparable](resp *http.Response, respBody *T) error {
respBodyRaw, readRespErr := io.ReadAll(resp.Body)
if readRespErr != nil {
return readRespErr
}
if decodeRespBodyErr := json.Unmarshal(respBodyRaw, &respBody); decodeRespBodyErr != nil {
return decodeRespBodyErr
}
return nil
}
// this function is used to wait for a final status/timeout
func waitToBeFinished(maxRuntimeInMinute time.Duration, pollIntervalInSecond time.Duration, client httpClient, getStatusReq *http.Request, reqId string, reqStatus *string) error {
timeout := time.After(maxRuntimeInMinute)
ticker := time.Tick(pollIntervalInSecond)
reqFinalStatus := []string{StatusComplete, StatusError, StatusAborted}
for {
select {
case <-timeout:
return fmt.Errorf("Timed out: max runtime %v reached.", maxRuntimeInMinute)
case <-ticker:
if pollStatusOfUpdateAddOnErr := pollStatusOfUpdateAddOn(client, getStatusReq, reqId, reqStatus); pollStatusOfUpdateAddOnErr != nil {
err := fmt.Errorf("Error happened when waiting for the addon update request %v to reach a final status. Error: %v\n", reqId, pollStatusOfUpdateAddOnErr)
return err
}
if !slices.Contains(reqFinalStatus, *reqStatus) {
fmt.Printf("Addon update request %v is still in progress, will poll the status in %v.\n", reqId, pollIntervalInSecond)
} else {
return nil
}
}
}
}