Skip to content

Commit

Permalink
Service Manager client - get Service Offering details and plans (#717)
Browse files Browse the repository at this point in the history
* Add ServicePlan struct

* wip

* Service Offering with plans request

* Add struct tags for plans in Service Offering details

* Add fake service plans

* Add unit test for service offering details and plans for given service offering ID

* Fix typos

* Refactor URL query for Service Plans
  • Loading branch information
szwedm authored and kyma-gopher-bot committed Aug 22, 2024
1 parent 90871c1 commit e6ca07c
Show file tree
Hide file tree
Showing 5 changed files with 527 additions and 13 deletions.
77 changes: 77 additions & 0 deletions internal/service-manager/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ const (
defaultSecret = "sap-btp-service-operator"
defaultNamespace = "kyma-system"
ServiceOfferingsPath = "/v1/service_offerings"
ServicePlansPath = "/v1/service_plans"

// see https://help.sap.com/docs/service-manager/sap-service-manager/filtering-parameters-and-operators
URLFieldQueryKey = "fieldQuery"
servicePlansForServiceOfferingQueryFormat = "service_offering_id eq '%s'"
)

type Config struct {
Expand Down Expand Up @@ -160,3 +165,75 @@ func (c *Client) ServiceOfferings() (*types.ServiceOfferings, error) {

return &serviceOfferings, nil
}

func (c *Client) ServiceOfferingDetails(serviceOfferingID string) (*types.ServiceOfferingDetails, error) {
so, err := c.serviceOfferingByID(serviceOfferingID)
if err != nil {
return nil, err
}

plans, err := c.servicePlansForServiceOffering(serviceOfferingID)
if err != nil {
return nil, err
}

return &types.ServiceOfferingDetails{
ServiceOffering: *so,
ServicePlans: *plans,
}, nil
}

func (c *Client) serviceOfferingByID(serviceOfferingID string) (*types.ServiceOffering, error) {
req, err := http.NewRequest(http.MethodGet, c.smURL+ServiceOfferingsPath+"/"+serviceOfferingID, nil)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}

defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

var so types.ServiceOffering
if err := json.Unmarshal(body, &so); err != nil {
return nil, err
}

return &so, nil
}

func (c *Client) servicePlansForServiceOffering(serviceOfferingID string) (*types.ServicePlans, error) {
req, err := http.NewRequest(http.MethodGet, c.smURL+ServicePlansPath, nil)
if err != nil {
return nil, err
}
values := req.URL.Query()
values.Add(URLFieldQueryKey, fmt.Sprintf(servicePlansForServiceOfferingQueryFormat, serviceOfferingID))
req.URL.RawQuery = values.Encode()
req.Header.Add("Content-Type", "application/json")

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}

defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

var plans types.ServicePlans
if err := json.Unmarshal(body, &plans); err != nil {
return nil, err
}

return &plans, nil
}
186 changes: 173 additions & 13 deletions internal/service-manager/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

