Skip to content

Commit 16d81a2

Browse files
authored
feat(bff): Stub endpoints for CRUD Model Registries (kubeflow#847)
Signed-off-by: Eder Ignatowicz <[email protected]>
1 parent 414ae8a commit 16d81a2

File tree

10 files changed

+354
-61
lines changed

10 files changed

+354
-61
lines changed

clients/ui/bff/internal/api/app.go

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ package api
33
import (
44
"context"
55
"fmt"
6-
helper "github.com/kubeflow/model-registry/ui/bff/internal/helpers"
76
"log/slog"
87
"net/http"
98
"path"
109

10+
helper "github.com/kubeflow/model-registry/ui/bff/internal/helpers"
11+
1112
"github.com/kubeflow/model-registry/ui/bff/internal/config"
1213
"github.com/kubeflow/model-registry/ui/bff/internal/integrations"
1314
"github.com/kubeflow/model-registry/ui/bff/internal/repositories"
@@ -19,26 +20,29 @@ import (
1920
const (
2021
Version = "1.0.0"
2122

22-
PathPrefix = "/model-registry"
23-
ApiPathPrefix = "/api/v1"
24-
ModelRegistryId = "model_registry_id"
25-
RegisteredModelId = "registered_model_id"
26-
ModelVersionId = "model_version_id"
27-
ModelArtifactId = "model_artifact_id"
28-
ArtifactId = "artifact_id"
29-
HealthCheckPath = ApiPathPrefix + "/healthcheck"
30-
UserPath = ApiPathPrefix + "/user"
31-
ModelRegistryListPath = ApiPathPrefix + "/model_registry"
32-
NamespaceListPath = ApiPathPrefix + "/namespaces"
33-
ModelRegistryPath = ModelRegistryListPath + "/:" + ModelRegistryId
34-
RegisteredModelListPath = ModelRegistryPath + "/registered_models"
35-
RegisteredModelPath = RegisteredModelListPath + "/:" + RegisteredModelId
36-
RegisteredModelVersionsPath = RegisteredModelPath + "/versions"
37-
ModelVersionListPath = ModelRegistryPath + "/model_versions"
38-
ModelVersionPath = ModelVersionListPath + "/:" + ModelVersionId
39-
ModelVersionArtifactListPath = ModelVersionPath + "/artifacts"
40-
ModelArtifactListPath = ModelRegistryPath + "/model_artifacts"
41-
ModelArtifactPath = ModelArtifactListPath + "/:" + ModelArtifactId
23+
PathPrefix = "/model-registry"
24+
ApiPathPrefix = "/api/v1"
25+
ModelRegistryId = "model_registry_id"
26+
RegisteredModelId = "registered_model_id"
27+
ModelVersionId = "model_version_id"
28+
ModelArtifactId = "model_artifact_id"
29+
ArtifactId = "artifact_id"
30+
HealthCheckPath = ApiPathPrefix + "/healthcheck"
31+
UserPath = ApiPathPrefix + "/user"
32+
ModelRegistryListPath = ApiPathPrefix + "/model_registry"
33+
ModelRegistryPath = ModelRegistryListPath + "/:" + ModelRegistryId
34+
NamespaceListPath = ApiPathPrefix + "/namespaces"
35+
SettingsPath = ApiPathPrefix + "/settings"
36+
ModelRegistrySettingsListPath = SettingsPath + "/model_registry"
37+
ModelRegistrySettingsPath = ModelRegistrySettingsListPath + "/:" + ModelRegistryId
38+
RegisteredModelListPath = ModelRegistryPath + "/registered_models"
39+
RegisteredModelPath = RegisteredModelListPath + "/:" + RegisteredModelId
40+
RegisteredModelVersionsPath = RegisteredModelPath + "/versions"
41+
ModelVersionListPath = ModelRegistryPath + "/model_versions"
42+
ModelVersionPath = ModelVersionListPath + "/:" + ModelVersionId
43+
ModelVersionArtifactListPath = ModelVersionPath + "/artifacts"
44+
ModelArtifactListPath = ModelRegistryPath + "/model_artifacts"
45+
ModelArtifactPath = ModelArtifactListPath + "/:" + ModelArtifactId
4246

4347
ArtifactListPath = ModelRegistryPath + "/artifacts"
4448
ArtifactPath = ArtifactListPath + "/:" + ArtifactId
@@ -119,14 +123,18 @@ func (app *App) Routes() http.Handler {
119123
apiRouter.PATCH(ArtifactPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.UpdateArtifactHandler))))
120124
apiRouter.GET(ModelVersionArtifactListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetAllModelArtifactsByModelVersionHandler))))
121125
apiRouter.POST(ModelVersionArtifactListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.CreateModelArtifactByModelVersionHandler))))
122-
apiRouter.PATCH(ModelRegistryPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.UpdateModelVersionHandler))))
123126

