Skip to content

Commit 666de2c

Browse files
ederignlucferbux
authored andcommitted
Refactor Kubernetes Client Factory and Introduce Red Hat Extensions
- Added a new function `NewKubernetesClientFactory` to handle client creation based on authentication method. - Introduced a new markdown file for documenting BFF handler extensions for downstream builds. - Implemented handler overrides for Model Registry Settings to support Red Hat-specific behavior. - Created a new repository for Model Registry Settings with Kubernetes integration. - Added methods for CRUD operations on Model Registry settings, including handling database secrets. - Implemented conversion functions for unstructured Kubernetes objects to strongly typed models. - Ensured that all new functionality adheres to existing interfaces and maintains backward compatibility.
1 parent f0419f2 commit 666de2c

File tree

16 files changed

+1930
-74
lines changed

16 files changed

+1930
-74
lines changed

clients/ui/bff/.vscode/launch.json

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"program": "${workspaceFolder}/bin/bff",
1010
"args": [
1111
"--port=4000",
12-
"--auth-method=internal",
12+
"--auth-method=user_token",
1313
"--auth-token-header=Authorization",
1414
"--auth-token-prefix=Bearer",
1515
"--static-assets-dir=./static",
@@ -30,7 +30,7 @@
3030
"program": "${workspaceFolder}/bin/bff",
3131
"args": [
3232
"--port=4000",
33-
"--auth-method=internal",
33+
"--auth-method=user_token",
3434
"--auth-token-header=Authorization",
3535
"--auth-token-prefix=Bearer",
3636
"--static-assets-dir=./static",
@@ -51,13 +51,34 @@
5151
"program": "${workspaceFolder}/bin/bff",
5252
"args": [
5353
"--port=4000",
54-
"--auth-method=internal",
54+
"--auth-method=user_token",
5555
"--auth-token-header=Authorization",
5656
"--auth-token-prefix=Bearer",
5757
"--static-assets-dir=./static",
5858
"--mock-k8s-client=false",
5959
"--mock-mr-client=false",
60-
"--dev-mode=false",
60+
"--dev-mode=true",
61+
"--dev-mode-port=8080",
62+
"--standalone-mode=true",
63+
"--log-level=info"
64+
],
65+
"preLaunchTask": "make build debug"
66+
},
67+
{
68+
"name": "XFW Debug BFF (MOCK_K8S_CLIENT=false, MOCK_MR_CLIENT=false)",
69+
"type": "go",
70+
"request": "launch",
71+
"mode": "exec",
72+
"program": "${workspaceFolder}/bin/bff",
73+
"args": [
74+
"--port=4000",
75+
"--auth-method=user_token_red_hat",
76+
"--auth-token-header=X-Forwarded-Access-Token",
77+
"--auth-token-prefix=",
78+
"--static-assets-dir=./static",
79+
"--mock-k8s-client=false",
80+
"--mock-mr-client=false",
81+
"--dev-mode=true",
6182
"--dev-mode-port=8080",
6283
"--standalone-mode=true",
6384
"--log-level=info"

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

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ const (
7575
CatalogSourcePreviewPath = ModelCatalogSettingsPathPrefix + "/source_preview"
7676
)
7777

78+
const (
79+
// TODO(upstream): Keep handler IDs unexported so the extension mechanism stays agnostic of downstream overrides.
80+
handlerModelRegistrySettingsListID HandlerID = "modelRegistrySettings:list"
81+
handlerModelRegistrySettingsCreateID HandlerID = "modelRegistrySettings:create"
82+
handlerModelRegistrySettingsGetID HandlerID = "modelRegistrySettings:get"
83+
handlerModelRegistrySettingsUpdateID HandlerID = "modelRegistrySettings:update"
84+
handlerModelRegistrySettingsDeleteID HandlerID = "modelRegistrySettings:delete"
85+
)
86+
7887
type App struct {
7988
config config.EnvConfig
8089
logger *slog.Logger
@@ -251,11 +260,36 @@ func (app *App) Routes() http.Handler {
251260
// SettingsPath endpoints are used to manage the model registry settings and create new model registries
252261
// We are still discussing the best way to create model registries in the community
253262
// But in the meantime, those endpoints are STUBs endpoints used to unblock the frontend development
254-
apiRouter.GET(ModelRegistrySettingsListPath, app.AttachNamespace(app.GetAllModelRegistriesSettingsHandler))
255-
apiRouter.POST(ModelRegistrySettingsListPath, app.AttachNamespace(app.CreateModelRegistrySettingsHandler))
256-
apiRouter.GET(ModelRegistrySettingsPath, app.AttachNamespace(app.GetModelRegistrySettingsHandler))
257-
apiRouter.PATCH(ModelRegistrySettingsPath, app.AttachNamespace(app.UpdateModelRegistrySettingsHandler))
258-
apiRouter.DELETE(ModelRegistrySettingsPath, app.AttachNamespace(app.DeleteModelRegistrySettingsHandler))
263+
apiRouter.GET(
264+
ModelRegistrySettingsListPath,
265+
app.handlerWithOverride(handlerModelRegistrySettingsListID, func() httprouter.Handle {
266+
return app.AttachNamespace(app.GetAllModelRegistriesSettingsHandler)
267+
}),
268+
)
269+
apiRouter.POST(
270+
ModelRegistrySettingsListPath,
271+
app.handlerWithOverride(handlerModelRegistrySettingsCreateID, func() httprouter.Handle {
272+
return app.AttachNamespace(app.CreateModelRegistrySettingsHandler)
273+
}),
274+
)
275+
apiRouter.GET(
276+
ModelRegistrySettingsPath,
277+
app.handlerWithOverride(handlerModelRegistrySettingsGetID, func() httprouter.Handle {
278+
return app.AttachNamespace(app.GetModelRegistrySettingsHandler)
279+
}),
280+
)
281+
apiRouter.PATCH(
282+
ModelRegistrySettingsPath,
283+
app.handlerWithOverride(handlerModelRegistrySettingsUpdateID, func() httprouter.Handle {
284+
return app.AttachNamespace(app.UpdateModelRegistrySettingsHandler)
285+
}),
286+
)
287+
apiRouter.DELETE(
288+
ModelRegistrySettingsPath,
289+
app.handlerWithOverride(handlerModelRegistrySettingsDeleteID, func() httprouter.Handle {
290+
return app.AttachNamespace(app.DeleteModelRegistrySettingsHandler)
291+
}),
292+
)
259293

260294
//SettingsPath: Certificate endpoints
261295
apiRouter.GET(CertificatesPath, app.AttachNamespace(app.GetCertificatesHandler))
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package api
2+
3+
import (
4+
"log/slog"
5+
"sync"
6+
7+
"github.com/julienschmidt/httprouter"
8+
)
9+
10+
// HandlerID identifies an overridable HTTP handler.
11+
// TODO(upstream): Keep this type exported so downstream code can reference arbitrary handler keys without
12+
// requiring upstream to enumerate them.
13+
type HandlerID string
14+
15+
// HandlerFactory builds a router handler that has access to the App instance.
16+
// Implementations can opt to call buildDefault() to reuse the default upstream handler.
17+
type HandlerFactory func(app *App, buildDefault func() httprouter.Handle) httprouter.Handle
18+
19+
var (
20+
handlerOverrideMu sync.RWMutex
21+
handlerOverrides = map[HandlerID]HandlerFactory{}
22+
)
23+
24+
// RegisterHandlerOverride allows downstream code to override a specific handler.
25+
// TODO(downstream): Call this from vendor packages (e.g., internal/redhat/handlers) to inject custom behavior.
26+
// Calling this function multiple times for the same id will replace the previous override.
27+
func RegisterHandlerOverride(id HandlerID, factory HandlerFactory) {
28+
handlerOverrideMu.Lock()
29+
defer handlerOverrideMu.Unlock()
30+
handlerOverrides[id] = factory
31+
}
32+
33+
func getHandlerOverride(id HandlerID) HandlerFactory {
34+
handlerOverrideMu.RLock()
35+
defer handlerOverrideMu.RUnlock()
36+
return handlerOverrides[id]
37+
}
38+
39+
// handlerWithOverride returns the handler registered for the given id or builds the default one.
40+
// TODO(upstream): This glue stays upstream so the router keeps working even when no downstream overrides exist.
41+
func (app *App) handlerWithOverride(id HandlerID, buildDefault func() httprouter.Handle) httprouter.Handle {
42+
if factory := getHandlerOverride(id); factory != nil {
43+
app.logHandlerOverride(id)
44+
return factory(app, buildDefault)
45+
}
46+
return buildDefault()
47+
}
48+
49+
func (app *App) logHandlerOverride(id HandlerID) {
50+
if app == nil || app.logger == nil {
51+
return
52+
}
53+
app.logger.Debug("Using handler override", slog.String("handlerID", string(id)))
54+
}

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

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ type ModelRegistryAndCredentialsSettingsEnvelope Envelope[models.ModelRegistryAn
1818
type ModelRegistrySettingsPayloadEnvelope Envelope[models.ModelRegistrySettingsPayload, None]
1919

2020
func (app *App) GetAllModelRegistriesSettingsHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
21-
ctxLogger := helper.GetContextLoggerFromReq(r)
22-
ctxLogger.Info("This functionality is not implement yet. This is a STUB API to unblock frontend development")
2321

2422
namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string)
2523
if !ok || namespace == "" {
@@ -36,17 +34,17 @@ func (app *App) GetAllModelRegistriesSettingsHandler(w http.ResponseWriter, r *h
3634
Key: "ssl-secret-key",
3735
}
3836

39-
registries := []models.ModelRegistryKind{createSampleModelRegistry("model-registry", namespace, &sslRootCertificateConfigMap, nil),
37+
registries := []models.ModelRegistryKind{
38+
createSampleModelRegistry("model-registry", namespace, &sslRootCertificateConfigMap, nil),
4039
createSampleModelRegistry("model-registry-dora", namespace, nil, &sslRootCertificateSecret),
41-
createSampleModelRegistry("model-registry-bella", namespace, nil, nil)}
40+
createSampleModelRegistry("model-registry-bella", namespace, nil, nil),
41+
}
4242

4343
modelRegistryRes := ModelRegistrySettingsListEnvelope{
4444
Data: registries,
4545
}
4646

47-
err := app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil)
48-
49-
if err != nil {
47+
if err := app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil); err != nil {
5048
app.serverErrorResponse(w, r, err)
5149
}
5250

@@ -73,9 +71,7 @@ func (app *App) GetModelRegistrySettingsHandler(w http.ResponseWriter, r *http.R
7371
Data: modelRegistryWithCreds,
7472
}
7573

76-
err := app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil)
77-
78-
if err != nil {
74+
if err := app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil); err != nil {
7975
app.serverErrorResponse(w, r, err)
8076
}
8177
}
@@ -94,31 +90,18 @@ func (app *App) CreateModelRegistrySettingsHandler(w http.ResponseWriter, r *htt
9490
app.serverErrorResponse(w, r, fmt.Errorf("error decoding JSON:: %v", err.Error()))
9591
return
9692
}
97-
98-
var modelRegistryName = envelope.Data.ModelRegistry.Metadata.Name
99-
100-
if modelRegistryName == "" {
101-
app.badRequestResponse(w, r, fmt.Errorf("model registry name is required"))
102-
return
103-
}
104-
105-
ctxLogger.Info("Creating model registry", "name", modelRegistryName)
106-
107-
// For now, we're using the stub implementation, but we'd use envelope.Data.ModelRegistry
108-
// and other fields from the payload in a real implementation
93+
modelRegistryName := envelope.Data.ModelRegistry.Metadata.Name
10994
registry := createSampleModelRegistry(modelRegistryName, namespace, nil, nil)
11095

11196
modelRegistryRes := ModelRegistrySettingsEnvelope{
11297
Data: registry,
11398
}
11499

115100
w.Header().Set("Location", r.URL.JoinPath(modelRegistryRes.Data.Metadata.Name).String())
116-
writeErr := app.WriteJSON(w, http.StatusCreated, modelRegistryRes, nil)
117-
if writeErr != nil {
101+
if err := app.WriteJSON(w, http.StatusCreated, modelRegistryRes, nil); err != nil {
118102
app.serverErrorResponse(w, r, fmt.Errorf("error writing JSON"))
119103
return
120104
}
121-
122105
}
123106

124107
func (app *App) UpdateModelRegistrySettingsHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
@@ -137,8 +120,7 @@ func (app *App) UpdateModelRegistrySettingsHandler(w http.ResponseWriter, r *htt
137120
Data: registry,
138121
}
139122

140-
err := app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil)
141-
if err != nil {
123+
if err := app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil); err != nil {
142124
app.serverErrorResponse(w, r, fmt.Errorf("error writing JSON"))
143125
return
144126
}
@@ -161,9 +143,7 @@ func (app *App) DeleteModelRegistrySettingsHandler(w http.ResponseWriter, r *htt
161143
Data: registry,
162144
}
163145

164-
err := app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil)
165-
166-
if err != nil {
146+
if err := app.WriteJSON(w, http.StatusOK, modelRegistryRes, nil); err != nil {
167147
app.serverErrorResponse(w, r, err)
168148
}
169149

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"log/slog"
8+
9+
"github.com/kubeflow/model-registry/ui/bff/internal/config"
10+
k8s "github.com/kubeflow/model-registry/ui/bff/internal/integrations/kubernetes"
11+
"github.com/kubeflow/model-registry/ui/bff/internal/repositories"
12+
)
13+
14+
// BadRequest exposes the internal bad request helper for extensions.
15+
func (app *App) BadRequest(w http.ResponseWriter, r *http.Request, err error) {
16+
if app == nil {
17+
return
18+
}
19+
app.badRequestResponse(w, r, err)
20+
}
21+
22+
// ServerError exposes the internal server error helper for extensions.
23+
func (app *App) ServerError(w http.ResponseWriter, r *http.Request, err error) {
24+
if app == nil {
25+
return
26+
}
27+
app.serverErrorResponse(w, r, err)
28+
}
29+
30+
// NotImplemented writes a standard placeholder response for unimplemented endpoints.
31+
func (app *App) NotImplemented(w http.ResponseWriter, r *http.Request, feature string) {
32+
app.serverErrorResponse(w, r, fmt.Errorf("%s is not implemented", feature))
33+
}
34+
35+
// Config exposes the application configuration for extensions.
36+
func (app *App) Config() config.EnvConfig {
37+
return app.config
38+
}
39+
40+
// Logger exposes the application logger for extensions.
41+
func (app *App) Logger() *slog.Logger {
42+
return app.logger
43+
}
44+
45+
// KubernetesClientFactory exposes the k8s factory for extensions.
46+
func (app *App) KubernetesClientFactory() k8s.KubernetesClientFactory {
47+
return app.kubernetesClientFactory
48+
}
49+
50+
// Repositories exposes the repositories container for extensions.
51+
func (app *App) Repositories() *repositories.Repositories {
52+
return app.repositories
53+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package api
2+
3+
import (
4+
"io"
5+
"log/slog"
6+
7+
"github.com/kubeflow/model-registry/ui/bff/internal/config"
8+
k8s "github.com/kubeflow/model-registry/ui/bff/internal/integrations/kubernetes"
9+
"github.com/kubeflow/model-registry/ui/bff/internal/repositories"
10+
)
11+
12+
// NewTestApp exposes a minimal constructor that allows tests and downstream
13+
// extensions to configure specific App dependencies without invoking the
14+
// production bootstrap logic.
15+
func NewTestApp(cfg config.EnvConfig, logger *slog.Logger, factory k8s.KubernetesClientFactory, repos *repositories.Repositories) *App {
16+
if logger == nil {
17+
logger = slog.New(slog.NewTextHandler(io.Discard, nil))
18+
}
19+
return &App{
20+
config: cfg,
21+
logger: logger,
22+
kubernetesClientFactory: factory,
23+
repositories: repos,
24+
}
25+
}

clients/ui/bff/internal/integrations/kubernetes/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package kubernetes
22

33
import (
44
"context"
5+
56
corev1 "k8s.io/api/core/v1"
67
)
78

0 commit comments

Comments
 (0)