servicemanager "github.com/kyma-project/btp-manager/internal/service-manager"
Expand All @@ -21,11 +22,13 @@ import (

const (
serviceOfferingsJSONPath = "testdata/service_offerings.json"
servicePlansJSONPath = "testdata/service_plans.json"
)

func TestClient(t *testing.T) {
// given
secretProvider := newFakeSecretProvider()
secretProvider.AddSecret(defaultSecret())
srv, err := initFakeServer()
require.NoError(t, err)

Expand All @@ -34,39 +37,51 @@ func TestClient(t *testing.T) {
httpClient := srv.Client()
url := srv.URL

allServiceOfferings := getAllServiceOfferingsFromJSON(t)
allServicePlans := getAllServicePlansFromJSON(t)

t.Run("should get service offerings available for the default credentials", func(t *testing.T) {
// given
ctx := context.TODO()
secretProvider.AddSecret(defaultSecret())
smClient := servicemanager.NewClient(ctx, slog.Default(), secretProvider)

var expectedServiceOfferings types.ServiceOfferings
soJSON, err := getResourcesFromJSONFile(serviceOfferingsJSONPath)
require.NoError(t, err)
// when
err = smClient.Defaults(ctx)

soBytes, err := json.Marshal(soJSON)
// then
require.NoError(t, err)

err = json.Unmarshal(soBytes, &expectedServiceOfferings)
require.NoError(t, err)
// given
smClient.SetHTTPClient(httpClient)
smClient.SetSMURL(url)

// when
err = smClient.Defaults(ctx)
sos, err := smClient.ServiceOfferings()

// then
require.NoError(t, err)
assert.Len(t, sos.ServiceOfferings, 4)
assert.ElementsMatch(t, allServiceOfferings.ServiceOfferings, sos.ServiceOfferings)
})

t.Run("should get service offering details and plans for given service offering ID", func(t *testing.T) {
// given
ctx := context.TODO()
smClient := servicemanager.NewClient(ctx, slog.Default(), secretProvider)
smClient.SetHTTPClient(httpClient)
smClient.SetSMURL(url)
soID := "fc26622b-aeb2-4f3c-95da-8eb337a26883"
expectedServiceOffering := getServiceOfferingByID(allServiceOfferings, soID)
filteredServicePlans := filterServicePlansByServiceOfferingID(allServicePlans, soID)

// when
so, err := smClient.ServiceOfferings()
sod, err := smClient.ServiceOfferingDetails(soID)

// then
require.NoError(t, err)
assert.Len(t, so.ServiceOfferings, 4)
assert.ElementsMatch(t, expectedServiceOfferings.ServiceOfferings, so.ServiceOfferings)
assert.Len(t, sod.ServicePlans.ServicePlans, 3)
assert.Equal(t, expectedServiceOffering, sod.ServiceOffering)
assert.ElementsMatch(t, filteredServicePlans.ServicePlans, sod.ServicePlans.ServicePlans)
})
}

Expand All @@ -78,6 +93,8 @@ func initFakeServer() (*httptest.Server, error) {

mux := http.NewServeMux()
mux.HandleFunc("GET /v1/service_offerings", smHandler.getServiceOfferings)
mux.HandleFunc("GET /v1/service_offerings/{serviceOfferingID}", smHandler.getServiceOffering)
mux.HandleFunc("GET /v1/service_plans", smHandler.getServicePlans)

srv := httptest.NewUnstartedServer(mux)

Expand All @@ -86,15 +103,20 @@ func initFakeServer() (*httptest.Server, error) {

type fakeSMHandler struct {
serviceOfferings map[string]interface{}
servicePlans map[string]interface{}
}

func newFakeSMHandler() (*fakeSMHandler, error) {
so, err := getResourcesFromJSONFile(serviceOfferingsJSONPath)
sos, err := getResourcesFromJSONFile(serviceOfferingsJSONPath)
if err != nil {
return nil, fmt.Errorf("while getting service offerings from JSON file: %w", err)
}
plans, err := getResourcesFromJSONFile(servicePlansJSONPath)
if err != nil {
return nil, fmt.Errorf("while getting service plans from JSON file: %w", err)
}

return &fakeSMHandler{serviceOfferings: so}, nil
return &fakeSMHandler{serviceOfferings: sos, servicePlans: plans}, nil
}

func getResourcesFromJSONFile(jsonFilePath string) (map[string]interface{}, error) {
Expand Down Expand Up @@ -128,6 +150,97 @@ func (h *fakeSMHandler) getServiceOfferings(w http.ResponseWriter, r *http.Reque
}
}

func (h *fakeSMHandler) getServiceOffering(w http.ResponseWriter, r *http.Request) {
soID := r.PathValue("serviceOfferingID")
if len(soID) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}

data, err := json.Marshal(h.serviceOfferings)
if err != nil {
log.Println("error while marshalling service offerings data: %w", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

var sos types.ServiceOfferings
if err := json.Unmarshal(data, &sos); err != nil {
log.Println("error while unmarshalling service offerings data to struct: %w", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

data = make([]byte, 0)
for _, so := range sos.ServiceOfferings {
if so.ID == soID {
data, err = json.Marshal(so)
if err != nil {
log.Println("error while marshalling service offering data: %w", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
break
}
w.WriteHeader(http.StatusNotFound)
}

if _, err = w.Write(data); err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Println("error while writing service offerings data: %w", err)
return
}
}

func (h *fakeSMHandler) getServicePlans(w http.ResponseWriter, r *http.Request) {
values := r.URL.Query()
prefixedSoID := values.Get(servicemanager.URLFieldQueryKey)
IDFilter := ""
if len(prefixedSoID) != 0 {
fields := strings.Fields(prefixedSoID)
IDFilter = strings.Trim(fields[2], "'")
}

data, err := json.Marshal(h.servicePlans)
if err != nil {
log.Println("error while marshalling service plans data: %w", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

var responseSps types.ServicePlans
if err := json.Unmarshal(data, &responseSps); err != nil {
log.Println("error while unmarshalling service offerings data to struct: %w", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

if len(IDFilter) != 0 {
var filteredSps types.ServicePlans
for _, sp := range responseSps.ServicePlans {
if sp.ServiceOfferingID == IDFilter {
filteredSps.ServicePlans = append(filteredSps.ServicePlans, sp)
}
}
responseSps = filteredSps
}

data = make([]byte, 0)
data, err = json.Marshal(responseSps)
if err != nil {
log.Println("error while marshalling service plans data: %w", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

if _, err = w.Write(data); err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Println("error while writing service plans data: %w", err)
return
}
}

type fakeSecretProvider struct {
secrets []*corev1.Secret
}
Expand Down Expand Up @@ -168,3 +281,50 @@ func defaultSecret() *corev1.Secret {
},
}
}

func getAllServiceOfferingsFromJSON(t *testing.T) types.ServiceOfferings {
var allSos types.ServiceOfferings
soJSON, err := getResourcesFromJSONFile(serviceOfferingsJSONPath)
require.NoError(t, err)

soBytes, err := json.Marshal(soJSON)
require.NoError(t, err)

err = json.Unmarshal(soBytes, &allSos)
require.NoError(t, err)

return allSos
}

func getAllServicePlansFromJSON(t *testing.T) types.ServicePlans {
var allSp types.ServicePlans
spJSON, err := getResourcesFromJSONFile(servicePlansJSONPath)
require.NoError(t, err)

spBytes, err := json.Marshal(spJSON)
require.NoError(t, err)

err = json.Unmarshal(spBytes, &allSp)
require.NoError(t, err)

return allSp
}

func getServiceOfferingByID(serviceOfferings types.ServiceOfferings, serviceOfferingID string) types.ServiceOffering {
for _, so := range serviceOfferings.ServiceOfferings {
if so.ID == serviceOfferingID {
return so
}
}
return types.ServiceOffering{}
}

func filterServicePlansByServiceOfferingID(servicePlans types.ServicePlans, serviceOfferingID string) types.ServicePlans {
var filteredSp types.ServicePlans
for _, sp := range servicePlans.ServicePlans {
if sp.ServiceOfferingID == serviceOfferingID {
filteredSp.ServicePlans = append(filteredSp.ServicePlans, sp)
}
}
return filteredSp
}
Loading

0 comments on commit e6ca07c

Please sign in to comment.