124127
// Kubernetes routes
125128
apiRouter.GET(UserPath, app.UserHandler)
126-
// Perform SAR to Get List Services by Namespace
127-
apiRouter.GET(ModelRegistryListPath, app.AttachNamespace(app.PerformSARonGetListServicesByNamespace(app.ModelRegistryHandler)))
129+
apiRouter.GET(ModelRegistryListPath, app.AttachNamespace(app.PerformSARonGetListServicesByNamespace(app.GetAllModelRegistriesHandler)))
128130
if app.config.StandaloneMode {
129131
apiRouter.GET(NamespaceListPath, app.GetNamespacesHandler)
132+
//Those endpoints are not implement yet. This is a STUB API to unblock frontend development
133+
apiRouter.GET(ModelRegistrySettingsListPath, app.AttachNamespace(app.GetAllModelRegistriesSettingsHandler))
134+
apiRouter.POST(ModelRegistrySettingsListPath, app.AttachNamespace(app.CreateModelRegistrySettingsHandler))
135+
apiRouter.GET(ModelRegistrySettingsPath, app.AttachNamespace(app.GetModelRegistrySettingsHandler))
136+
apiRouter.PATCH(ModelRegistrySettingsPath, app.AttachNamespace(app.UpdateModelRegistrySettingsHandler))
137+
apiRouter.DELETE(ModelRegistrySettingsPath, app.AttachNamespace(app.DeleteModelRegistrySettingsHandler))
130138
}
131139

132140
// App Router

clients/ui/bff/internal/api/middleware.go

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111

