1
0
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 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>
This commit is contained in:
ranliii
2024-02-20 19:39:43 +01:00
committed by GitHub
parent a1908a67e0
commit f1234114be
15 changed files with 1517 additions and 17 deletions

View 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
}
}
}
}

View 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
}

View 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")
}

View 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)
})
}

View File

@@ -25,6 +25,7 @@ func GetAllStepMetadata() map[string]config.StepData {
"abapEnvironmentPushATCSystemConfig": abapEnvironmentPushATCSystemConfigMetadata(),
"abapEnvironmentRunATCCheck": abapEnvironmentRunATCCheckMetadata(),
"abapEnvironmentRunAUnitTest": abapEnvironmentRunAUnitTestMetadata(),
"abapLandscapePortalUpdateAddOnProduct": abapLandscapePortalUpdateAddOnProductMetadata(),
"ansSendEvent": ansSendEventMetadata(),
"apiKeyValueMapDownload": apiKeyValueMapDownloadMetadata(),
"apiKeyValueMapUpload": apiKeyValueMapUploadMetadata(),

View File

@@ -206,6 +206,7 @@ func Execute() {
rootCmd.AddCommand(TmsExportCommand())
rootCmd.AddCommand(IntegrationArtifactTransportCommand())
rootCmd.AddCommand(AscAppUploadCommand())
rootCmd.AddCommand(AbapLandscapePortalUpdateAddOnProductCommand())
rootCmd.AddCommand(ImagePushToRegistryCommand())
addRootFlags(rootCmd)

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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"}'

View File

@@ -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

View File

@@ -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',

View File

@@ -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'))
}
}

View File

@@ -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."
}
}
}

View 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)
}