Skip to content

Commit 1711fec

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 bde3b0f commit 1711fec

File tree

8 files changed

+588
-63
lines changed

8 files changed

+588
-63
lines changed

clients/ui/bff/docs/extensions.md

Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
# BFF Handler Extensions
2+
3+
Some downstream builds need to add behavior that does not belong upstream yet. Instead of maintaining long-lived forks, the BFF exposes a simple extension registry so downstream code can override individual handlers while reusing the rest of the stack.
4+
5+
## Table of Contents
6+
7+
- [Core Concepts](#core-concepts)
8+
- [Ownership Boundaries](#ownership-boundaries)
9+
- [Available App Methods](#available-app-methods)
10+
- [Step-by-Step: Overriding an Existing Handler](#step-by-step-overriding-an-existing-handler)
11+
- [Step-by-Step: Adding a Downstream-Only Route](#step-by-step-adding-a-downstream-only-route)
12+
- [Accessing the Kubernetes Client](#accessing-the-kubernetes-client)
13+
- [Importing Upstream Types](#importing-upstream-types)
14+
- [Testing Downstream Overrides](#testing-downstream-overrides)
15+
- [Current Handler ID Coverage](#current-handler-id-coverage)
16+
17+
---
18+
19+
## Core Concepts
20+
21+
- **Handler IDs** – Each overridable endpoint has a stable `HandlerID` string constant defined upstream in `internal/api/app.go`. Downstream code references these IDs to register overrides.
22+
- **Handler Factories** – Downstream packages call `api.RegisterHandlerOverride(id, factory)` inside `init()`. Factories receive the `*api.App` plus a `buildDefault` function, so you can either replace the handler entirely or fall back to upstream logic conditionally.
23+
- **Downstream-Only Routes** – Some endpoints exist upstream only as stubs returning 501 Not Implemented. These are designed to be fully implemented downstream. The route is wired upstream, but the real logic lives in the override.
24+
- **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.
25+
26+
---
27+
28+
## Ownership Boundaries
29+
30+
- **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.
31+
- **Downstream-only artifacts** live under `clients/ui/bff/internal/<vendor>/` (e.g., `internal/redhat/`). Any logic that assumes vendor-specific credentials, namespaces, or controllers must stay here so other distributions do not pick it up accidentally.
32+
- **Shared interfaces** (for example, repository interfaces or the handler override registry itself) stay upstream. Only implementations specific to a vendor move downstream.
33+
34+
**Rule of thumb:** If a change requires vendor-only RBAC, Kubernetes resources, or APIs invisible to open-source users, keep it downstream. Everything else should be proposed upstream.
35+
36+
---
37+
38+
## Available App Methods
39+
40+
The `*api.App` instance provides these exported methods for use in downstream handlers:
41+
42+
| Method | Description |
43+
|--------|-------------|
44+
| `app.Config()` | Returns `config.EnvConfig` with deployment settings |
45+
| `app.Logger()` | Returns `*slog.Logger` for structured logging |
46+
| `app.KubernetesClientFactory()` | Returns factory to build Kubernetes clients |
47+
| `app.Repositories()` | Returns `*repositories.Repositories` for data access |
48+
| `app.WriteJSON(w, status, data, headers)` | Writes JSON response with proper content-type |
49+
| `app.ReadJSON(w, r, dst)` | Parses JSON request body into destination struct |
50+
| `app.BadRequest(w, r, err)` | Returns HTTP 400 with error message |
51+
| `app.ServerError(w, r, err)` | Returns HTTP 500 with error message |
52+
| `app.NotImplemented(w, r, feature)` | Returns HTTP 501 for unimplemented features |
53+
| `app.AttachNamespace(handler)` | Middleware that extracts `namespace` query param into context |
54+
55+
---
56+
57+
## Step-by-Step: Overriding an Existing Handler
58+
59+
This workflow overrides an upstream handler with downstream-specific logic.
60+
61+
### 1. Add the blank import in `main.go`
62+
63+
In `clients/ui/bff/cmd/main.go`, add a blank import for your handlers package:
64+
65+
```go
66+
import (
67+
// ... other imports ...
68+
69+
// Import downstream handlers to register overrides via init()
70+
_ "github.com/kubeflow/model-registry/ui/bff/internal/redhat/handlers"
71+
)
72+
```
73+
74+
### 2. Locate the Handler ID upstream
75+
76+
Handler IDs are defined in `internal/api/app.go`. For example, Model Registry Settings endpoints:
77+
78+
```go
79+
const (
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+
```
87+
88+
### 3. Create the downstream handler file
89+
90+
Create a file in `internal/<vendor>/handlers/` (e.g., `internal/redhat/handlers/my_handler.go`):
91+
92+
```go
93+
package handlers
94+
95+
import (
96+
"fmt"
97+
"net/http"
98+
99+
"github.com/julienschmidt/httprouter"
100+
101+
"github.com/kubeflow/model-registry/ui/bff/internal/api"
102+
"github.com/kubeflow/model-registry/ui/bff/internal/constants"
103+
)
104+
105+
// Mirror the upstream handler ID string
106+
const myHandlerID = api.HandlerID("modelRegistrySettings:list")
107+
108+
func init() {
109+
api.RegisterHandlerOverride(myHandlerID, myOverrideFactory)
110+
}
111+
112+
func myOverrideFactory(app *api.App, buildDefault func() httprouter.Handle) httprouter.Handle {
113+
// Optionally fall back to upstream when mocking
114+
if app.Config().MockK8Client {
115+
return buildDefault()
116+
}
117+
118+
return app.AttachNamespace(func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
119+
namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string)
120+
if !ok || namespace == "" {
121+
app.BadRequest(w, r, fmt.Errorf("missing namespace"))
122+
return
123+
}
124+
125+
// Your downstream implementation here
126+
resp := api.ModelRegistrySettingsListEnvelope{Data: nil}
127+
if err := app.WriteJSON(w, http.StatusOK, resp, nil); err != nil {
128+
app.ServerError(w, r, err)
129+
}
130+
})
131+
}
132+
```
133+
134+
---
135+
136+
## Step-by-Step: Adding a Downstream-Only Route
137+
138+
For routes that have **no real upstream implementation** (upstream returns 501 Not Implemented), use this pattern.
139+
140+
### 1. Upstream defines the route and stub handler
141+
142+
In `internal/api/app.go`, the route is defined with a handler that returns 501:
143+
144+
```go
145+
// Path constant
146+
const KubernetesServicesListPath = SettingsPath + "/services"
147+
148+
// Handler ID
149+
const handlerKubernetesServicesListID HandlerID = "kubernetes:services:list"
150+
151+
// Route registration in Routes()
152+
apiRouter.GET(
153+
KubernetesServicesListPath,
154+
app.handlerWithOverride(handlerKubernetesServicesListID, func() httprouter.Handle {
155+
return app.AttachNamespace(app.kubernetesServicesNotImplementedHandler)
156+
}),
157+
)
158+
```
159+
160+
The stub handler in `internal/api/kubernetes_resources_handler.go`:
161+
162+
```go
163+
// Generic handler for endpoints not implemented upstream
164+
func (app *App) endpointNotImplementedHandler(feature string) func(http.ResponseWriter, *http.Request, httprouter.Params) {
165+
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
166+
app.NotImplemented(w, r, feature)
167+
}
168+
}
169+
```
170+
171+
### 2. Downstream provides the real implementation
172+
173+
In `internal/redhat/handlers/kubernetes_services.go`:
174+
175+
```go
176+
package handlers
177+
178+
import (
179+
"fmt"
180+
"net/http"
181+
182+
"github.com/julienschmidt/httprouter"
183+
"github.com/kubeflow/model-registry/ui/bff/internal/api"
184+
"github.com/kubeflow/model-registry/ui/bff/internal/constants"
185+
)
186+
187+
const kubernetesServicesListHandlerID = api.HandlerID("kubernetes:services:list")
188+
189+
func init() {
190+
api.RegisterHandlerOverride(kubernetesServicesListHandlerID, overrideKubernetesServicesList)
191+
}
192+
193+
func overrideKubernetesServicesList(app *api.App, buildDefault func() httprouter.Handle) httprouter.Handle {
194+
// Fall back to stub when K8s client is mocked
195+
if app.Config().MockK8Client {
196+
return buildDefault()
197+
}
198+
199+
return app.AttachNamespace(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
200+
namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string)
201+
if !ok || namespace == "" {
202+
app.BadRequest(w, r, fmt.Errorf("missing namespace"))
203+
return
204+
}
205+
206+
client, err := app.KubernetesClientFactory().GetClient(r.Context())
207+
if err != nil {
208+
app.ServerError(w, r, fmt.Errorf("failed to get client: %w", err))
209+
return
210+
}
211+
212+
// Real implementation using the Kubernetes client
213+
services, err := client.GetServiceDetails(r.Context(), namespace)
214+
if err != nil {
215+
app.ServerError(w, r, err)
216+
return
217+
}
218+
219+
// Build and return response
220+
items := make([]api.KubernetesServiceItem, 0, len(services))
221+
for _, svc := range services {
222+
items = append(items, api.KubernetesServiceItem{
223+
Name: svc.Name,
224+
Namespace: namespace,
225+
})
226+
}
227+
228+
resp := api.KubernetesServicesListEnvelope{Data: items}
229+
if err := app.WriteJSON(w, http.StatusOK, resp, nil); err != nil {
230+
app.ServerError(w, r, err)
231+
}
232+
})
233+
}
234+
```
235+
236+
---
237+
238+
## Accessing the Kubernetes Client
239+
240+
To interact with Kubernetes resources in your handler:
241+
242+
```go
243+
func myHandler(app *api.App) httprouter.Handle {
244+
return app.AttachNamespace(func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
245+
// Get the Kubernetes client from the factory
246+
client, err := app.KubernetesClientFactory().GetClient(r.Context())
247+
if err != nil {
248+
app.ServerError(w, r, fmt.Errorf("failed to get Kubernetes client: %w", err))
249+
return
250+
}
251+
252+
// Use the client to interact with Kubernetes
253+
// The client interface is defined in internal/integrations/kubernetes
254+
namespace := r.Context().Value(constants.NamespaceHeaderParameterKey).(string)
255+
256+
// Example: Get service details
257+
services, err := client.GetServiceDetails(r.Context(), namespace)
258+
if err != nil {
259+
app.ServerError(w, r, err)
260+
return
261+
}
262+
263+
// Process services...
264+
})
265+
}
266+
```
267+
268+
The `KubernetesClientInterface` provides methods for common operations. Check `internal/integrations/kubernetes/client.go` for available methods.
269+
270+
---
271+
272+
## Importing Upstream Types
273+
274+
When building downstream handlers, you'll commonly import these packages:
275+
276+
```go
277+
import (
278+
// Core API types and App
279+
"github.com/kubeflow/model-registry/ui/bff/internal/api"
280+
281+
// Configuration types
282+
"github.com/kubeflow/model-registry/ui/bff/internal/config"
283+
284+
// Context keys for namespace, identity, etc.
285+
"github.com/kubeflow/model-registry/ui/bff/internal/constants"
286+
287+
// Kubernetes client interface
288+
k8s "github.com/kubeflow/model-registry/ui/bff/internal/integrations/kubernetes"
289+
290+
// Data models
291+
"github.com/kubeflow/model-registry/ui/bff/internal/models"
292+
293+
// Upstream repositories (if needed)
294+
"github.com/kubeflow/model-registry/ui/bff/internal/repositories"
295+
296+
// Router
297+
"github.com/julienschmidt/httprouter"
298+
)
299+
```
300+
301+
**Commonly used types from `api` package:**
302+
303+
- `api.App` – Main application instance
304+
- `api.HandlerID` – Type for handler identifiers
305+
- `api.HandlerFactory` – Signature for override factories
306+
- `api.RegisterHandlerOverride()` – Register an override
307+
- Envelope types like `api.ModelRegistrySettingsListEnvelope`, `api.KubernetesServicesListEnvelope`
308+
309+
---
310+
311+
## Testing Downstream Overrides
312+
313+
Keep unit tests next to your override implementations in the downstream package.
314+
315+
### Testing with mocked Kubernetes client
316+
317+
When `MockK8Client=true`, your handlers should fall back to upstream stubs or return mock data:
318+
319+
```go
320+
func TestMyHandler_MockMode(t *testing.T) {
321+
// Create app with mocked K8s client
322+
cfg := config.EnvConfig{
323+
MockK8Client: true,
324+
}
325+
326+
// Your handler factory should return buildDefault() or mock behavior
327+
// when app.Config().MockK8Client is true
328+
}
329+
```
330+
331+
### Pattern for conditional override activation
332+
333+
```go
334+
func shouldUseDownstreamOverrides(app *api.App) bool {
335+
if app == nil {
336+
return false
337+
}
338+
// When K8s client is mocked, use upstream stub handlers
339+
return !app.Config().MockK8Client
340+
}
341+
342+
func myOverrideFactory(app *api.App, buildDefault func() httprouter.Handle) httprouter.Handle {
343+
// Fall back to upstream stub when K8s is mocked
344+
if !shouldUseDownstreamOverrides(app) {
345+
return buildDefault()
346+
}
347+
348+
// Real implementation
349+
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
350+
// ...
351+
}
352+
}
353+
```
354+
355+
### Test file structure
356+
357+
```
358+
internal/redhat/handlers/
359+
├── model_registry_settings.go
360+
├── model_registry_settings_test.go
361+
├── kubernetes_services.go
362+
└── kubernetes_services_test.go
363+
```
364+
365+
---
366+
367+
## Current Handler ID Coverage
368+
369+
These handler IDs are currently wired with `handlerWithOverride()` upstream:
370+
371+
| Handler ID | HTTP Method | Path | Description |
372+
|------------|-------------|------|-------------|
373+
| `modelRegistrySettings:list` | GET | `/api/v1/settings/model_registry` | List all model registries |
374+
| `modelRegistrySettings:create` | POST | `/api/v1/settings/model_registry` | Create a model registry |
375+
| `modelRegistrySettings:get` | GET | `/api/v1/settings/model_registry/:model_registry_id` | Get a model registry |
376+
| `modelRegistrySettings:update` | PATCH | `/api/v1/settings/model_registry/:model_registry_id` | Update a model registry |
377+
| `modelRegistrySettings:delete` | DELETE | `/api/v1/settings/model_registry/:model_registry_id` | Delete a model registry |
378+
| `kubernetes:services:list` | GET | `/api/v1/settings/services` | List Kubernetes services (downstream-only) |
379+
380+
---
381+
382+
## Change Workflow Summary
383+
384+
### To override an existing handler:
385+
386+
1. Find the handler ID in `internal/api/app.go`
387+
2. Create handler file in `internal/<vendor>/handlers/`
388+
3. Register override in `init()` with `api.RegisterHandlerOverride()`
389+
4. Add blank import in `cmd/main.go` (if not already present)
390+
391+
### To add a downstream-only route:
392+
393+
1. Define route path constant and handler ID in `internal/api/app.go`
394+
2. Create stub handler returning 501 in `internal/api/`
395+
3. Wire route with `handlerWithOverride()` in `Routes()`
396+
4. Implement real handler in `internal/<vendor>/handlers/`
397+
5. Register override in `init()` with `api.RegisterHandlerOverride()`

0 commit comments

Comments
 (0)