You've already forked sap-jenkins-library
mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-09-16 09:26:22 +02:00
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 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>
This commit is contained in:
474
cmd/abapLandscapePortalUpdateAddOnProduct.go
Normal file
474
cmd/abapLandscapePortalUpdateAddOnProduct.go
Normal file
@@ -0,0 +1,474 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
185
cmd/abapLandscapePortalUpdateAddOnProduct_generated.go
Normal file
185
cmd/abapLandscapePortalUpdateAddOnProduct_generated.go
Normal file
@@ -0,0 +1,185 @@
|
||||
// Code generated by piper's step-generator. DO NOT EDIT.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/SAP/jenkins-library/pkg/config"
|
||||
"github.com/SAP/jenkins-library/pkg/log"
|
||||
"github.com/SAP/jenkins-library/pkg/splunk"
|
||||
"github.com/SAP/jenkins-library/pkg/telemetry"
|
||||
"github.com/SAP/jenkins-library/pkg/validation"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type abapLandscapePortalUpdateAddOnProductOptions struct {
|
||||
LandscapePortalAPIServiceKey string `json:"landscapePortalAPIServiceKey,omitempty"`
|
||||
AbapSystemNumber string `json:"abapSystemNumber,omitempty"`
|
||||
AddonDescriptorFileName string `json:"addonDescriptorFileName,omitempty"`
|
||||
}
|
||||
|
||||
// AbapLandscapePortalUpdateAddOnProductCommand Update the AddOn product in SAP BTP ABAP Environment system of Landscape Portal
|
||||
func AbapLandscapePortalUpdateAddOnProductCommand() *cobra.Command {
|
||||
const STEP_NAME = "abapLandscapePortalUpdateAddOnProduct"
|
||||
|
||||
metadata := abapLandscapePortalUpdateAddOnProductMetadata()
|
||||
var stepConfig abapLandscapePortalUpdateAddOnProductOptions
|
||||
var startTime time.Time
|
||||
var logCollector *log.CollectorHook
|
||||
var splunkClient *splunk.Splunk
|
||||
telemetryClient := &telemetry.Telemetry{}
|
||||
|
||||
var createAbapLandscapePortalUpdateAddOnProductCmd = &cobra.Command{
|
||||
Use: STEP_NAME,
|
||||
Short: "Update the AddOn product in SAP BTP ABAP Environment system of Landscape Portal",
|
||||
Long: `This step describes the AddOn product update in SAP BTP ABAP Environment system of Landscape Portal`,
|
||||
PreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
startTime = time.Now()
|
||||
log.SetStepName(STEP_NAME)
|
||||
log.SetVerbose(GeneralConfig.Verbose)
|
||||
|
||||
GeneralConfig.GitHubAccessTokens = ResolveAccessTokens(GeneralConfig.GitHubTokens)
|
||||
|
||||
path, _ := os.Getwd()
|
||||
fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path}
|
||||
log.RegisterHook(fatalHook)
|
||||
|
||||
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
|
||||
if err != nil {
|
||||
log.SetErrorCategory(log.ErrorConfiguration)
|
||||
return err
|
||||
}
|
||||
log.RegisterSecret(stepConfig.LandscapePortalAPIServiceKey)
|
||||
|
||||
if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 {
|
||||
sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID)
|
||||
log.RegisterHook(&sentryHook)
|
||||
}
|
||||
|
||||
if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 || len(GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint) > 0 {
|
||||
splunkClient = &splunk.Splunk{}
|
||||
logCollector = &log.CollectorHook{CorrelationID: GeneralConfig.CorrelationID}
|
||||
log.RegisterHook(logCollector)
|
||||
}
|
||||
|
||||
if err = log.RegisterANSHookIfConfigured(GeneralConfig.CorrelationID); err != nil {
|
||||
log.Entry().WithError(err).Warn("failed to set up SAP Alert Notification Service log hook")
|
||||
}
|
||||
|
||||
validation, err := validation.New(validation.WithJSONNamesForStructFields(), validation.WithPredefinedErrorMessages())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = validation.ValidateStruct(stepConfig); err != nil {
|
||||
log.SetErrorCategory(log.ErrorConfiguration)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
stepTelemetryData := telemetry.CustomData{}
|
||||
stepTelemetryData.ErrorCode = "1"
|
||||
handler := func() {
|
||||
config.RemoveVaultSecretFiles()
|
||||
stepTelemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds())
|
||||
stepTelemetryData.ErrorCategory = log.GetErrorCategory().String()
|
||||
stepTelemetryData.PiperCommitHash = GitCommit
|
||||
telemetryClient.SetData(&stepTelemetryData)
|
||||
telemetryClient.Send()
|
||||
if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 {
|
||||
splunkClient.Initialize(GeneralConfig.CorrelationID,
|
||||
GeneralConfig.HookConfig.SplunkConfig.Dsn,
|
||||
GeneralConfig.HookConfig.SplunkConfig.Token,
|
||||
GeneralConfig.HookConfig.SplunkConfig.Index,
|
||||
GeneralConfig.HookConfig.SplunkConfig.SendLogs)
|
||||
splunkClient.Send(telemetryClient.GetData(), logCollector)
|
||||
}
|
||||
if len(GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint) > 0 {
|
||||
splunkClient.Initialize(GeneralConfig.CorrelationID,
|
||||
GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint,
|
||||
GeneralConfig.HookConfig.SplunkConfig.ProdCriblToken,
|
||||
GeneralConfig.HookConfig.SplunkConfig.ProdCriblIndex,
|
||||
GeneralConfig.HookConfig.SplunkConfig.SendLogs)
|
||||
splunkClient.Send(telemetryClient.GetData(), logCollector)
|
||||
}
|
||||
}
|
||||
log.DeferExitHandler(handler)
|
||||
defer handler()
|
||||
telemetryClient.Initialize(GeneralConfig.NoTelemetry, STEP_NAME, GeneralConfig.HookConfig.PendoConfig.Token)
|
||||
abapLandscapePortalUpdateAddOnProduct(stepConfig, &stepTelemetryData)
|
||||
stepTelemetryData.ErrorCode = "0"
|
||||
log.Entry().Info("SUCCESS")
|
||||
},
|
||||
}
|
||||
|
||||
addAbapLandscapePortalUpdateAddOnProductFlags(createAbapLandscapePortalUpdateAddOnProductCmd, &stepConfig)
|
||||
return createAbapLandscapePortalUpdateAddOnProductCmd
|
||||
}
|
||||
|
||||
func addAbapLandscapePortalUpdateAddOnProductFlags(cmd *cobra.Command, stepConfig *abapLandscapePortalUpdateAddOnProductOptions) {
|
||||
cmd.Flags().StringVar(&stepConfig.LandscapePortalAPIServiceKey, "landscapePortalAPIServiceKey", os.Getenv("PIPER_landscapePortalAPIServiceKey"), "Service key JSON string to access the Landscape Portal Access API")
|
||||
cmd.Flags().StringVar(&stepConfig.AbapSystemNumber, "abapSystemNumber", os.Getenv("PIPER_abapSystemNumber"), "System Number of the abap integration test system")
|
||||
cmd.Flags().StringVar(&stepConfig.AddonDescriptorFileName, "addonDescriptorFileName", `addon.yml`, "File name of the YAML file which describes the Product Version and corresponding Software Component Versions")
|
||||
|
||||
cmd.MarkFlagRequired("landscapePortalAPIServiceKey")
|
||||
cmd.MarkFlagRequired("abapSystemNumber")
|
||||
cmd.MarkFlagRequired("addonDescriptorFileName")
|
||||
}
|
||||
|
||||
// retrieve step metadata
|
||||
func abapLandscapePortalUpdateAddOnProductMetadata() config.StepData {
|
||||
var theMetaData = config.StepData{
|
||||
Metadata: config.StepMetadata{
|
||||
Name: "abapLandscapePortalUpdateAddOnProduct",
|
||||
Aliases: []config.Alias{},
|
||||
Description: "Update the AddOn product in SAP BTP ABAP Environment system of Landscape Portal",
|
||||
},
|
||||
Spec: config.StepSpec{
|
||||
Inputs: config.StepInputs{
|
||||
Secrets: []config.StepSecrets{
|
||||
{Name: "landscapePortalAPICredentialsId", Description: "Jenkins secret text credential ID containing the service key to access the Landscape Portal Access API", Type: "jenkins"},
|
||||
},
|
||||
Parameters: []config.StepParameters{
|
||||
{
|
||||
Name: "landscapePortalAPIServiceKey",
|
||||
ResourceRef: []config.ResourceReference{
|
||||
{
|
||||
Name: "landscapePortalAPICredentialsId",
|
||||
Param: "landscapePortalAPIServiceKey",
|
||||
Type: "secret",
|
||||
},
|
||||
},
|
||||
Scope: []string{"PARAMETERS"},
|
||||
Type: "string",
|
||||
Mandatory: true,
|
||||
Aliases: []config.Alias{},
|
||||
Default: os.Getenv("PIPER_landscapePortalAPIServiceKey"),
|
||||
},
|
||||
{
|
||||
Name: "abapSystemNumber",
|
||||
ResourceRef: []config.ResourceReference{},
|
||||
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
|
||||
Type: "string",
|
||||
Mandatory: true,
|
||||
Aliases: []config.Alias{},
|
||||
Default: os.Getenv("PIPER_abapSystemNumber"),
|
||||
},
|
||||
{
|
||||
Name: "addonDescriptorFileName",
|
||||
ResourceRef: []config.ResourceReference{},
|
||||
Scope: []string{"PARAMETERS", "STAGES", "STEPS", "GENERAL"},
|
||||
Type: "string",
|
||||
Mandatory: true,
|
||||
Aliases: []config.Alias{},
|
||||
Default: `addon.yml`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return theMetaData
|
||||
}
|
20
cmd/abapLandscapePortalUpdateAddOnProduct_generated_test.go
Normal file
20
cmd/abapLandscapePortalUpdateAddOnProduct_generated_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
//go:build unit
|
||||
// +build unit
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAbapLandscapePortalUpdateAddOnProductCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCmd := AbapLandscapePortalUpdateAddOnProductCommand()
|
||||
|
||||
// only high level testing performed - details are tested in step generation procedure
|
||||
assert.Equal(t, "abapLandscapePortalUpdateAddOnProduct", testCmd.Use, "command name incorrect")
|
||||
|
||||
}
|
655
cmd/abapLandscapePortalUpdateAddOnProduct_test.go
Normal file
655
cmd/abapLandscapePortalUpdateAddOnProduct_test.go
Normal file
@@ -0,0 +1,655 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
resBodyJSON_token = `{"access_token": "some-access-token", "token_type": "bearer", "expires_in": 86400, "scope": "some-scope"}`
|
||||
resBodyJSON_sys = `{"SystemId": "some-system-id", "SystemNumber": "some-system-number", "zone_id": "some-zone-id"}`
|
||||
resBodyJSON_req_S = `{"RequestId": "some-request-id","zone_id": "some-zone-id", "Status": "S", "SystemId": "some-system-id"}`
|
||||
resBodyJSON_req_I = `{"RequestId": "some-request-id","zone_id": "some-zone-id", "Status": "I", "SystemId": "some-system-id"}`
|
||||
resBodyJSON_req_C = `{"RequestId": "some-request-id","zone_id": "some-zone-id", "Status": "C", "SystemId": "some-system-id"}`
|
||||
resBodyJSON_req_E = `{"RequestId": "some-request-id","zone_id": "some-zone-id", "Status": "E", "SystemId": "some-system-id"}`
|
||||
resBodyJSON_req_X = `{"RequestId": "some-request-id","zone_id": "some-zone-id", "Status": "X", "SystemId": "some-system-id"}`
|
||||
)
|
||||
|
||||
type mockClient struct {
|
||||
DoFunc func(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
var GetDoFunc func(req *http.Request) (*http.Response, error)
|
||||
|
||||
var testUaa = uaa{
|
||||
CertUrl: "https://some-cert-url.com",
|
||||
ClientId: "some-client-id",
|
||||
Certificate: "-----BEGIN CERTIFICATE-----\nMIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw\nDgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow\nEjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d\n7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B\n5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr\nBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1\nNDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l\nWf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc\n6MF9+Yw1Yy0t\n-----END CERTIFICATE-----",
|
||||
Key: "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49\nAwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q\nEKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==\n-----END EC PRIVATE KEY-----",
|
||||
}
|
||||
var mockServKey = serviceKey{
|
||||
Url: "https://some-url.com",
|
||||
Uaa: testUaa,
|
||||
}
|
||||
|
||||
var mockServiceKeyJSON = `{
|
||||
"url": "https://some-url.com",
|
||||
"uaa": {
|
||||
"clientid": "some-client-id",
|
||||
"url": "https://some-uaa-url.com",
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\nMIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw\nDgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow\nEjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d\n7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B\n5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr\nBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1\nNDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l\nWf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc\n6MF9+Yw1Yy0t\n-----END CERTIFICATE-----",
|
||||
"certurl": "https://some-cert-url.com",
|
||||
"key": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49\nAwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q\nEKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==\n-----END EC PRIVATE KEY-----"
|
||||
},
|
||||
"vendor": "SAP"
|
||||
}`
|
||||
|
||||
var mockUpdateAddOnConfig = abapLandscapePortalUpdateAddOnProductOptions{
|
||||
LandscapePortalAPIServiceKey: mockServiceKeyJSON,
|
||||
AbapSystemNumber: "some-system-number",
|
||||
AddonDescriptorFileName: "addon.yml",
|
||||
}
|
||||
|
||||
func (m *mockClient) Do(req *http.Request) (*http.Response, error) {
|
||||
return GetDoFunc(req)
|
||||
}
|
||||
|
||||
func init() {
|
||||
client = &mockClient{}
|
||||
clientToken = &mockClient{}
|
||||
}
|
||||
|
||||
func TestParseServiceKeyAndPrepareAccessTokenHttpClient(t *testing.T) {
|
||||
t.Run("Successfully parsed service key", func(t *testing.T) {
|
||||
var testServKey serviceKey
|
||||
clientParseServKey := clientToken
|
||||
|
||||
err := parseServiceKeyAndPrepareAccessTokenHttpClient(mockUpdateAddOnConfig.LandscapePortalAPIServiceKey, &clientParseServKey, &testServKey)
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "https://some-url.com", testServKey.Url)
|
||||
assert.Equal(t, "some-client-id", testServKey.Uaa.ClientId)
|
||||
assert.Equal(t, "https://some-cert-url.com", testServKey.Uaa.CertUrl)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetLPAPIAccessToken(t *testing.T) {
|
||||
t.Run("Successfully got LP API access token", func(t *testing.T) {
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
resBodyReader := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
res, err := getLPAPIAccessToken(clientToken, mockServKey)
|
||||
|
||||
assert.Equal(t, "some-access-token", res)
|
||||
assert.Equal(t, nil, err)
|
||||
})
|
||||
|
||||
t.Run("Failed to get LP API access token", func(t *testing.T) {
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
return nil, fmt.Errorf("Failed to get access token.")
|
||||
}
|
||||
res, err := getLPAPIAccessToken(clientToken, mockServKey)
|
||||
|
||||
assert.Equal(t, "", res)
|
||||
assert.Equal(t, fmt.Errorf("Failed to get access token."), err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSystemBySystemNumber(t *testing.T) {
|
||||
reqUrl_token := mockServKey.Uaa.CertUrl + "/oauth/token"
|
||||
reqUrl_sys := mockServKey.Url + "/api/systems/" + mockUpdateAddOnConfig.AbapSystemNumber
|
||||
|
||||
t.Run("Successfully got ABAP system", func(t *testing.T) {
|
||||
var testSysId string
|
||||
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.String() == reqUrl_token {
|
||||
resBodyReader_token := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_sys {
|
||||
resBodyReader_sys := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_sys)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_sys,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("some-unknown-error")
|
||||
}
|
||||
|
||||
err := getSystemBySystemNumber(&mockUpdateAddOnConfig, client, clientToken, mockServKey, &testSysId)
|
||||
|
||||
assert.Equal(t, "some-system-id", testSysId)
|
||||
assert.Equal(t, nil, err)
|
||||
})
|
||||
|
||||
t.Run("Failed to get ABAP system", func(t *testing.T) {
|
||||
var testSysId string
|
||||
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.String() == reqUrl_token {
|
||||
resBodyReader_token := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_sys {
|
||||
return nil, fmt.Errorf("Failed to get ABAP system.")
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("some-unknown-error")
|
||||
}
|
||||
|
||||
err := getSystemBySystemNumber(&mockUpdateAddOnConfig, client, clientToken, mockServKey, &testSysId)
|
||||
|
||||
assert.Equal(t, "", testSysId)
|
||||
assert.Equal(t, fmt.Errorf("Failed to get ABAP system."), err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateAddOn(t *testing.T) {
|
||||
testSysId := "some-system-id"
|
||||
reqUrl_token := mockServKey.Uaa.CertUrl + "/oauth/token"
|
||||
reqUrl_update := mockServKey.Url + "/api/systems/" + testSysId + "/deployProduct"
|
||||
|
||||
t.Run("Successfully updated addon", func(t *testing.T) {
|
||||
// write addon.yml
|
||||
dir := t.TempDir()
|
||||
oldCWD, _ := os.Getwd()
|
||||
_ = os.Chdir(dir)
|
||||
// clean up tmp dir
|
||||
defer func() {
|
||||
_ = os.Chdir(oldCWD)
|
||||
}()
|
||||
|
||||
addonYML := `addonProduct: some-addon-product
|
||||
addonVersion: 1.0.0
|
||||
`
|
||||
addonYMLBytes := []byte(addonYML)
|
||||
os.WriteFile("addon.yml", addonYMLBytes, 0644)
|
||||
|
||||
// mock Do func
|
||||
var testReqId string
|
||||
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.String() == reqUrl_token {
|
||||
resBodyReader_token := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_update {
|
||||
resBodyReader_req_S := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_req_S)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_req_S,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("some-unknown-error")
|
||||
}
|
||||
|
||||
err := updateAddOn(mockUpdateAddOnConfig.AddonDescriptorFileName, client, clientToken, mockServKey, testSysId, &testReqId)
|
||||
|
||||
assert.Equal(t, "some-request-id", testReqId)
|
||||
assert.Equal(t, nil, err)
|
||||
})
|
||||
|
||||
t.Run("Failed to update addon", func(t *testing.T) {
|
||||
// write addon.yml
|
||||
dir := t.TempDir()
|
||||
oldCWD, _ := os.Getwd()
|
||||
_ = os.Chdir(dir)
|
||||
// clean up tmp dir
|
||||
defer func() {
|
||||
_ = os.Chdir(oldCWD)
|
||||
}()
|
||||
|
||||
addonYML := `addonProduct: some-addon-product
|
||||
addonVersion: 1.0.0
|
||||
`
|
||||
addonYMLBytes := []byte(addonYML)
|
||||
os.WriteFile("addon.yml", addonYMLBytes, 0644)
|
||||
|
||||
// mock Do func
|
||||
var testReqId string
|
||||
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.String() == reqUrl_token {
|
||||
resBodyReader_token := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_update {
|
||||
return nil, fmt.Errorf("Failed to update addon.")
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("some-unknown-error")
|
||||
}
|
||||
|
||||
err := updateAddOn(mockUpdateAddOnConfig.AddonDescriptorFileName, client, clientToken, mockServKey, testSysId, &testReqId)
|
||||
|
||||
assert.Equal(t, "", testReqId)
|
||||
assert.Equal(t, fmt.Errorf("Failed to update addon."), err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPollStatusOfUpdateAddOn(t *testing.T) {
|
||||
var testReq http.Request
|
||||
|
||||
testReqId := "some-request-id"
|
||||
reqUrl_token := mockServKey.Uaa.CertUrl + "/oauth/token"
|
||||
reqUrl_pollAndCancel := mockServKey.Url + "/api/requests/" + testReqId
|
||||
|
||||
t.Run("Successfully polled request status", func(t *testing.T) {
|
||||
var testStatus string
|
||||
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.String() == reqUrl_token {
|
||||
resBodyReader_token := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_pollAndCancel {
|
||||
resBodyReader_pollStatus := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_req_I)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_pollStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("some-unknown-error")
|
||||
}
|
||||
|
||||
err1 := prepareGetStatusHttpRequest(clientToken, mockServKey, testReqId, &testReq)
|
||||
err2 := pollStatusOfUpdateAddOn(client, &testReq, testReqId, &testStatus)
|
||||
|
||||
assert.Equal(t, "I", testStatus)
|
||||
assert.Equal(t, nil, err1)
|
||||
assert.Equal(t, nil, err2)
|
||||
})
|
||||
|
||||
t.Run("Failed to poll request status", func(t *testing.T) {
|
||||
var testStatus string
|
||||
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.String() == reqUrl_token {
|
||||
resBodyReader_token := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_pollAndCancel {
|
||||
return nil, fmt.Errorf("Failed to poll status.")
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("some-unknown-error")
|
||||
}
|
||||
|
||||
err1 := prepareGetStatusHttpRequest(clientToken, mockServKey, testReqId, &testReq)
|
||||
err2 := pollStatusOfUpdateAddOn(client, &testReq, testReqId, &testStatus)
|
||||
|
||||
assert.Equal(t, "", testStatus)
|
||||
assert.Equal(t, nil, err1)
|
||||
assert.Equal(t, fmt.Errorf("Failed to poll status."), err2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCancelUpdateAddOn(t *testing.T) {
|
||||
testReqId := "some-request-id"
|
||||
reqUrl_token := mockServKey.Uaa.CertUrl + "/oauth/token"
|
||||
reqUrl_pollAndCancel := mockServKey.Url + "/api/requests/" + testReqId
|
||||
|
||||
t.Run("Successfully canceled addon update", func(t *testing.T) {
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
resBodyReader_token := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
if req.URL.String() == reqUrl_token {
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_pollAndCancel {
|
||||
resBodyReader_cancelUpdate := io.NopCloser(nil)
|
||||
return &http.Response{
|
||||
StatusCode: 204,
|
||||
Body: resBodyReader_cancelUpdate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("some-unknown-error")
|
||||
}
|
||||
|
||||
err := cancelUpdateAddOn(client, clientToken, mockServKey, testReqId)
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
})
|
||||
|
||||
t.Run("Failed to cancel addon update", func(t *testing.T) {
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.String() == reqUrl_token {
|
||||
resBodyReader_token := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_pollAndCancel {
|
||||
return nil, fmt.Errorf("Failed to cancel addon update.")
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("some-unknown-error")
|
||||
}
|
||||
|
||||
err := cancelUpdateAddOn(client, clientToken, mockServKey, testReqId)
|
||||
|
||||
assert.Equal(t, fmt.Errorf("Failed to cancel addon update."), err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunAbapLandscapePortalUpdateAddOnProduct(t *testing.T) {
|
||||
reqUrl_token := mockServKey.Uaa.CertUrl + "/oauth/token"
|
||||
reqUrl_sys := mockServKey.Url + "/api/systems/" + mockUpdateAddOnConfig.AbapSystemNumber
|
||||
reqUrl_update := mockServKey.Url + "/api/systems/" + "some-system-id" + "/deployProduct"
|
||||
reqUrl_pollAndCancel := mockServKey.Url + "/api/requests/" + "some-request-id"
|
||||
|
||||
t.Run("Successfully ran update addon in ABAP system", func(t *testing.T) {
|
||||
// write addon.yml
|
||||
dir := t.TempDir()
|
||||
oldCWD, _ := os.Getwd()
|
||||
_ = os.Chdir(dir)
|
||||
// clean up tmp dir
|
||||
defer func() {
|
||||
_ = os.Chdir(oldCWD)
|
||||
}()
|
||||
|
||||
addonYML := `addonProduct: some-addon-product
|
||||
addonVersion: 1.0.0
|
||||
`
|
||||
addonYMLBytes := []byte(addonYML)
|
||||
os.WriteFile("addon.yml", addonYMLBytes, 0644)
|
||||
|
||||
// mock Do func
|
||||
maxRuntimeInMinute := time.Duration(1) * time.Minute
|
||||
pollIntervalInSecond := time.Duration(1) * time.Second
|
||||
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.String() == reqUrl_token {
|
||||
resBodyReader_token := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_sys {
|
||||
resBodyReader_sys := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_sys)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_sys,
|
||||
}, nil
|
||||
}
|
||||
if req.URL.String() == reqUrl_update {
|
||||
resBodyReader_update := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_req_S)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_update,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_pollAndCancel {
|
||||
resBodyReader_pollStatus_C := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_req_C)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_pollStatus_C,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("some-unknown-error")
|
||||
}
|
||||
|
||||
// execution and assertion
|
||||
err := runAbapLandscapePortalUpdateAddOnProduct(&mockUpdateAddOnConfig, client, clientToken, mockServKey, maxRuntimeInMinute, pollIntervalInSecond)
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
})
|
||||
|
||||
t.Run("Update addon ended in error", func(t *testing.T) {
|
||||
// write addon.yml
|
||||
dir := t.TempDir()
|
||||
oldCWD, _ := os.Getwd()
|
||||
_ = os.Chdir(dir)
|
||||
// clean up tmp dir
|
||||
defer func() {
|
||||
_ = os.Chdir(oldCWD)
|
||||
}()
|
||||
|
||||
addonYML := `addonProduct: some-addon-product
|
||||
addonVersion: 1.0.0
|
||||
`
|
||||
addonYMLBytes := []byte(addonYML)
|
||||
os.WriteFile("addon.yml", addonYMLBytes, 0644)
|
||||
|
||||
// mock Do func
|
||||
maxRuntimeInMinute := time.Duration(1) * time.Minute
|
||||
pollIntervalInSecond := time.Duration(1) * time.Second
|
||||
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.String() == reqUrl_token {
|
||||
resBodyReader_token := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_sys {
|
||||
resBodyReader_sys := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_sys)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_sys,
|
||||
}, nil
|
||||
}
|
||||
if req.URL.String() == reqUrl_update {
|
||||
resBodyReader_update := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_req_S)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_update,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_pollAndCancel && req.Method == "GET" {
|
||||
resBodyReader_pollStatus_E := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_req_E)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_pollStatus_E,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_pollAndCancel && req.Method == "DELETE" {
|
||||
resBodyReader_cancelUpdate := io.NopCloser(nil)
|
||||
return &http.Response{
|
||||
StatusCode: 204,
|
||||
Body: resBodyReader_cancelUpdate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("some-unknown-error")
|
||||
}
|
||||
|
||||
// execution and assertion
|
||||
expectedErr1 := fmt.Errorf("Addon update failed.\n")
|
||||
expectedErr2 := fmt.Errorf("The final status of addon update is E. Error: %v\n", expectedErr1)
|
||||
|
||||
err := runAbapLandscapePortalUpdateAddOnProduct(&mockUpdateAddOnConfig, client, clientToken, mockServKey, maxRuntimeInMinute, pollIntervalInSecond)
|
||||
|
||||
assert.Equal(t, expectedErr2, err)
|
||||
})
|
||||
|
||||
t.Run("Update addon was aborted", func(t *testing.T) {
|
||||
// write addon.yml
|
||||
dir := t.TempDir()
|
||||
oldCWD, _ := os.Getwd()
|
||||
_ = os.Chdir(dir)
|
||||
// clean up tmp dir
|
||||
defer func() {
|
||||
_ = os.Chdir(oldCWD)
|
||||
}()
|
||||
|
||||
addonYML := `addonProduct: some-addon-product
|
||||
addonVersion: 1.0.0
|
||||
`
|
||||
addonYMLBytes := []byte(addonYML)
|
||||
os.WriteFile("addon.yml", addonYMLBytes, 0644)
|
||||
|
||||
// mock Do func
|
||||
maxRuntimeInMinute := time.Duration(1) * time.Minute
|
||||
pollIntervalInSecond := time.Duration(1) * time.Second
|
||||
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.String() == reqUrl_token {
|
||||
resBodyReader_token := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_sys {
|
||||
resBodyReader_sys := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_sys)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_sys,
|
||||
}, nil
|
||||
}
|
||||
if req.URL.String() == reqUrl_update {
|
||||
resBodyReader_update := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_req_S)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_update,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_pollAndCancel && req.Method == "GET" {
|
||||
resBodyReader_pollStatus_X := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_req_X)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_pollStatus_X,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_pollAndCancel && req.Method == "DELETE" {
|
||||
resBodyReader_cancelUpdate := io.NopCloser(nil)
|
||||
return &http.Response{
|
||||
StatusCode: 204,
|
||||
Body: resBodyReader_cancelUpdate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("some-unknown-error")
|
||||
}
|
||||
|
||||
// execution and assertion
|
||||
expectedErr1 := fmt.Errorf("Addon update was aborted.\n")
|
||||
expectedErr2 := fmt.Errorf("The final status of addon update is X. Error: %v\n", expectedErr1)
|
||||
|
||||
err := runAbapLandscapePortalUpdateAddOnProduct(&mockUpdateAddOnConfig, client, clientToken, mockServKey, maxRuntimeInMinute, pollIntervalInSecond)
|
||||
|
||||
assert.Equal(t, expectedErr2, err)
|
||||
})
|
||||
|
||||
t.Run("Update addon reached timeout", func(t *testing.T) {
|
||||
// write addon.yml
|
||||
dir := t.TempDir()
|
||||
oldCWD, _ := os.Getwd()
|
||||
_ = os.Chdir(dir)
|
||||
// clean up tmp dir
|
||||
defer func() {
|
||||
_ = os.Chdir(oldCWD)
|
||||
}()
|
||||
|
||||
addonYML := `addonProduct: some-addon-product
|
||||
addonVersion: 1.0.0
|
||||
`
|
||||
addonYMLBytes := []byte(addonYML)
|
||||
os.WriteFile("addon.yml", addonYMLBytes, 0644)
|
||||
|
||||
// mock Do func
|
||||
maxRuntimeInMinute := time.Duration(3) * time.Second
|
||||
pollIntervalInSecond := time.Duration(1) * time.Second
|
||||
|
||||
GetDoFunc = func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.String() == reqUrl_token {
|
||||
resBodyReader_token := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_token)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_sys {
|
||||
resBodyReader_sys := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_sys)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_sys,
|
||||
}, nil
|
||||
}
|
||||
if req.URL.String() == reqUrl_update {
|
||||
resBodyReader_update := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_req_S)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_update,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.URL.String() == reqUrl_pollAndCancel && req.Method == "GET" {
|
||||
resBodyReader_pollStatus_I := io.NopCloser(bytes.NewReader([]byte(resBodyJSON_req_I)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: resBodyReader_pollStatus_I,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("some-unknown-error")
|
||||
}
|
||||
|
||||
// execution and assertion
|
||||
expectedErr1 := fmt.Errorf("Timed out: max runtime %v reached.", maxRuntimeInMinute)
|
||||
expectedErr2 := fmt.Errorf("Error occurred before a final status can be reached. Error: %v\n", expectedErr1)
|
||||
|
||||
err := runAbapLandscapePortalUpdateAddOnProduct(&mockUpdateAddOnConfig, client, clientToken, mockServKey, maxRuntimeInMinute, pollIntervalInSecond)
|
||||
|
||||
assert.Equal(t, expectedErr2, err)
|
||||
})
|
||||
}
|
@@ -25,6 +25,7 @@ func GetAllStepMetadata() map[string]config.StepData {
|
||||
"abapEnvironmentPushATCSystemConfig": abapEnvironmentPushATCSystemConfigMetadata(),
|
||||
"abapEnvironmentRunATCCheck": abapEnvironmentRunATCCheckMetadata(),
|
||||
"abapEnvironmentRunAUnitTest": abapEnvironmentRunAUnitTestMetadata(),
|
||||
"abapLandscapePortalUpdateAddOnProduct": abapLandscapePortalUpdateAddOnProductMetadata(),
|
||||
"ansSendEvent": ansSendEventMetadata(),
|
||||
"apiKeyValueMapDownload": apiKeyValueMapDownloadMetadata(),
|
||||
"apiKeyValueMapUpload": apiKeyValueMapUploadMetadata(),
|
||||
|
@@ -206,6 +206,7 @@ func Execute() {
|
||||
rootCmd.AddCommand(TmsExportCommand())
|
||||
rootCmd.AddCommand(IntegrationArtifactTransportCommand())
|
||||
rootCmd.AddCommand(AscAppUploadCommand())
|
||||
rootCmd.AddCommand(AbapLandscapePortalUpdateAddOnProductCommand())
|
||||
rootCmd.AddCommand(ImagePushToRegistryCommand())
|
||||
|
||||
addRootFlags(rootCmd)
|
||||
|
@@ -0,0 +1,56 @@
|
||||
# ${docGenStepName}
|
||||
|
||||
## ${docGenDescription}
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Please make sure, that you are under Embedded Steampunk environment.
|
||||
- Please make sure, that the service landscape-portal-api-for-s4hc with plan api was assigned as entitlement to the subaccount, where you are about to deploy addon product.
|
||||
- Please make sure, that before deploying addon product, an instance of landscape-portal-api-for-s4hc (plan api) was created, and a service key with x509 authentication mechanism was created for the instance. The service key needs to be stored in the Jenkins Credentials Store.
|
||||
- Please make sure, that the system to deploy addon product is active, and the descriptor file with deployment information is available.
|
||||
|
||||
## ${docGenParameters}
|
||||
|
||||
## ${docGenConfiguration}
|
||||
|
||||
## ${docJenkinsPluginDependencies}
|
||||
|
||||
## Example: Configuration in the config.yml
|
||||
|
||||
The recommended way to configure your pipeline is via the config.yml file. In this case, calling the step in the Jenkinsfile is reduced to one line:
|
||||
|
||||
```groovy
|
||||
abapLandscapePortalUpdateAddOnProduct script: this
|
||||
```
|
||||
|
||||
The configuration values for the addon update can be passed through the `config.yml` file:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
abapLandscapePortalUpdateAddOnProduct:
|
||||
landscapePortalAPICredentialsId: 'landscapePortalAPICredentialsId'
|
||||
abapSystemNumber: 'abapSystemNumber'
|
||||
addonDescriptorFileName: 'addon.yml'
|
||||
addonDescriptor: 'addonDescriptor'
|
||||
```
|
||||
|
||||
## Example: Configuration in the Jenkinsfile
|
||||
|
||||
The step, including all parameters, can also be called directly from the Jenkinsfile. In the following example, a configuration file is used.
|
||||
|
||||
```groovy
|
||||
abapLandscapePortalUpdateAddOnProduct (
|
||||
script: this,
|
||||
landscapePortalAPICredentialsId: 'landscapePortalAPICredentialsId'
|
||||
abapSystemNumber: 'abapSystemNumber'
|
||||
addonDescriptorFileName: 'addon.yml'
|
||||
addonDescriptor: 'addonDescriptor'
|
||||
)
|
||||
```
|
||||
|
||||
The file `addon.yml` would look like this:
|
||||
|
||||
```yaml
|
||||
addonProduct: some-addon-product
|
||||
addonVersion: some-addon-version
|
||||
```
|
3
go.mod
3
go.mod
@@ -101,7 +101,7 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.21.0 // indirect
|
||||
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
|
||||
golang.org/x/tools v0.14.0 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect
|
||||
)
|
||||
@@ -238,6 +238,7 @@ require (
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
golang.org/x/crypto v0.18.0
|
||||
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
|
6
go.sum
6
go.sum
@@ -1212,6 +1212,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo=
|
||||
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
@@ -1490,8 +1492,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@@ -47,6 +47,7 @@ stages:
|
||||
abapSystemSizeOfPersistence: 2
|
||||
abapSystemSizeOfRuntime: 1
|
||||
confirmDeletion: 'true'
|
||||
integrationTestOption: 'systemProvisioning'
|
||||
includeAddon: 'true'
|
||||
cfServiceKeyName: 'sap_com_0582'
|
||||
cfServiceKeyConfig: '{"scenario_id":"SAP_COM_0582","type":"basic"}'
|
||||
|
@@ -0,0 +1,41 @@
|
||||
metadata:
|
||||
name: abapLandscapePortalUpdateAddOnProduct
|
||||
description: "Update the AddOn product in SAP BTP ABAP Environment system of Landscape Portal"
|
||||
longDescription: |
|
||||
This step describes the AddOn product update in SAP BTP ABAP Environment system of Landscape Portal
|
||||
spec:
|
||||
inputs:
|
||||
secrets:
|
||||
- name: landscapePortalAPICredentialsId
|
||||
description: Jenkins secret text credential ID containing the service key to access the Landscape Portal Access API
|
||||
type: jenkins
|
||||
params:
|
||||
- name: landscapePortalAPIServiceKey
|
||||
type: string
|
||||
description: Service key JSON string to access the Landscape Portal Access API
|
||||
scope:
|
||||
- PARAMETERS
|
||||
mandatory: true
|
||||
secret: true
|
||||
resourceRef:
|
||||
- name: landscapePortalAPICredentialsId
|
||||
type: secret
|
||||
param: landscapePortalAPIServiceKey
|
||||
- name: abapSystemNumber
|
||||
description: System Number of the abap integration test system
|
||||
type: string
|
||||
mandatory: true
|
||||
scope:
|
||||
- PARAMETERS
|
||||
- STAGES
|
||||
- STEPS
|
||||
- name: addonDescriptorFileName
|
||||
type: string
|
||||
description: File name of the YAML file which describes the Product Version and corresponding Software Component Versions
|
||||
mandatory: true
|
||||
default: addon.yml
|
||||
scope:
|
||||
- PARAMETERS
|
||||
- STAGES
|
||||
- STEPS
|
||||
- GENERAL
|
@@ -125,6 +125,7 @@ public class CommonStepsTest extends BasePiperTest{
|
||||
'abapEnvironmentRunAUnitTest', //implementing new golang pattern without fields
|
||||
'abapEnvironmentCreateSystem', //implementing new golang pattern without fields
|
||||
'abapEnvironmentPushATCSystemConfig', //implementing new golang pattern without fields
|
||||
'abapLandscapePortalUpdateAddOnProduct', //implementing new golang pattern without fields
|
||||
'artifactPrepareVersion',
|
||||
'cloudFoundryCreateService', //implementing new golang pattern without fields
|
||||
'cloudFoundryCreateServiceKey', //implementing new golang pattern without fields
|
||||
@@ -195,7 +196,7 @@ public class CommonStepsTest extends BasePiperTest{
|
||||
'integrationArtifactGetServiceEndpoint', //implementing new golang pattern without fields
|
||||
'integrationArtifactDownload', //implementing new golang pattern without fields
|
||||
'integrationArtifactUpload', //implementing new golang pattern without fields
|
||||
'integrationArtifactTransport', //implementing new golang pattern without fields
|
||||
'integrationArtifactTransport', //implementing new golang pattern without fields
|
||||
'integrationArtifactTriggerIntegrationTest', //implementing new golang pattern without fields
|
||||
'integrationArtifactUnDeploy', //implementing new golang pattern without fields
|
||||
'integrationArtifactResource', //implementing new golang pattern without fields
|
||||
@@ -226,7 +227,7 @@ public class CommonStepsTest extends BasePiperTest{
|
||||
'azureBlobUpload',
|
||||
'awsS3Upload',
|
||||
'ansSendEvent',
|
||||
'apiProviderList', //implementing new golang pattern without fields
|
||||
'apiProviderList', //implementing new golang pattern without fields
|
||||
'tmsUpload',
|
||||
'tmsExport',
|
||||
'imagePushToRegistry',
|
||||
|
@@ -43,6 +43,7 @@ class abapEnvironmentPipelineStageIntegrationTestsTest extends BasePiperTest {
|
||||
helper.registerAllowedMethod('cloudFoundryDeleteService', [Map.class], {m -> stepsCalled.add('cloudFoundryDeleteService')})
|
||||
helper.registerAllowedMethod('abapEnvironmentBuild', [Map.class], {m -> stepsCalled.add('abapEnvironmentBuild')})
|
||||
helper.registerAllowedMethod('cloudFoundryCreateServiceKey', [Map.class], {m -> stepsCalled.add('cloudFoundryCreateServiceKey')})
|
||||
helper.registerAllowedMethod('abapLandscapePortalUpdateAddOnProduct', [Map.class], {m -> stepsCalled.add('abapLandscapePortalUpdateAddOnProduct')})
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -51,7 +52,7 @@ class abapEnvironmentPipelineStageIntegrationTestsTest extends BasePiperTest {
|
||||
nullScript.commonPipelineEnvironment.configuration.runStage = [
|
||||
'Integration Tests': true
|
||||
]
|
||||
jsr.step.abapEnvironmentPipelineStageIntegrationTests(script: nullScript, confirmDeletion: true)
|
||||
jsr.step.abapEnvironmentPipelineStageIntegrationTests(script: nullScript, integrationTestOption: 'systemProvisioning', confirmDeletion: true)
|
||||
|
||||
assertThat(stepsCalled, hasItems('input'))
|
||||
assertThat(stepsCalled, hasItems('abapEnvironmentCreateSystem'))
|
||||
@@ -66,7 +67,7 @@ class abapEnvironmentPipelineStageIntegrationTestsTest extends BasePiperTest {
|
||||
nullScript.commonPipelineEnvironment.configuration.runStage = [
|
||||
'Integration Tests': true
|
||||
]
|
||||
jsr.step.abapEnvironmentPipelineStageIntegrationTests(script: nullScript, confirmDeletion: false)
|
||||
jsr.step.abapEnvironmentPipelineStageIntegrationTests(script: nullScript, integrationTestOption: 'systemProvisioning', confirmDeletion: false)
|
||||
|
||||
|
||||
assertThat(stepsCalled, not(hasItem('input')))
|
||||
@@ -86,7 +87,7 @@ class abapEnvironmentPipelineStageIntegrationTestsTest extends BasePiperTest {
|
||||
]
|
||||
|
||||
try {
|
||||
jsr.step.abapEnvironmentPipelineStageIntegrationTests(script: nullScript, confirmDeletion: false)
|
||||
jsr.step.abapEnvironmentPipelineStageIntegrationTests(script: nullScript, integrationTestOption: 'systemProvisioning', confirmDeletion: false)
|
||||
fail("Expected exception")
|
||||
} catch (Exception e) {
|
||||
// failure expected
|
||||
@@ -112,4 +113,40 @@ class abapEnvironmentPipelineStageIntegrationTestsTest extends BasePiperTest {
|
||||
'cloudFoundryCreateServiceKey')))
|
||||
}
|
||||
|
||||
@Test
|
||||
void testabapLandscapePortalUpdateAddOnProduct() {
|
||||
|
||||
nullScript.commonPipelineEnvironment.configuration.runStage = [
|
||||
'Integration Tests': true
|
||||
]
|
||||
jsr.step.abapEnvironmentPipelineStageIntegrationTests(script: nullScript, integrationTestOption: 'addOnDeployment')
|
||||
|
||||
|
||||
assertThat(stepsCalled, not(hasItems('input',
|
||||
'abapEnvironmentCreateSystem',
|
||||
'cloudFoundryDeleteService',
|
||||
'cloudFoundryCreateServiceKey')))
|
||||
assertThat(stepsCalled, hasItems('abapLandscapePortalUpdateAddOnProduct'))
|
||||
assertThat(stepsCalled, hasItems('abapEnvironmentBuild'))
|
||||
}
|
||||
|
||||
@Test
|
||||
void testabapLandscapePortalUpdateAddOnProductFails() {
|
||||
|
||||
helper.registerAllowedMethod('abapLandscapePortalUpdateAddOnProduct', [Map.class], {m -> stepsCalled.add('abapLandscapePortalUpdateAddOnProduct'); error("Failed")})
|
||||
|
||||
nullScript.commonPipelineEnvironment.configuration.runStage = [
|
||||
'Integration Tests': true
|
||||
]
|
||||
|
||||
try {
|
||||
jsr.step.abapEnvironmentPipelineStageIntegrationTests(script: nullScript, integrationTestOption: 'addOnDeployment')
|
||||
fail("Expected exception")
|
||||
} catch (Exception e) {
|
||||
// failure expected
|
||||
}
|
||||
|
||||
assertThat(stepsCalled, not(hasItem('input')))
|
||||
assertThat(stepsCalled, hasItems('abapLandscapePortalUpdateAddOnProduct'))
|
||||
}
|
||||
}
|
||||
|
@@ -12,9 +12,9 @@ import static com.sap.piper.Prerequisites.checkScript
|
||||
'cloudFoundryDeleteService',
|
||||
/** If set to true, a confirmation is required to delete the system */
|
||||
'confirmDeletion',
|
||||
/** If set to true, the system is never deleted */
|
||||
'debug',
|
||||
'testBuild' // Parameter for test execution mode, if true stage will be skipped
|
||||
'debug', // If set to true, the system is never deleted
|
||||
'testBuild', // Parameter for test execution mode, if true stage will be skipped
|
||||
'integrationTestOption' // Integration test option
|
||||
]
|
||||
@Field Set STAGE_STEP_KEYS = GENERAL_CONFIG_KEYS
|
||||
@Field Set STEP_CONFIG_KEYS = STAGE_STEP_KEYS
|
||||
@@ -34,12 +34,15 @@ void call(Map parameters = [:]) {
|
||||
.addIfEmpty('confirmDeletion', true)
|
||||
.addIfEmpty('debug', false)
|
||||
.addIfEmpty('testBuild', false)
|
||||
.addIfEmpty('integrationTestOption', 'systemProvisioning')
|
||||
.use()
|
||||
|
||||
if (config.testBuild) {
|
||||
echo "Stage 'Integration Tests' skipped as parameter 'testBuild' is active"
|
||||
} else {
|
||||
piperStageWrapper (script: script, stageName: stageName, stashContent: [], stageLocking: false) {
|
||||
return null;
|
||||
}
|
||||
piperStageWrapper (script: script, stageName: stageName, stashContent: [], stageLocking: false) {
|
||||
if (config.integrationTestOption == 'systemProvisioning') {
|
||||
try {
|
||||
abapEnvironmentCreateSystem(script: parameters.script, includeAddon: true)
|
||||
cloudFoundryCreateServiceKey(script: parameters.script)
|
||||
@@ -48,14 +51,25 @@ void call(Map parameters = [:]) {
|
||||
echo "Deployment test of add-on product failed."
|
||||
throw e
|
||||
} finally {
|
||||
if (config.confirmDeletion) {
|
||||
input message: "Deployment test has been executed. Once you proceed, the test system will be deleted."
|
||||
}
|
||||
if (!config.debug) {
|
||||
cloudFoundryDeleteService script: parameters.script
|
||||
}
|
||||
}
|
||||
} else if (config.integrationTestOption == 'addOnDeployment') {
|
||||
try {
|
||||
abapLandscapePortalUpdateAddOnProduct(script: parameters.script)
|
||||
abapEnvironmentBuild(script: parameters.script, phase: 'GENERATION', downloadAllResultFiles: true, useFieldsOfAddonDescriptor: '[{"use":"Name","renameTo":"SWC"}]')
|
||||
} catch (Exception e) {
|
||||
echo "Deployment test of add-on product failed."
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
e = new Error('Unsupoorted integration test option.')
|
||||
throw e
|
||||
}
|
||||
|
||||
if (config.confirmDeletion) {
|
||||
input message: "Deployment test has been executed. Once you proceed, the test system will be deleted."
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
11
vars/abapLandscapePortalUpdateAddOnProduct.groovy
Normal file
11
vars/abapLandscapePortalUpdateAddOnProduct.groovy
Normal file
@@ -0,0 +1,11 @@
|
||||
import groovy.transform.Field
|
||||
|
||||
@Field String STEP_NAME = getClass().getName()
|
||||
@Field String METADATA_FILE = 'metadata/abapLandscapePortalUpdateAddOnProduct.yaml'
|
||||
|
||||
void call(Map parameters = [:]) {
|
||||
List credentials = [
|
||||
[type: 'token', id: 'landscapePortalAPICredentialsId', env: ['PIPER_landscapePortalAPIServiceKey']]
|
||||
]
|
||||
piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials)
|
||||
}
|
Reference in New Issue
Block a user