1212
"github.com/google/uuid"
1313
"github.com/julienschmidt/httprouter"
14-
"github.com/kubeflow/model-registry/ui/bff/internal/config"
1514
"github.com/kubeflow/model-registry/ui/bff/internal/constants"
1615
helper "github.com/kubeflow/model-registry/ui/bff/internal/helpers"
1716
"github.com/kubeflow/model-registry/ui/bff/internal/integrations"
@@ -113,11 +112,18 @@ func (app *App) AttachRESTClient(next func(http.ResponseWriter, *http.Request, h
113112
app.badRequestResponse(w, r, fmt.Errorf("missing namespace in the context"))
114113
}
115114

116-
modelRegistryBaseURL, err := resolveModelRegistryURL(r.Context(), namespace, modelRegistryID, app.kubernetesClient, app.config)
115+
modelRegistry, err := app.repositories.ModelRegistry.GetModelRegistry(r.Context(), app.kubernetesClient, namespace, modelRegistryID)
117116
if err != nil {
118117
app.notFoundResponse(w, r)
119118
return
120119
}
120+
modelRegistryBaseURL := modelRegistry.ServerAddress
121+
122+
// If we are in dev mode, we need to resolve the server address to the local host
123+
// to allow the client to connect to the model registry via port forwarded from the cluster to the local machine.
124+
if app.config.DevMode {
125+
modelRegistryBaseURL = app.repositories.ModelRegistry.ResolveServerAddress("localhost", int32(app.config.DevModePort))
126+
}
121127

122128
// Set up a child logger for the rest client that automatically adds the request id to all statements for
123129
// tracing.
@@ -141,22 +147,6 @@ func (app *App) AttachRESTClient(next func(http.ResponseWriter, *http.Request, h
141147
}
142148
}
143149

144-
func resolveModelRegistryURL(sessionCtx context.Context, namespace string, serviceName string, client integrations.KubernetesClientInterface, config config.EnvConfig) (string, error) {
145-
146-
serviceDetails, err := client.GetServiceDetailsByName(sessionCtx, namespace, serviceName)
147-
if err != nil {
148-
return "", err
149-
}
150-
151-
if config.DevMode {
152-
serviceDetails.ClusterIP = "localhost"
153-
serviceDetails.HTTPPort = int32(config.DevModePort)
154-
}
155-
156-
url := fmt.Sprintf("http://%s:%d/api/model_registry/v1alpha3", serviceDetails.ClusterIP, serviceDetails.HTTPPort)
157-
return url, nil
158-
}
159-
160150
func (app *App) AttachNamespace(next func(http.ResponseWriter, *http.Request, httprouter.Params)) httprouter.Handle {
161151
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
162152
namespace := r.URL.Query().Get(string(constants.NamespaceHeaderParameterKey))

clients/ui/bff/internal/api/model_registry_handler.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ package api
22

33
import (
44
"fmt"
5+
"net/http"
6+
57
"github.com/julienschmidt/httprouter"
68
"github.com/kubeflow/model-registry/ui/bff/internal/constants"
79
"github.com/kubeflow/model-registry/ui/bff/internal/models"
8-
"net/http"
910
)
1011

1112
type ModelRegistryListEnvelope Envelope[[]models.ModelRegistryModel, None]
13+
type ModelRegistryEnvelope Envelope[models.ModelRegistryModel, None]
1214

13-
func (app *App) ModelRegistryHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
15+
func (app *App) GetAllModelRegistriesHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
1416

1517
namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string)
1618
if !ok || namespace == "" {

clients/ui/bff/internal/api/model_registry_handler_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"io"
8+
"net/http"
9+
"net/http/httptest"
10+
711
"github.com/kubeflow/model-registry/ui/bff/internal/constants"
812
"github.com/kubeflow/model-registry/ui/bff/internal/mocks"
913
"github.com/kubeflow/model-registry/ui/bff/internal/models"
1014
"github.com/kubeflow/model-registry/ui/bff/internal/repositories"
1115
. "github.com/onsi/ginkgo/v2"
1216
. "github.com/onsi/gomega"
13-
"io"
14-
"net/http"
15-
"net/http/httptest"
1617
)
1718

1819
var _ = Describe("TestModelRegistryHandler", func() {
@@ -38,7 +39,7 @@ var _ = Describe("TestModelRegistryHandler", func() {
3839
rr := httptest.NewRecorder()
3940

4041
By("creating the http request for the handler")
41-
testApp.ModelRegistryHandler(rr, req, nil)
42+
testApp.GetAllModelRegistriesHandler(rr, req, nil)
4243
rs := rr.Result()
4344
defer rs.Body.Close()
4445
body, err := io.ReadAll(rs.Body)
@@ -52,8 +53,8 @@ var _ = Describe("TestModelRegistryHandler", func() {
5253

5354
By("should match the expected model registries")
5455
var expected = []models.ModelRegistryModel{
55-
{Name: "model-registry", Description: "Model Registry Description", DisplayName: "Model Registry"},
56-
{Name: "model-registry-one", Description: "Model Registry One description", DisplayName: "Model Registry One"},
56+
{Name: "model-registry", Description: "Model Registry Description", DisplayName: "Model Registry", ServerAddress: "http://127.0.0.1:8080/api/model_registry/v1alpha3"},
57+
{Name: "model-registry-one", Description: "Model Registry One description", DisplayName: "Model Registry One", ServerAddress: "http://127.0.0.1:8080/api/model_registry/v1alpha3"},
5758
}
5859
Expect(actual.Data).To(ConsistOf(expected))
5960
})
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"time"
7+
8+
"github.com/julienschmidt/httprouter"
9+
"github.com/kubeflow/model-registry/ui/bff/internal/constants"
10+
helper "github.com/kubeflow/model-registry/ui/bff/internal/helpers"
11+
"github.com/kubeflow/model-registry/ui/bff/internal/models"
12+
)
13+
14+
type ModelRegistrySettingsListEnvelope Envelope[[]models.ModelRegistryKind, None]
15+
type ModelRegistrySettingsEnvelope Envelope[models.ModelRegistryKind, None]
16+
17+
func (app *App) GetAllModelRegistriesSettingsHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
18+
ctxLogger := helper.GetContextLoggerFromReq(r)
19+
ctxLogger.Info("This functionality is not implement yet. This is a STUB API to unblock frontend development")
20+
21+
namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string)
22+
if !ok || namespace == "" {
23+
app.badRequestResponse(w, r, fmt.Errorf("missing namespace in the context"))
24+
}
25+
26+
registries := []models.ModelRegistryKind{createSampleModelRegistry("model-registry", namespace),
27+
createSampleModelRegistry("model-registry-dora", namespace),
28+
createSampleModelRegistry("model-registry-bella", namespace)}
29+
30+
modelRegistryRes := ModelRegistrySettingsListEnvelope{
31+
Data: registries,
32+
}
33+
34+
err := app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil)
35+
36+
if err != nil {
37+
app.serverErrorResponse(w, r, err)
38+
}
39+
40+
}
41+
42+
func (app *App) GetModelRegistrySettingsHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
43+
ctxLogger := helper.GetContextLoggerFromReq(r)
44+
ctxLogger.Info("This functionality is not implement yet. This is a STUB API to unblock frontend development")
45+
46+
namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string)
47+
if !ok || namespace == "" {
48+
app.badRequestResponse(w, r, fmt.Errorf("missing namespace in the context"))
49+
}
50+
51+
modelId := ps.ByName(ModelRegistryId)
52+
registry := createSampleModelRegistry(modelId, namespace)
53+
54+
modelRegistryRes := ModelRegistrySettingsEnvelope{
55+
Data: registry,
56+
}
57+
58+
err := app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil)
59+
60+
if err != nil {
61+
app.serverErrorResponse(w, r, err)
62+
}
63+
}
64+
65+
func (app *App) CreateModelRegistrySettingsHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
66+
ctxLogger := helper.GetContextLoggerFromReq(r)
67+
ctxLogger.Info("This functionality is not implement yet. This is a STUB API to unblock frontend development")
68+
69+
namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string)
70+
if !ok || namespace == "" {
71+
app.badRequestResponse(w, r, fmt.Errorf("missing namespace in the context"))
72+
}
73+
74+
registry := createSampleModelRegistry("new-model-registry", namespace)
75+
76+
modelRegistryRes := ModelRegistrySettingsEnvelope{
77+
Data: registry,
78+
}
79+
80+
w.Header().Set("Location", r.URL.JoinPath(modelRegistryRes.Data.Metadata.Name).String())
81+
err := app.WriteJSON(w, http.StatusCreated, modelRegistryRes, nil)
82+
if err != nil {
83+
app.serverErrorResponse(w, r, fmt.Errorf("error writing JSON"))
84+
return
85+
}
86+
87+
}
88+
89+
func (app *App) UpdateModelRegistrySettingsHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
90+
ctxLogger := helper.GetContextLoggerFromReq(r)
91+
ctxLogger.Info("This functionality is not implement yet. This is a STUB API to unblock frontend development")
92+
93+
namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string)
94+
if !ok || namespace == "" {
95+
app.badRequestResponse(w, r, fmt.Errorf("missing namespace in the context"))
96+
}
97+
98+
modelId := ps.ByName(ModelRegistryId)
99+
registry := createSampleModelRegistry(modelId, namespace)
100+
101+
modelRegistryRes := ModelRegistrySettingsEnvelope{
102+
Data: registry,
103+
}
104+
105+
err := app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil)
106+
if err != nil {
107+
app.serverErrorResponse(w, r, fmt.Errorf("error writing JSON"))
108+
return
109+
}
110+
}
111+
112+
func (app *App) DeleteModelRegistrySettingsHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
113+
ctxLogger := helper.GetContextLoggerFromReq(r)
114+
ctxLogger.Info("This functionality is not implement yet. This is a STUB API to unblock frontend development")
115+
116+
namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string)
117+
if !ok || namespace == "" {
118+
app.badRequestResponse(w, r, fmt.Errorf("missing namespace in the context"))
119+
}
120+
121+
w.WriteHeader(200)
122+
}
123+
124+
// This function is a temporary function to create a sample model registry kind until we have a real implementation
125+
func createSampleModelRegistry(name string, namespace string) models.ModelRegistryKind {
126+
127+
creationTime, _ := time.Parse(time.RFC3339, "2024-03-14T08:01:42Z")
128+
lastTransitionTime, _ := time.Parse(time.RFC3339, "2024-03-22T09:30:02Z")
129+
130+
return models.ModelRegistryKind{
131+
APIVersion: "modelregistry.io/v1alpha1",
132+
Kind: "ModelRegistry",
133+
Metadata: models.Metadata{
134+
Name: name,
135+
Namespace: namespace,
136+
CreationTimestamp: creationTime,
137+
Annotations: map[string]string{},
138+
},
139+
Spec: models.ModelRegistrySpec{
140+
GRPC: models.EmptyObject{},
141+
REST: models.EmptyObject{},
142+
Istio: models.IstioConfig{
143+
Gateway: models.GatewayConfig{
144+
GRPC: models.GRPCConfig{
145+
TLS: models.EmptyObject{},
146+
},
147+
REST: models.RESTConfig{
148+
TLS: models.EmptyObject{},
149+
},
150+
},
151+
},
152+
DatabaseConfig: models.DatabaseConfig{
153+
DatabaseType: models.MySQL,
154+
Database: "model-registry",
155+
Host: "model-registry-db",
156+
//intentionally not set
157+
// PasswordSecret: models.PasswordSecret{
158+
// Key: "database-password",
159+
// Name: "model-registry-db",
160+
// },
161+
Port: 5432,
162+
SkipDBCreation: false,
163+
Username: "mlmduser",
164+
SSLRootCertificateConfigMap: "ssl-config-map",
165+
SSLRootCertificateSecret: "ssl-secret",
166+
},
167+
},
168+
Status: models.Status{
169+
Conditions: []models.Condition{
170+
{
171+
LastTransitionTime: lastTransitionTime,
172+
Message: "Deployment for custom resource " + name + " was successfully created",
173+
Reason: "CreatedDeployment",
174+
Status: "True",
175+
Type: "Progressing",
176+
},
177+
},
178+
},
179+
}
180+
}

clients/ui/bff/internal/mocks/k8s_mock.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ func (m *KubernetesClientMock) GetServiceDetails(sessionCtx context.Context, nam
192192
return nil, fmt.Errorf("failed to get service details: %w", err)
193193
}
194194

195+
//in the mock we are changing the cluster ip to localhost
196+
//to be able to connect with local model registry
195197
for i := range originalServices {
196198
originalServices[i].ClusterIP = "127.0.0.1"
197199
originalServices[i].HTTPPort = 8080
@@ -205,7 +207,8 @@ func (m *KubernetesClientMock) GetServiceDetailsByName(sessionCtx context.Contex
205207
if err != nil {
206208
return k8s.ServiceDetails{}, fmt.Errorf("failed to get service details: %w", err)
207209
}
208-
//changing from cluster service ip to localhost
210+
//in the mock we are changing the cluster ip to localhost
211+
//to be able to connect with local model registry
209212
originalService.ClusterIP = "127.0.0.1"
210213
originalService.HTTPPort = 8080
211214

0 commit comments

Comments
 (0)