From e04fd6fa1f0abe6b18787b192e01164db1db06db Mon Sep 17 00:00:00 2001 From: Kiran Kamineni Date: Thu, 18 Jul 2019 14:11:11 -0700 Subject: Add a list api for instances curl -X GET /v1/instance returns all the instances created. It returns abbreviated instances for improved readability. For details on what resources were created for each instance, use the ID with GET. Issue-ID: MULTICLOUD-715 Change-Id: I05afe0fd2c254acbca4329289c81545f95c9fac5 Signed-off-by: Kiran Kamineni --- src/k8splugin/api/api.go | 1 + src/k8splugin/api/instancehandler.go | 18 +++++ src/k8splugin/api/instancehandler_test.go | 118 +++++++++++++++++++++++++++++- src/k8splugin/internal/app/instance.go | 42 +++++++++++ 4 files changed, 177 insertions(+), 2 deletions(-) diff --git a/src/k8splugin/api/api.go b/src/k8splugin/api/api.go index 353972a1..1bbe14a3 100644 --- a/src/k8splugin/api/api.go +++ b/src/k8splugin/api/api.go @@ -38,6 +38,7 @@ func NewRouter(defClient rb.DefinitionManager, instHandler := instanceHandler{client: instClient} instRouter := router.PathPrefix("/v1").Subrouter() instRouter.HandleFunc("/instance", instHandler.createHandler).Methods("POST") + instRouter.HandleFunc("/instance", instHandler.listHandler).Methods("GET") instRouter.HandleFunc("/instance/{instID}", instHandler.getHandler).Methods("GET") instRouter.HandleFunc("/instance/{instID}", instHandler.deleteHandler).Methods("DELETE") // (TODO): Fix update method diff --git a/src/k8splugin/api/instancehandler.go b/src/k8splugin/api/instancehandler.go index 42f3b212..3ec055bc 100644 --- a/src/k8splugin/api/instancehandler.go +++ b/src/k8splugin/api/instancehandler.go @@ -106,6 +106,24 @@ func (i instanceHandler) getHandler(w http.ResponseWriter, r *http.Request) { } } +// getHandler retrieves information about an instance via the ID +func (i instanceHandler) listHandler(w http.ResponseWriter, r *http.Request) { + + resp, err := i.client.List() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(resp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + // deleteHandler method terminates an instance via the ID func (i instanceHandler) deleteHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) diff --git a/src/k8splugin/api/instancehandler_test.go b/src/k8splugin/api/instancehandler_test.go index 35cef531..83fa3d2b 100644 --- a/src/k8splugin/api/instancehandler_test.go +++ b/src/k8splugin/api/instancehandler_test.go @@ -21,6 +21,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "sort" "testing" "github.com/onap/multicloud-k8s/src/k8splugin/internal/app" @@ -38,8 +39,9 @@ type mockInstanceClient struct { app.InstanceManager // Items and err will be used to customize each test // via a localized instantiation of mockInstanceClient - items []app.InstanceResponse - err error + items []app.InstanceResponse + miniitems []app.InstanceMiniResponse + err error } func (m *mockInstanceClient) Create(inp app.InstanceRequest) (app.InstanceResponse, error) { @@ -58,6 +60,14 @@ func (m *mockInstanceClient) Get(id string) (app.InstanceResponse, error) { return m.items[0], nil } +func (m *mockInstanceClient) List() ([]app.InstanceMiniResponse, error) { + if m.err != nil { + return []app.InstanceMiniResponse{}, m.err + } + + return m.miniitems, nil +} + func (m *mockInstanceClient) Find(rbName string, ver string, profile string, labelKeys map[string]string) ([]app.InstanceResponse, error) { if m.err != nil { return nil, m.err @@ -297,6 +307,110 @@ func TestInstanceGetHandler(t *testing.T) { } } +func TestInstanceListHandler(t *testing.T) { + testCases := []struct { + label string + input string + expectedCode int + expectedResponse []app.InstanceMiniResponse + instClient *mockInstanceClient + }{ + { + label: "Fail to List Instance", + input: "HaKpys8e", + expectedCode: http.StatusInternalServerError, + instClient: &mockInstanceClient{ + err: pkgerrors.New("Internal error"), + }, + }, + { + label: "Succesful List Instances", + expectedCode: http.StatusOK, + expectedResponse: []app.InstanceMiniResponse{ + { + ID: "HaKpys8e", + Request: app.InstanceRequest{ + RBName: "test-rbdef", + RBVersion: "v1", + ProfileName: "profile1", + CloudRegion: "region1", + }, + Namespace: "testnamespace", + }, + { + ID: "HaKpys8f", + Request: app.InstanceRequest{ + RBName: "test-rbdef-two", + RBVersion: "versionsomething", + ProfileName: "profile3", + CloudRegion: "region1", + }, + Namespace: "testnamespace-two", + }, + }, + instClient: &mockInstanceClient{ + miniitems: []app.InstanceMiniResponse{ + { + ID: "HaKpys8e", + Request: app.InstanceRequest{ + RBName: "test-rbdef", + RBVersion: "v1", + ProfileName: "profile1", + CloudRegion: "region1", + }, + Namespace: "testnamespace", + }, + { + ID: "HaKpys8f", + Request: app.InstanceRequest{ + RBName: "test-rbdef-two", + RBVersion: "versionsomething", + ProfileName: "profile3", + CloudRegion: "region1", + }, + Namespace: "testnamespace-two", + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("GET", "/v1/instance", nil) + resp := executeRequest(request, NewRouter(nil, nil, testCase.instClient, nil, nil, nil)) + + if testCase.expectedCode != resp.StatusCode { + t.Fatalf("Request method returned: %v and it was expected: %v", + resp.StatusCode, testCase.expectedCode) + } + if resp.StatusCode == http.StatusOK { + var response []app.InstanceMiniResponse + err := json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + t.Fatalf("Parsing the returned response got an error (%s)", err) + } + + // Since the order of returned slice is not guaranteed + // Sort them first and then do deepequal + // Check both and return error if both don't match + sort.Slice(response, func(i, j int) bool { + return response[i].ID < response[j].ID + }) + + sort.Slice(testCase.expectedResponse, func(i, j int) bool { + return testCase.expectedResponse[i].ID < testCase.expectedResponse[j].ID + }) + + if reflect.DeepEqual(testCase.expectedResponse, response) == false { + t.Fatalf("TestGetHandler returned:\n result=%v\n expected=%v", + &response, testCase.expectedResponse) + } + } + }) + } +} + func TestDeleteHandler(t *testing.T) { testCases := []struct { label string diff --git a/src/k8splugin/internal/app/instance.go b/src/k8splugin/internal/app/instance.go index 5272d60f..d28fe799 100644 --- a/src/k8splugin/internal/app/instance.go +++ b/src/k8splugin/internal/app/instance.go @@ -19,6 +19,7 @@ package app import ( "encoding/base64" "encoding/json" + "log" "math/rand" "github.com/onap/multicloud-k8s/src/k8splugin/internal/db" @@ -46,10 +47,20 @@ type InstanceResponse struct { Resources []helm.KubernetesResource `json:"resources"` } +// InstanceMiniResponse contains the response from instantiation +// It does NOT include the created resources. +// Use the regular GET to get the created resources for a particular instance +type InstanceMiniResponse struct { + ID string `json:"id"` + Request InstanceRequest `json:"request"` + Namespace string `json:"namespace"` +} + // InstanceManager is an interface exposes the instantiation functionality type InstanceManager interface { Create(i InstanceRequest) (InstanceResponse, error) Get(id string) (InstanceResponse, error) + List() ([]InstanceMiniResponse, error) Find(rbName string, ver string, profile string, labelKeys map[string]string) ([]InstanceResponse, error) Delete(id string) error } @@ -171,6 +182,37 @@ func (v *InstanceClient) Get(id string) (InstanceResponse, error) { return InstanceResponse{}, pkgerrors.New("Error getting Instance") } +// List returns the instance for corresponding ID +// Empty string returns all +func (v *InstanceClient) List() ([]InstanceMiniResponse, error) { + + dbres, err := db.DBconn.ReadAll(v.storeName, v.tagInst) + if err != nil || len(dbres) == 0 { + return []InstanceMiniResponse{}, pkgerrors.Wrap(err, "Listing Instances") + } + + var results []InstanceMiniResponse + for key, value := range dbres { + //value is a byte array + if value != nil { + resp := InstanceResponse{} + err = db.DBconn.Unmarshal(value, &resp) + if err != nil { + log.Printf("[Instance] Error: %s Unmarshaling Instance: %s", err.Error(), key) + } + + miniresp := InstanceMiniResponse{ + ID: resp.ID, + Request: resp.Request, + Namespace: resp.Namespace, + } + results = append(results, miniresp) + } + } + + return results, nil +} + // Find returns the instances that match the given criteria // If version is empty, it will return all instances for a given rbName // If profile is empty, it will return all instances for a given rbName+version -- cgit 1.2.3-korg