mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-01-16 05:16:08 +02:00
f1234114be
* 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 commit1ee0bcd80d
, reversing changes made to3c4a99dfb0
. * try to fix error * try to fix error 2 * try to fix error 3 * align go.mod with master branch * revert go.mod to commit3c4a99d
* for test * revert test changes * new unit test * Revert "Revert "Merge branch 'master' into abap-environment-update-addon-product"" This reverts commit363c038001
. * 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>
475 lines
15 KiB
Go
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
|
|
}
|
|
}
|
|
}
|
|
}
|