Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loading Service Instances in BTP Manager UI Backend - API #746

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ require (
github.com/prometheus/common v0.53.0 // indirect
github.com/prometheus/procfs v0.14.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/net v0.25.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
Expand Down
34 changes: 20 additions & 14 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ type Config struct {

type API struct {
server *http.Server
smClient *servicemanager.Client
secretProvider *clusterobject.SecretProvider
smClient servicemanager.Client
secretProvider clusterobject.SecretProvider
frontendFS http.FileSystem
logger *slog.Logger
}

func NewAPI(cfg Config, serviceManager *servicemanager.Client, secretProvider *clusterobject.SecretProvider, fs http.FileSystem) *API {
func NewAPI(cfg Config, serviceManager servicemanager.Client, secretProvider clusterobject.SecretProvider, fs http.FileSystem) *API {
srv := &http.Server{
Addr: fmt.Sprintf(":%s", cfg.Port),
ReadTimeout: cfg.ReadTimeout,
Expand All @@ -49,21 +49,26 @@ func NewAPI(cfg Config, serviceManager *servicemanager.Client, secretProvider *c
}

func (a *API) Start() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/secrets", a.ListSecrets)
mux.HandleFunc("GET /api/service-instances", a.ListServiceInstances)
mux.HandleFunc("PUT /api/service-instance/{id}", a.CreateServiceInstance)
mux.HandleFunc("GET /api/service-instance/{id}", a.GetServiceInstance)
mux.HandleFunc("GET /api/service-offerings/{namespace}/{name}", a.ListServiceOfferings)
mux.HandleFunc("GET /api/service-offering/{id}", a.GetServiceOffering)
mux.HandleFunc("POST /api/service-bindings", a.CreateServiceBindings)
mux.HandleFunc("GET /api/service-binding/{id}", a.GetServiceBinding)
mux.Handle("GET /", http.FileServer(a.frontendFS))
a.server.Handler = mux
router := http.NewServeMux()

a.AttachRoutes(router)
a.server.Handler = router

log.Fatal(a.server.ListenAndServe())
}

func (a *API) AttachRoutes(router *http.ServeMux) {
router.HandleFunc("GET /api/secrets", a.ListSecrets)
router.HandleFunc("GET /api/service-instances", a.ListServiceInstances)
jaroslaw-pieszka marked this conversation as resolved.
Show resolved Hide resolved
router.HandleFunc("PUT /api/service-instance/{id}", a.CreateServiceInstance)
router.HandleFunc("GET /api/service-instance/{id}", a.GetServiceInstance)
router.HandleFunc("GET /api/service-offerings/{namespace}/{name}", a.ListServiceOfferings)
router.HandleFunc("GET /api/service-offering/{id}", a.GetServiceOffering)
router.HandleFunc("POST /api/service-bindings", a.CreateServiceBindings)
router.HandleFunc("GET /api/service-binding/{id}", a.GetServiceBinding)
router.Handle("GET /", http.FileServer(a.frontendFS))
}

func (a *API) CreateServiceInstance(writer http.ResponseWriter, request *http.Request) {
a.setupCors(writer, request)
}
Expand Down Expand Up @@ -122,6 +127,7 @@ func (a *API) GetServiceInstance(writer http.ResponseWriter, request *http.Reque

func (a *API) ListServiceInstances(writer http.ResponseWriter, request *http.Request) {
a.setupCors(writer, request)

sis, err := a.smClient.ServiceInstances()
if returnError(writer, err) {
return
Expand Down
153 changes: 153 additions & 0 deletions internal/api/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package api

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path"
"testing"

clusterojbect "github.com/kyma-project/btp-manager/internal/cluster-object/automock"
servicemanager "github.com/kyma-project/btp-manager/internal/service-manager/automock"
"github.com/kyma-project/btp-manager/internal/service-manager/types"
"github.com/kyma-project/btp-manager/ui"
"github.com/stretchr/testify/assert"

"github.com/stretchr/testify/require"
)

const testResourcesDir = "testdata"

func TestApiResponses(t *testing.T) {

tests := []struct {
name string
path string
body string
method string
expectedStatus int
file string
items *types.ServiceInstances
}{
{
name: "list instances should return all its services",
file: "list-instances-happy-expected.json",
path: "api/service-instances",
method: http.MethodGet,
expectedStatus: http.StatusOK,
items: &types.ServiceInstances{
Items: []types.ServiceInstance{
{
Common: types.Common{
ID: "1",
Name: "service-1",
Description: "",
},
},
{
Common: types.Common{
ID: "2",
Name: "service-2",
Description: "",
},
},
},
},
},
}
for _, tt := range tests {
jaroslaw-pieszka marked this conversation as resolved.
Show resolved Hide resolved
t.Run(tt.name, func(t *testing.T) {
// given
router := http.NewServeMux()

sm := servicemanager.NewClient(t)
sm.On("ServiceInstances").Return(tt.items, nil)

provider := clusterojbect.NewProvider(t)

api := NewAPI(Config{}, sm, provider, ui.NewUIStaticFS())
api.AttachRoutes(router)

httpServer := httptest.NewServer(router)
defer httpServer.Close()

// when
resp := callAPI(t, httpServer, tt.method, tt.path, tt.body)

// then
assert.Equal(t, tt.expectedStatus, resp.StatusCode)

got, err := io.ReadAll(resp.Body)
require.NoError(t, err)
validateJSON(t, got, tt.file)
})
}

t.Run("should return 503 on error", func(t *testing.T) {
// given
router := http.NewServeMux()

sm := servicemanager.NewClient(t)
sm.On("ServiceInstances").Return(nil, fmt.Errorf("error"))

provider := clusterojbect.NewProvider(t)

api := NewAPI(Config{}, sm, provider, ui.NewUIStaticFS())
api.AttachRoutes(router)

httpServer := httptest.NewServer(router)
defer httpServer.Close()

// when
resp := callAPI(t, httpServer, http.MethodGet, "api/service-instances", "")

// then
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
})
}

func validateJSON(t *testing.T, got []byte, file string) {
expected := readJsonFile(t, file)
prettyExpected := indent([]byte(expected), t)
prettyGot := indent(got, t)

if !assert.JSONEq(t, prettyGot.String(), prettyExpected.String()) {
t.Errorf("%v Schema() = \n######### GOT ###########%v\n######### ENDGOT ########, expected \n##### EXPECTED #####%v\n##### ENDEXPECTED #####", file, prettyGot.String(), prettyExpected.String())
}
}

func indent(expected []byte, t *testing.T) *bytes.Buffer {
jaroslaw-pieszka marked this conversation as resolved.
Show resolved Hide resolved
var pretty bytes.Buffer
err := json.Indent(&pretty, []byte(expected), "", " ")
if err != nil {
t.Error(err)
t.Fail()
}

return &pretty
}

func readJsonFile(t *testing.T, file string) string {
t.Helper()

filename := path.Join(testResourcesDir, file)
jsonFile, err := os.ReadFile(filename)
require.NoError(t, err)

return string(jsonFile)
}

func callAPI(t *testing.T, httpServer *httptest.Server, method string, path string, body string) *http.Response {
cli := httpServer.Client()
req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", httpServer.URL, path), bytes.NewBuffer([]byte(body)))
req.Header.Set("X-Broker-API-Version", "2.15")
require.NoError(t, err)

resp, err := cli.Do(req)
require.NoError(t, err)
return resp
}
38 changes: 38 additions & 0 deletions internal/api/responses/converters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package responses

import (
"testing"

"github.com/kyma-project/btp-manager/internal/service-manager/types"
"github.com/stretchr/testify/assert"
)

func TestConverters(t *testing.T) {

t.Run("should set correct len of items", func(t *testing.T) {
// given
servicesInstances := &types.ServiceInstances{
Items: []types.ServiceInstance{
{
Common: types.Common{
ID: "1",
Name: "service-1",
Description: "",
},
},
{
Common: types.Common{
ID: "2",
Name: "service-2",
Description: "",
},
},
},
}
// when
vmServiceInstances := ToServiceInstancesVM(servicesInstances)

// then
assert.Equal(t, 2, vmServiceInstances.NumItems)
})
}
23 changes: 23 additions & 0 deletions internal/api/testdata/list-instances-happy-expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"numItems": 2,
"items": [
{
"id": "1",
"name": "service-1",
"namespace": "",
"servicePlanID": "",
"servicePlanName": "",
"subaccountID": "",
"clusterID": ""
},
{
"id": "2",
"name": "service-2",
"namespace": "",
"servicePlanID": "",
"servicePlanName": "",
"subaccountID": "",
"clusterID": ""
}
]
}
89 changes: 89 additions & 0 deletions internal/cluster-object/automock/provider.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading