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

Update API models to use envelope format consistently #381

Merged
merged 1 commit into from
Sep 13, 2024
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
10 changes: 5 additions & 5 deletions clients/ui/bff/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ curl -i localhost:4000/api/v1/model_registry
```
```
# GET /v1/model_registry/{model_registry_id}/registered_models
curl -i localhost:4000/api/v1/model_registry/model_registry_1/registered_models
curl -i localhost:4000/api/v1/model_registry/model-registry/registered_models
```
```
#POST /v1/model_registry/{model_registry_id}/registered_models
curl -i -X POST "http://localhost:4000/api/v1/model_registry/model_registry/registered_models" \
curl -i -X POST "http://localhost:4000/api/v1/model_registry/model-registry/registered_models" \
-H "Content-Type: application/json" \
-d '{
-d '{ "data": {
"customProperties": {
"my-label9": {
"metadataType": "MetadataStringValue",
Expand All @@ -91,9 +91,9 @@ curl -i -X POST "http://localhost:4000/api/v1/model_registry/model_registry/regi
"name": "bella",
"owner": "eder",
"state": "LIVE"
}'
}}'
```
```
# GET /v1/model_registry/{model_registry_id}/registered_models/{registered_model_id}
curl -i localhost:4000/api/v1/model_registry/model_registry/registered_models/1
curl -i localhost:4000/api/v1/model_registry/model-registry/registered_models/1
```
6 changes: 5 additions & 1 deletion clients/ui/bff/api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ type ErrorResponse struct {
Message string `json:"message"`
}

type ErrorEnvelope struct {
Error *integrations.HTTPError `json:"error"`
}

func (app *App) LogError(r *http.Request, err error) {
var (
method = r.Method
Expand All @@ -40,7 +44,7 @@ func (app *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err e

func (app *App) errorResponse(w http.ResponseWriter, r *http.Request, error *integrations.HTTPError) {

env := Envelope{"error": error}
env := ErrorEnvelope{Error: error}

err := app.WriteJSON(w, error.StatusCode, env, nil)

Expand Down
13 changes: 10 additions & 3 deletions clients/ui/bff/api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import (
"strings"
)

type Envelope map[string]interface{}
type Envelope[D any, M any] struct {
Data D `json:"data,omitempty"`
Metadata M `json:"metadata,omitempty"`
}

type TypedEnvelope[T any] map[string]T
type None *struct{}

func (app *App) WriteJSON(w http.ResponseWriter, status int, data any, headers http.Header) error {

Expand All @@ -29,7 +32,11 @@ func (app *App) WriteJSON(w http.ResponseWriter, status int, data any, headers h

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(js)
_, err = w.Write(js)

if err != nil {
return err
}

return nil
}
Expand Down
7 changes: 5 additions & 2 deletions clients/ui/bff/api/model_registry_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package api

import (
"github.com/julienschmidt/httprouter"
"github.com/kubeflow/model-registry/ui/bff/data"
"net/http"
)

type ModelRegistryListEnvelope Envelope[[]data.ModelRegistryModel, None]

func (app *App) ModelRegistryHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {

registries, err := app.models.ModelRegistry.FetchAllModelRegistries(app.kubernetesClient)
Expand All @@ -13,8 +16,8 @@ func (app *App) ModelRegistryHandler(w http.ResponseWriter, r *http.Request, ps
return
}

modelRegistryRes := Envelope{
"model_registry": registries,
modelRegistryRes := ModelRegistryListEnvelope{
Data: registries,
}

err = app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil)
Expand Down
18 changes: 5 additions & 13 deletions clients/ui/bff/api/model_registry_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,28 +29,20 @@ func TestModelRegistryHandler(t *testing.T) {
defer rs.Body.Close()
body, err := io.ReadAll(rs.Body)
assert.NoError(t, err)
var modelRegistryRes Envelope
err = json.Unmarshal(body, &modelRegistryRes)
var actual ModelRegistryListEnvelope
err = json.Unmarshal(body, &actual)
assert.NoError(t, err)

assert.Equal(t, http.StatusOK, rr.Code)

// Convert the unmarshalled data to the expected type
actualModelRegistry := make([]data.ModelRegistryModel, 0)
for _, v := range modelRegistryRes["model_registry"].([]interface{}) {
model := v.(map[string]interface{})
actualModelRegistry = append(actualModelRegistry, data.ModelRegistryModel{Name: model["name"].(string), Description: model["description"].(string), DisplayName: model["displayName"].(string)})
}
modelRegistryRes["model_registry"] = actualModelRegistry

var expected = Envelope{
"model_registry": []data.ModelRegistryModel{
var expected = ModelRegistryListEnvelope{
Data: []data.ModelRegistryModel{
{Name: "model-registry", Description: "Model registry description", DisplayName: "Model Registry"},
{Name: "model-registry-dora", Description: "Model registry dora description", DisplayName: "Model Registry Dora"},
{Name: "model-registry-bella", Description: "Model registry bella description", DisplayName: "Model Registry Bella"},
},
}

assert.Equal(t, expected, modelRegistryRes)
assert.Equal(t, expected, actual)

}
26 changes: 17 additions & 9 deletions clients/ui/bff/api/registered_models_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (
"net/http"
)

type RegisteredModelEnvelope Envelope[*openapi.RegisteredModel, None]
type RegisteredModelListEnvelope Envelope[*openapi.RegisteredModelList, None]

func (app *App) GetAllRegisteredModelsHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
//TODO (ederign) implement pagination
client, ok := r.Context().Value(httpClientKey).(integrations.HTTPClientInterface)
Expand All @@ -25,8 +28,8 @@ func (app *App) GetAllRegisteredModelsHandler(w http.ResponseWriter, r *http.Req
return
}

modelRegistryRes := Envelope{
"registered_model_list": modelList,
modelRegistryRes := RegisteredModelListEnvelope{
Data: modelList,
}

err = app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil)
Expand All @@ -42,18 +45,20 @@ func (app *App) CreateRegisteredModelHandler(w http.ResponseWriter, r *http.Requ
return
}

var model openapi.RegisteredModel
if err := json.NewDecoder(r.Body).Decode(&model); err != nil {
var envelope RegisteredModelEnvelope
if err := json.NewDecoder(r.Body).Decode(&envelope); err != nil {
app.serverErrorResponse(w, r, fmt.Errorf("error decoding JSON:: %v", err.Error()))
return
}

if err := validation.ValidateRegisteredModel(model); err != nil {
data := *envelope.Data

if err := validation.ValidateRegisteredModel(data); err != nil {
app.badRequestResponse(w, r, fmt.Errorf("validation error:: %v", err.Error()))
return
}

jsonData, err := json.Marshal(model)
jsonData, err := json.Marshal(data)
if err != nil {
app.serverErrorResponse(w, r, fmt.Errorf("error marshaling model to JSON: %w", err))
return
Expand All @@ -75,8 +80,11 @@ func (app *App) CreateRegisteredModelHandler(w http.ResponseWriter, r *http.Requ
return
}

response := RegisteredModelEnvelope{
Data: createdModel,
}
w.Header().Set("Location", fmt.Sprintf("%s/%s", RegisteredModelsPath, *createdModel.Id))
err = app.WriteJSON(w, http.StatusCreated, createdModel, nil)
err = app.WriteJSON(w, http.StatusCreated, response, nil)
if err != nil {
app.serverErrorResponse(w, r, fmt.Errorf("error writing JSON"))
return
Expand All @@ -101,8 +109,8 @@ func (app *App) GetRegisteredModelHandler(w http.ResponseWriter, r *http.Request
return
}

result := Envelope{
"registered_model": model,
result := RegisteredModelEnvelope{
Data: model,
}

err = app.WriteJSON(w, http.StatusOK, result, nil)
Expand Down
44 changes: 25 additions & 19 deletions clients/ui/bff/api/registered_models_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestGetRegisteredModelHandler(t *testing.T) {
}

req, err := http.NewRequest(http.MethodGet,
"/api/v1/model-registry/model-registry/registered_models/1", nil)
"/api/v1/model_registry/model-registry/registered_models/1", nil)
assert.NoError(t, err)

ctx := context.WithValue(req.Context(), httpClientKey, mockClient)
Expand All @@ -37,19 +37,21 @@ func TestGetRegisteredModelHandler(t *testing.T) {

body, err := io.ReadAll(rs.Body)
assert.NoError(t, err)
var registeredModelRes TypedEnvelope[openapi.RegisteredModel]
var registeredModelRes RegisteredModelEnvelope
err = json.Unmarshal(body, &registeredModelRes)
assert.NoError(t, err)

assert.Equal(t, http.StatusOK, rr.Code)

var expected = TypedEnvelope[openapi.RegisteredModel]{
"registered_model": mocks.GetRegisteredModelMocks()[0],
mockModel := mocks.GetRegisteredModelMocks()[0]

var expected = RegisteredModelEnvelope{
Data: &mockModel,
}

//TODO assert the full structure, I couldn't get unmarshalling to work for the full customProperties values
// this issue is in the test only
assert.Equal(t, expected["registered_model"].Name, registeredModelRes["registered_model"].Name)
assert.Equal(t, expected.Data.Name, registeredModelRes.Data.Name)
}

func TestGetAllRegisteredModelsHandler(t *testing.T) {
Expand All @@ -61,7 +63,7 @@ func TestGetAllRegisteredModelsHandler(t *testing.T) {
}

req, err := http.NewRequest(http.MethodGet,
"/api/v1/model-registry/model-registry/registered_models", nil)
"/api/v1/model_registry/model-registry/registered_models", nil)
assert.NoError(t, err)

ctx := context.WithValue(req.Context(), httpClientKey, mockClient)
Expand All @@ -76,20 +78,22 @@ func TestGetAllRegisteredModelsHandler(t *testing.T) {

body, err := io.ReadAll(rs.Body)
assert.NoError(t, err)
var registeredModelsListRes TypedEnvelope[openapi.RegisteredModelList]
var registeredModelsListRes RegisteredModelListEnvelope
err = json.Unmarshal(body, &registeredModelsListRes)
assert.NoError(t, err)

assert.Equal(t, http.StatusOK, rr.Code)

var expected = TypedEnvelope[openapi.RegisteredModelList]{
"registered_model_list": mocks.GetRegisteredModelListMock(),
modelList := mocks.GetRegisteredModelListMock()

var expected = RegisteredModelListEnvelope{
Data: &modelList,
}

assert.Equal(t, expected["registered_model_list"].Size, registeredModelsListRes["registered_model_list"].Size)
assert.Equal(t, expected["registered_model_list"].PageSize, registeredModelsListRes["registered_model_list"].PageSize)
assert.Equal(t, expected["registered_model_list"].NextPageToken, registeredModelsListRes["registered_model_list"].NextPageToken)
assert.Equal(t, len(expected["registered_model_list"].Items), len(registeredModelsListRes["registered_model_list"].Items))
assert.Equal(t, expected.Data.Size, registeredModelsListRes.Data.Size)
assert.Equal(t, expected.Data.PageSize, registeredModelsListRes.Data.PageSize)
assert.Equal(t, expected.Data.NextPageToken, registeredModelsListRes.Data.NextPageToken)
assert.Equal(t, len(expected.Data.Items), len(registeredModelsListRes.Data.Items))
}

func TestCreateRegisteredModelHandler(t *testing.T) {
Expand All @@ -100,14 +104,16 @@ func TestCreateRegisteredModelHandler(t *testing.T) {
modelRegistryClient: mockMRClient,
}

newModel := openapi.NewRegisteredModelCreate("Model One")
newModelJSON, err := newModel.MarshalJSON()
newModel := openapi.NewRegisteredModel("Model One")
newEnvelope := RegisteredModelEnvelope{Data: newModel}

newModelJSON, err := json.Marshal(newEnvelope)
assert.NoError(t, err)

reqBody := bytes.NewReader(newModelJSON)

req, err := http.NewRequest(http.MethodPost,
"/api/v1/model-registry/model-registry/registered_models", reqBody)
"/api/v1/model_registry/model-registry/registered_models", reqBody)
assert.NoError(t, err)

ctx := context.WithValue(req.Context(), httpClientKey, mockClient)
Expand All @@ -122,14 +128,14 @@ func TestCreateRegisteredModelHandler(t *testing.T) {

body, err := io.ReadAll(rs.Body)
assert.NoError(t, err)
var registeredModelRes openapi.RegisteredModel
err = json.Unmarshal(body, &registeredModelRes)
var actual RegisteredModelEnvelope
err = json.Unmarshal(body, &actual)
assert.NoError(t, err)

assert.Equal(t, http.StatusCreated, rr.Code)

var expected = mocks.GetRegisteredModelMocks()[0]

assert.Equal(t, expected.Name, registeredModelRes.Name)
assert.Equal(t, expected.Name, actual.Data.Name)
assert.NotEmpty(t, rs.Header.Get("location"))
}