Skip to content

Commit 1a10968

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. feat: adapt the code to upstream Signed-off-by: lucferbux <[email protected]>
1 parent 6920bc9 commit 1a10968

File tree

10 files changed

+289
-69
lines changed

10 files changed

+289
-69
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/docs/extensions.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# BFF handler extensions
2+
3+
Some downstream builds (for example, the RHOAI dashboard) need to add behavior that does not belong upstream yet. Instead of maintaining long-lived forks, the BFF now exposes a simple extension registry so downstream code can override individual handlers while reusing the rest of the stack.
4+
5+
## Core concepts
6+
7+
- **Handler IDs** – Each overridable endpoint exposes a stable `HandlerID` constant (see `internal/api/extensions.go`). Model Registry Settings routes are wired first.
8+
- **Factories** – Downstream packages call `api.RegisterHandlerOverride(id, factory)` inside `init()`. Factories receive the `*api.App` plus the default handler builder, so you can either replace the handler entirely or fall back to upstream logic.
9+
- **Dependencies** – The `App` exposes read-only accessors for configuration, repositories, and the Kubernetes client factory. It also exposes helper methods such as `BadRequest`, `ServerError`, and `WriteJSON` so overrides can follow the same conventions as upstream handlers.
10+
11+
## Ownership boundaries
12+
13+
- **Upstream-only artifacts** live under `internal/api`, `internal/repositories`, and the default handler tree. These packages must remain vendor-neutral and keep their existing contracts intact so downstream imports keep compiling.
14+
- **Downstream-only artifacts** live under `clients/ui/bff/internal/<override-folder>` (and sibling vendor folders, if they are ever added). Any logic that assumes Red Hat credentials, namespaces, or controllers must stay here so other distributions do not pick it up accidentally.
15+
- **Shared interfaces** (for example repository interfaces or the handler override registry itself) stay upstream. Only implementers that are specific to a vendor move downstream.
16+
17+
Use this rule of thumb: if a change requires Red Hat-only RBAC, Kubernetes resources, or APIs that are invisible to open-source users, keep it downstream. Everything else should be proposed upstream.
18+
19+
## Minimal example
20+
21+
```go
22+
package handlers
23+
24+
import (
25+
"fmt"
26+
"net/http"
27+
28+
"github.com/julienschmidt/httprouter"
29+
30+
"github.com/kubeflow/model-registry/ui/bff/internal/api"
31+
"github.com/kubeflow/model-registry/ui/bff/internal/constants"
32+
)
33+
34+
const (
35+
modelRegistrySettingsListHandlerID = api.HandlerID("modelRegistrySettings:list")
36+
)
37+
38+
func init() {
39+
api.RegisterHandlerOverride(modelRegistrySettingsListHandlerID, overrideModelRegistrySettingsList)
40+
}
41+
42+
func overrideModelRegistrySettingsList(app *api.App, _ func() httprouter.Handle) httprouter.Handle {
43+
return app.AttachNamespace(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
44+
namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string)
45+
if !ok || namespace == "" {
46+
app.BadRequest(w, r, fmt.Errorf("missing namespace"))
47+
return
48+
}
49+
50+
client, err := app.KubernetesClientFactory().GetClient(r.Context())
51+
if err != nil {
52+
app.ServerError(w, r, fmt.Errorf("failed to build client: %w", err))
53+
return
54+
}
55+
56+
// Use the client to fetch data from Kubernetes and build the response.
57+
// Downstream repositories live in internal/<override-repository>/repositories.
58+
_ = client // placeholder for actual implementation
59+
60+
resp := api.ModelRegistrySettingsListEnvelope{Data: nil}
61+
if err := app.WriteJSON(w, http.StatusOK, resp, nil); err != nil {
62+
app.ServerError(w, r, err)
63+
}
64+
})
65+
}
66+
```
67+
68+
Package registration is purely compile-time: add a blank import in `clients/ui/bff/cmd/main.go` for the downstream handlers package. When the package is imported, overrides registered in `init()` are automatically active—no configuration flags required. The `buildDefault` parameter is available if an override needs to delegate to upstream logic conditionally.
69+
70+
## Managing downstream overrides
71+
72+
- **Structure** – Place handler factories below `clients/ui/bff/internal/<override-folder>/handlers`, and keep any repository or helper implementations under `clients/ui/bff/internal/<override-folder>/repositories`. This mirrors the upstream layout so the APIs remain familiar.
73+
- **Activation** – Overrides are active whenever their package is imported. Use a blank import (e.g., `_ "github.com/.../internal/<override-folder>/handlers"`) in the main entry point to enable them. No configuration flags are needed.
74+
- **Conditional delegation** – If an override needs to fall back to upstream logic under certain conditions, call `buildDefault()`. Otherwise, the downstream handler runs unconditionally.
75+
- **Shared clients** – Build Kubernetes or database clients via `app.KubernetesClientFactory()` or other upstream factories. Never duplicate client configuration downstream; add capabilities to the upstream factory instead when needed.
76+
- **Testing** – Keep unit and integration tests downstream next to the overrides. Use the upstream interfaces to mock dependencies the same way default handlers do.
77+
78+
## Change workflow
79+
80+
1. **Add the handler ID upstream** – introduce a new `HandlerID` constant and wrap the router registration with `app.handlerWithOverride`. Document the ID in this file under *Current coverage*.
81+
2. **Introduce downstream logic** – implement handler factories (and any supporting repositories) under `clients/ui/bff/internal/<override-folder>`. Register them in the package `init()` by calling `api.RegisterHandlerOverride`.
82+
3. **Wire repositories** – if the downstream handler needs bespoke storage logic, implement a downstream repository that satisfies the upstream interface and expose it via `app.Repositories()` overrides.
83+
4. **Document and test** – update this guide when extending coverage, and add downstream tests to catch regressions before shipping.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
//nolint:unused // Used by downstream implementations
34+
func getHandlerOverride(id HandlerID) HandlerFactory {
35+
handlerOverrideMu.RLock()
36+
defer handlerOverrideMu.RUnlock()
37+
return handlerOverrides[id]
38+
}
39+
40+
// handlerWithOverride returns the handler registered for the given id or builds the default one.
41+
// TODO(upstream): This glue stays upstream so the router keeps working even when no downstream overrides exist.
42+
//
43+
//nolint:unused // Used by downstream implementations
44+
func (app *App) handlerWithOverride(id HandlerID, buildDefault func() httprouter.Handle) httprouter.Handle {
45+
if factory := getHandlerOverride(id); factory != nil {
46+
app.logHandlerOverride(id)
47+
return factory(app, buildDefault)
48+
}
49+
return buildDefault()
50+
}
51+
52+
//nolint:unused // Used by downstream implementations
53+
func (app *App) logHandlerOverride(id HandlerID) {
54+
if app == nil || app.logger == nil {
55+
return
56+
}
57+
app.logger.Debug("Using handler override", slog.String("handlerID", string(id)))
58+
}

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)