From 431e31bb139ddd5e38080e6b95396082a083c334 Mon Sep 17 00:00:00 2001 From: Eder Ignatowicz Date: Fri, 13 Dec 2024 16:10:59 -0500 Subject: [PATCH 1/2] feat(bff): list user namespaces in dev mode Signed-off-by: Eder Ignatowicz --- clients/ui/bff/README.md | 7 +- clients/ui/bff/internal/api/app.go | 15 ++- .../internal/api/healthcheck__handler_test.go | 6 +- .../bff/internal/api/healthcheck_handler.go | 9 +- clients/ui/bff/internal/api/middleware.go | 48 ++++++-- .../ui/bff/internal/api/namespaces_handler.go | 36 ++++++ .../internal/api/namespaces_handler_test.go | 113 ++++++++++++++++++ clients/ui/bff/internal/api/test_utils.go | 4 +- clients/ui/bff/internal/api/user_handler.go | 8 +- .../ui/bff/internal/api/user_handler_test.go | 14 ++- clients/ui/bff/internal/integrations/k8s.go | 41 +++++++ clients/ui/bff/internal/mocks/k8s_mock.go | 2 +- clients/ui/bff/internal/models/namespace.go | 11 ++ .../ui/bff/internal/repositories/namespace.go | 28 +++++ .../bff/internal/repositories/repositories.go | 2 + 15 files changed, 311 insertions(+), 33 deletions(-) create mode 100644 clients/ui/bff/internal/api/namespaces_handler.go create mode 100644 clients/ui/bff/internal/api/namespaces_handler_test.go create mode 100644 clients/ui/bff/internal/models/namespace.go create mode 100644 clients/ui/bff/internal/repositories/namespace.go diff --git a/clients/ui/bff/README.md b/clients/ui/bff/README.md index fa2ad679..f700a52a 100644 --- a/clients/ui/bff/README.md +++ b/clients/ui/bff/README.md @@ -58,6 +58,7 @@ make docker-build |----------------------------------------------------------------------------------------------|----------------------------------------------|-------------------------------------------------------------| | GET /v1/healthcheck | HealthcheckHandler | Show application information. | | GET /v1/user | UserHandler | Show "kubeflow-user-id" from header information. | +| GET /v1/namespaces | NamespacesHandler | Get all user namespaces. | | GET /v1/model_registry | ModelRegistryHandler | Get all model registries, | | GET /v1/model_registry/{model_registry_id}/registered_models | GetAllRegisteredModelsHandler | Gets a list of all RegisteredModel entities. | | POST /v1/model_registry/{model_registry_id}/registered_models | CreateRegisteredModelHandler | Create a RegisteredModel entity. | @@ -83,6 +84,10 @@ curl -i -H "kubeflow-userid: user@example.com" localhost:4000/api/v1/healthcheck curl -i -H "kubeflow-userid: user@example.com" localhost:4000/api/v1/user ``` ``` +# GET /v1/namespaces +curl -i -H "kubeflow-userid: user@example.com" localhost:4000/api/v1/namespaces +``` +``` # GET /v1/model_registry curl -i -H "kubeflow-userid: user@example.com" localhost:4000/api/v1/model_registry ``` @@ -250,4 +255,4 @@ The mock Kubernetes environment is activated when the environment variable `MOCK - `model-registry`: resides in the `kubeflow` namespace with the label `component: model-registry`. - `model-registry-dora`: resides in the `dora-namespace` namespace with the label `component: model-registry`. - `model-registry-bella`: resides in the `kubeflow` namespace with the label `component: model-registry`. - - `non-model-registry`: resides in the `kubeflow` namespace *without* the label `component: model-registry`. \ No newline at end of file + - `non-model-registry`: resides in the `kubeflow` namespace *without* the label `component: model-registry`. diff --git a/clients/ui/bff/internal/api/app.go b/clients/ui/bff/internal/api/app.go index c7047f29..6f2d985f 100644 --- a/clients/ui/bff/internal/api/app.go +++ b/clients/ui/bff/internal/api/app.go @@ -23,6 +23,7 @@ const ( HealthCheckPath = PathPrefix + "/healthcheck" UserPath = PathPrefix + "/user" ModelRegistryListPath = PathPrefix + "/model_registry" + NamespaceListPath = PathPrefix + "/namespaces" ModelRegistryPath = ModelRegistryListPath + "/:" + ModelRegistryId RegisteredModelListPath = ModelRegistryPath + "/registered_models" RegisteredModelPath = RegisteredModelListPath + "/:" + RegisteredModelId @@ -96,17 +97,25 @@ func (app *App) Routes() http.Handler { router.PATCH(RegisteredModelPath, app.AttachRESTClient(app.UpdateRegisteredModelHandler)) router.GET(RegisteredModelVersionsPath, app.AttachRESTClient(app.GetAllModelVersionsForRegisteredModelHandler)) router.POST(RegisteredModelVersionsPath, app.AttachRESTClient(app.CreateModelVersionForRegisteredModelHandler)) - router.GET(ModelVersionPath, app.AttachRESTClient(app.GetModelVersionHandler)) router.POST(ModelVersionListPath, app.AttachRESTClient(app.CreateModelVersionHandler)) router.PATCH(ModelVersionPath, app.AttachRESTClient(app.UpdateModelVersionHandler)) router.GET(ModelVersionArtifactListPath, app.AttachRESTClient(app.GetAllModelArtifactsByModelVersionHandler)) router.POST(ModelVersionArtifactListPath, app.AttachRESTClient(app.CreateModelArtifactByModelVersionHandler)) + router.PATCH(ModelRegistryPath, app.AttachRESTClient(app.UpdateModelVersionHandler)) // Kubernetes client routes router.GET(UserPath, app.UserHandler) router.GET(ModelRegistryListPath, app.ModelRegistryHandler) - router.PATCH(ModelRegistryPath, app.AttachRESTClient(app.UpdateModelVersionHandler)) + if app.config.DevMode { + router.GET(NamespaceListPath, app.GetNamespacesHandler) + } + + accessControlExemptPaths := map[string]struct{}{ + HealthCheckPath: {}, + UserPath: {}, + NamespaceListPath: {}, + } - return app.RecoverPanic(app.enableCORS(app.RequireAccessControl(router))) + return app.RecoverPanic(app.enableCORS(app.RequireAccessControl(app.InjectUserHeaders(router), accessControlExemptPaths))) } diff --git a/clients/ui/bff/internal/api/healthcheck__handler_test.go b/clients/ui/bff/internal/api/healthcheck__handler_test.go index 20ac52df..0212a58c 100644 --- a/clients/ui/bff/internal/api/healthcheck__handler_test.go +++ b/clients/ui/bff/internal/api/healthcheck__handler_test.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "github.com/kubeflow/model-registry/ui/bff/internal/config" "github.com/kubeflow/model-registry/ui/bff/internal/mocks" @@ -25,11 +26,12 @@ func TestHealthCheckHandler(t *testing.T) { rr := httptest.NewRecorder() req, err := http.NewRequest(http.MethodGet, HealthCheckPath, nil) + ctx := context.WithValue(req.Context(), KubeflowUserIdKey, mocks.KubeflowUserIDHeaderValue) + req = req.WithContext(ctx) assert.NoError(t, err) - req.Header.Set(kubeflowUserId, mocks.KubeflowUserIDHeaderValue) - app.HealthcheckHandler(rr, req, nil) + rs := rr.Result() defer rs.Body.Close() diff --git a/clients/ui/bff/internal/api/healthcheck_handler.go b/clients/ui/bff/internal/api/healthcheck_handler.go index df6d4702..6ee2049a 100644 --- a/clients/ui/bff/internal/api/healthcheck_handler.go +++ b/clients/ui/bff/internal/api/healthcheck_handler.go @@ -1,15 +1,20 @@ package api import ( + "errors" "github.com/julienschmidt/httprouter" "net/http" ) func (app *App) HealthcheckHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - userID := r.Header.Get(kubeflowUserId) + userId, ok := r.Context().Value(KubeflowUserIdKey).(string) + if !ok || userId == "" { + app.serverErrorResponse(w, r, errors.New("failed to retrieve kubeflow-userid from context")) + return + } - healthCheck, err := app.repositories.HealthCheck.HealthCheck(Version, userID) + healthCheck, err := app.repositories.HealthCheck.HealthCheck(Version, userId) if err != nil { app.serverErrorResponse(w, r, err) return diff --git a/clients/ui/bff/internal/api/middleware.go b/clients/ui/bff/internal/api/middleware.go index 02275b5f..2b826736 100644 --- a/clients/ui/bff/internal/api/middleware.go +++ b/clients/ui/bff/internal/api/middleware.go @@ -2,6 +2,7 @@ package api import ( "context" + "errors" "fmt" "net/http" @@ -12,8 +13,17 @@ import ( type contextKey string -const httpClientKey contextKey = "httpClientKey" -const kubeflowUserId = "kubeflow-userid" +const ( + httpClientKey contextKey = "httpClientKey" + + //Kubeflow authorization operates using custom authentication headers: + // Note: The functionality for `kubeflow-groups` is not fully operational at Kubeflow platform at this time + // But it will be soon implemented on Model Registry BFF + KubeflowUserIdKey contextKey = "kubeflowUserId" // kubeflow-userid :contains the user's email address + KubeflowUserIDHeader = "kubeflow-userid" + KubeflowUserGroupsKey contextKey = "kubeflowUserGroups" // kubeflow-groups : Holds a comma-separated list of user groups + KubeflowUserGroupsIdHeader = "kubeflow-groups" +) func (app *App) RecoverPanic(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -28,6 +38,26 @@ func (app *App) RecoverPanic(next http.Handler) http.Handler { }) } +func (app *App) InjectUserHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + userId := r.Header.Get(KubeflowUserIDHeader) + userGroups := r.Header.Get(KubeflowUserGroupsIdHeader) + + //Note: The functionality for `kubeflow-groups` is not fully operational at Kubeflow platform at this time + if userId == "" { + app.badRequestResponse(w, r, errors.New("missing required header: kubeflow-userid")) + return + } + + ctx := r.Context() + ctx = context.WithValue(ctx, KubeflowUserIdKey, userId) + ctx = context.WithValue(ctx, KubeflowUserGroupsKey, userGroups) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + func (app *App) enableCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // TODO(ederign) restrict CORS to a much smaller set of trusted origins. @@ -74,22 +104,16 @@ func resolveModelRegistryURL(id string, client integrations.KubernetesClientInte return url, nil } -func (app *App) RequireAccessControl(next http.Handler) http.Handler { +func (app *App) RequireAccessControl(next http.Handler, exemptPaths map[string]struct{}) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Skip SAR for health check - if r.URL.Path == HealthCheckPath { - next.ServeHTTP(w, r) - return - } - - // Skip SAR for user info - if r.URL.Path == UserPath { + // Skip SAR for exempt paths + if _, exempt := exemptPaths[r.URL.Path]; exempt { next.ServeHTTP(w, r) return } - user := r.Header.Get(kubeflowUserId) + user := r.Header.Get(KubeflowUserIDHeader) if user == "" { app.forbiddenResponse(w, r, "missing kubeflow-userid header") return diff --git a/clients/ui/bff/internal/api/namespaces_handler.go b/clients/ui/bff/internal/api/namespaces_handler.go new file mode 100644 index 00000000..fe60b190 --- /dev/null +++ b/clients/ui/bff/internal/api/namespaces_handler.go @@ -0,0 +1,36 @@ +package api + +import ( + "errors" + "github.com/kubeflow/model-registry/ui/bff/internal/models" + "net/http" + + "github.com/julienschmidt/httprouter" +) + +type NamespacesEnvelope Envelope[[]models.NamespaceModel, None] + +func (app *App) GetNamespacesHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + + userId, ok := r.Context().Value(KubeflowUserIdKey).(string) + if !ok || userId == "" { + app.serverErrorResponse(w, r, errors.New("failed to retrieve kubeflow-userid from context")) + return + } + + namespaces, err := app.repositories.Namespace.GetNamespaces(app.kubernetesClient, userId) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + + namespacesEnvelope := NamespacesEnvelope{ + Data: namespaces, + } + + err = app.WriteJSON(w, http.StatusOK, namespacesEnvelope, nil) + + if err != nil { + app.serverErrorResponse(w, r, err) + } +} diff --git a/clients/ui/bff/internal/api/namespaces_handler_test.go b/clients/ui/bff/internal/api/namespaces_handler_test.go new file mode 100644 index 00000000..fafa8d28 --- /dev/null +++ b/clients/ui/bff/internal/api/namespaces_handler_test.go @@ -0,0 +1,113 @@ +package api + +import ( + "context" + "encoding/json" + "github.com/kubeflow/model-registry/ui/bff/internal/config" + "github.com/kubeflow/model-registry/ui/bff/internal/mocks" + "github.com/kubeflow/model-registry/ui/bff/internal/models" + "github.com/kubeflow/model-registry/ui/bff/internal/repositories" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "io" + "net/http" + "net/http/httptest" +) + +var _ = Describe("TestNamespacesHandler", func() { + Context("when running in dev mode", Ordered, func() { + var testApp App + + BeforeAll(func() { + By("setting up the test app in dev mode") + testApp = App{ + config: config.EnvConfig{DevMode: true}, + kubernetesClient: k8sClient, + repositories: repositories.NewRepositories(mockMRClient), + logger: logger, + } + }) + + It("should return only dora-namespace for doraNonAdmin@example.com", func() { + By("creating the HTTP request with the kubeflow-userid header") + req, err := http.NewRequest(http.MethodGet, NamespaceListPath, nil) + ctx := context.WithValue(req.Context(), KubeflowUserIdKey, mocks.DoraNonAdminUser) + req = req.WithContext(ctx) + Expect(err).NotTo(HaveOccurred()) + rr := httptest.NewRecorder() + + By("calling the GetNamespacesHandler") + testApp.GetNamespacesHandler(rr, req, nil) + rs := rr.Result() + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + Expect(err).NotTo(HaveOccurred()) + + By("unmarshalling the response") + var actual NamespacesEnvelope + err = json.Unmarshal(body, &actual) + Expect(err).NotTo(HaveOccurred()) + Expect(rr.Code).To(Equal(http.StatusOK)) + + By("validating the response contains only dora-namespace") + expected := []models.NamespaceModel{{Name: "dora-namespace"}} + Expect(actual.Data).To(ConsistOf(expected)) + }) + + It("should return all namespaces for user@example.com", func() { + By("creating the HTTP request with the kubeflow-userid header") + req, err := http.NewRequest(http.MethodGet, NamespaceListPath, nil) + ctx := context.WithValue(req.Context(), KubeflowUserIdKey, mocks.KubeflowUserIDHeaderValue) + req = req.WithContext(ctx) + Expect(err).NotTo(HaveOccurred()) + req.Header.Set("kubeflow-userid", "user@example.com") + rr := httptest.NewRecorder() + + By("calling the GetNamespacesHandler") + testApp.GetNamespacesHandler(rr, req, nil) + rs := rr.Result() + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + Expect(err).NotTo(HaveOccurred()) + + By("unmarshalling the response") + var actual NamespacesEnvelope + err = json.Unmarshal(body, &actual) + Expect(err).NotTo(HaveOccurred()) + Expect(rr.Code).To(Equal(http.StatusOK)) + + By("validating the response contains all namespaces") + expected := []models.NamespaceModel{ + {Name: "kubeflow"}, + {Name: "dora-namespace"}, + } + Expect(actual.Data).To(ContainElements(expected)) + }) + + It("should return all namespaces for non-existent user", func() { + By("creating the HTTP request with a non-existent kubeflow-userid") + req, err := http.NewRequest(http.MethodGet, NamespaceListPath, nil) + ctx := context.WithValue(req.Context(), KubeflowUserIdKey, "nonexistent@example.com") + req = req.WithContext(ctx) + Expect(err).NotTo(HaveOccurred()) + rr := httptest.NewRecorder() + + By("calling the GetNamespacesHandler") + testApp.GetNamespacesHandler(rr, req, nil) + rs := rr.Result() + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + Expect(err).NotTo(HaveOccurred()) + + By("unmarshalling the response") + var actual NamespacesEnvelope + err = json.Unmarshal(body, &actual) + Expect(err).NotTo(HaveOccurred()) + Expect(rr.Code).To(Equal(http.StatusOK)) + + By("validating the response contains no namespaces") + Expect(actual.Data).To(BeEmpty()) + }) + }) + +}) diff --git a/clients/ui/bff/internal/api/test_utils.go b/clients/ui/bff/internal/api/test_utils.go index 3a2ec65a..77f56373 100644 --- a/clients/ui/bff/internal/api/test_utils.go +++ b/clients/ui/bff/internal/api/test_utils.go @@ -12,7 +12,7 @@ import ( "net/http/httptest" ) -func setupApiTest[T any](method string, url string, body interface{}, k8sClient k8s.KubernetesClientInterface, kubeflowUserIDHeader string) (T, *http.Response, error) { +func setupApiTest[T any](method string, url string, body interface{}, k8sClient k8s.KubernetesClientInterface, kubeflowUserIDHeaderValue string) (T, *http.Response, error) { mockMRClient, err := mocks.NewModelRegistryClient(nil) if err != nil { return *new(T), nil, err @@ -44,7 +44,7 @@ func setupApiTest[T any](method string, url string, body interface{}, k8sClient } // Set the kubeflow-userid header - req.Header.Set(kubeflowUserId, kubeflowUserIDHeader) + req.Header.Set(KubeflowUserIDHeader, kubeflowUserIDHeaderValue) ctx := context.WithValue(req.Context(), httpClientKey, mockClient) req = req.WithContext(ctx) diff --git a/clients/ui/bff/internal/api/user_handler.go b/clients/ui/bff/internal/api/user_handler.go index a5bcd264..9ec135cc 100644 --- a/clients/ui/bff/internal/api/user_handler.go +++ b/clients/ui/bff/internal/api/user_handler.go @@ -11,13 +11,13 @@ type UserEnvelope Envelope[*models.User, None] func (app *App) UserHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - userHeader := r.Header.Get(kubeflowUserId) - if userHeader == "" { - app.serverErrorResponse(w, r, errors.New("kubeflow-userid not present on header")) + userId, ok := r.Context().Value(KubeflowUserIdKey).(string) + if !ok || userId == "" { + app.serverErrorResponse(w, r, errors.New("failed to retrieve kubeflow-userid from context")) return } - user, err := app.repositories.User.GetUser(app.kubernetesClient, userHeader) + user, err := app.repositories.User.GetUser(app.kubernetesClient, userId) if err != nil { app.serverErrorResponse(w, r, err) return diff --git a/clients/ui/bff/internal/api/user_handler_test.go b/clients/ui/bff/internal/api/user_handler_test.go index 0accd253..13cbf95a 100644 --- a/clients/ui/bff/internal/api/user_handler_test.go +++ b/clients/ui/bff/internal/api/user_handler_test.go @@ -1,7 +1,9 @@ package api import ( + "context" "encoding/json" + "github.com/kubeflow/model-registry/ui/bff/internal/mocks" "io" "net/http" "net/http/httptest" @@ -32,10 +34,10 @@ var _ = Describe("TestUserHandler", func() { It("should show that KubeflowUserIDHeaderValue (user@example.com) is a cluster-admin", func() { By("creating the http request") req, err := http.NewRequest(http.MethodGet, UserPath, nil) + ctx := context.WithValue(req.Context(), KubeflowUserIdKey, mocks.KubeflowUserIDHeaderValue) + req = req.WithContext(ctx) Expect(err).NotTo(HaveOccurred()) - req.Header.Set(kubeflowUserId, KubeflowUserIDHeaderValue) - By("creating the http test infrastructure") rr := httptest.NewRecorder() @@ -60,10 +62,10 @@ var _ = Describe("TestUserHandler", func() { It("should show that DoraNonAdminUser (doraNonAdmin@example.com) is not a cluster-admin", func() { By("creating the http request") req, err := http.NewRequest(http.MethodGet, UserPath, nil) + ctx := context.WithValue(req.Context(), KubeflowUserIdKey, DoraNonAdminUser) + req = req.WithContext(ctx) Expect(err).NotTo(HaveOccurred()) - req.Header.Set(kubeflowUserId, DoraNonAdminUser) - By("creating the http test infrastructure") rr := httptest.NewRecorder() @@ -90,10 +92,10 @@ var _ = Describe("TestUserHandler", func() { By("creating the http request") req, err := http.NewRequest(http.MethodGet, UserPath, nil) + ctx := context.WithValue(req.Context(), KubeflowUserIdKey, randomUser) + req = req.WithContext(ctx) Expect(err).NotTo(HaveOccurred()) - req.Header.Set(kubeflowUserId, randomUser) - By("creating the http test infrastructure") rr := httptest.NewRecorder() diff --git a/clients/ui/bff/internal/integrations/k8s.go b/clients/ui/bff/internal/integrations/k8s.go index 8b6c7170..6c3d8c37 100644 --- a/clients/ui/bff/internal/integrations/k8s.go +++ b/clients/ui/bff/internal/integrations/k8s.go @@ -30,6 +30,7 @@ type KubernetesClientInterface interface { IsInCluster() bool PerformSAR(user string) (bool, error) IsClusterAdmin(user string) (bool, error) + GetNamespaces(user string) ([]corev1.Namespace, error) } type ServiceDetails struct { @@ -306,3 +307,43 @@ func (kc *KubernetesClient) IsClusterAdmin(user string) (bool, error) { return false, nil } + +func (kc *KubernetesClient) GetNamespaces(user string) ([]corev1.Namespace, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + //list all namespaces + namespaceList := &corev1.NamespaceList{} + err := kc.ControllerRuntimeClient.List(ctx, namespaceList) + if err != nil { + return nil, fmt.Errorf("failed to list namespaces: %w", err) + } + + //check user access with SAR for each namespace + var namespaces []corev1.Namespace + for _, ns := range namespaceList.Items { + sar := &authv1.SubjectAccessReview{ + Spec: authv1.SubjectAccessReviewSpec{ + User: user, + ResourceAttributes: &authv1.ResourceAttributes{ + Namespace: ns.Name, + Verb: "get", + Resource: "namespaces", + }, + }, + } + + response, err := kc.KubernetesNativeClient.AuthorizationV1().SubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{}) + if err != nil { + kc.Logger.Error("failed to perform SubjectAccessReview", "namespace", ns.Name, "error", err) + continue + } + + if response.Status.Allowed { + namespaces = append(namespaces, ns) + } + } + + return namespaces, nil + +} diff --git a/clients/ui/bff/internal/mocks/k8s_mock.go b/clients/ui/bff/internal/mocks/k8s_mock.go index 9fcc8a56..d07e06ad 100644 --- a/clients/ui/bff/internal/mocks/k8s_mock.go +++ b/clients/ui/bff/internal/mocks/k8s_mock.go @@ -303,7 +303,7 @@ func createNamespaceRestrictedRBAC(k8sClient client.Client, ctx context.Context, Rules: []rbacv1.PolicyRule{ { APIGroups: []string{""}, - Resources: []string{"services"}, + Resources: []string{"services", "namespaces"}, Verbs: []string{"get", "list"}, }, }, diff --git a/clients/ui/bff/internal/models/namespace.go b/clients/ui/bff/internal/models/namespace.go new file mode 100644 index 00000000..2f37b184 --- /dev/null +++ b/clients/ui/bff/internal/models/namespace.go @@ -0,0 +1,11 @@ +package models + +type NamespaceModel struct { + Name string `json:"name"` +} + +func NewNamespaceModelFromNamespace(name string) NamespaceModel { + return NamespaceModel{ + Name: name, + } +} diff --git a/clients/ui/bff/internal/repositories/namespace.go b/clients/ui/bff/internal/repositories/namespace.go new file mode 100644 index 00000000..ae547a88 --- /dev/null +++ b/clients/ui/bff/internal/repositories/namespace.go @@ -0,0 +1,28 @@ +package repositories + +import ( + "fmt" + k8s "github.com/kubeflow/model-registry/ui/bff/internal/integrations" + "github.com/kubeflow/model-registry/ui/bff/internal/models" +) + +type NamespaceRepository struct{} + +func NewNamespaceRepository() *NamespaceRepository { + return &NamespaceRepository{} +} + +func (r *NamespaceRepository) GetNamespaces(client k8s.KubernetesClientInterface, user string) ([]models.NamespaceModel, error) { + + namespaces, err := client.GetNamespaces(user) + if err != nil { + return nil, fmt.Errorf("error fetching namespaces: %w", err) + } + + var namespaceModels = []models.NamespaceModel{} + for _, ns := range namespaces { + namespaceModels = append(namespaceModels, models.NewNamespaceModelFromNamespace(ns.Name)) + } + + return namespaceModels, nil +} diff --git a/clients/ui/bff/internal/repositories/repositories.go b/clients/ui/bff/internal/repositories/repositories.go index 5efa9b84..434c2d6b 100644 --- a/clients/ui/bff/internal/repositories/repositories.go +++ b/clients/ui/bff/internal/repositories/repositories.go @@ -6,6 +6,7 @@ type Repositories struct { ModelRegistry *ModelRegistryRepository ModelRegistryClient ModelRegistryClientInterface User *UserRepository + Namespace *NamespaceRepository } func NewRepositories(modelRegistryClient ModelRegistryClientInterface) *Repositories { @@ -14,5 +15,6 @@ func NewRepositories(modelRegistryClient ModelRegistryClientInterface) *Reposito ModelRegistry: NewModelRegistryRepository(), ModelRegistryClient: modelRegistryClient, User: NewUserRepository(), + Namespace: NewNamespaceRepository(), } } From 9baad82a39ada6d674bb5a4555cdb14c857814c3 Mon Sep 17 00:00:00 2001 From: Eder Ignatowicz Date: Thu, 19 Dec 2024 09:42:52 -0500 Subject: [PATCH 2/2] Update clients/ui/bff/internal/api/namespaces_handler_test.go Co-authored-by: Alex Creasy Signed-off-by: Eder Ignatowicz --- clients/ui/bff/internal/api/namespaces_handler_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/ui/bff/internal/api/namespaces_handler_test.go b/clients/ui/bff/internal/api/namespaces_handler_test.go index fafa8d28..b4869058 100644 --- a/clients/ui/bff/internal/api/namespaces_handler_test.go +++ b/clients/ui/bff/internal/api/namespaces_handler_test.go @@ -84,7 +84,7 @@ var _ = Describe("TestNamespacesHandler", func() { Expect(actual.Data).To(ContainElements(expected)) }) - It("should return all namespaces for non-existent user", func() { + It("should return no namespaces for non-existent user", func() { By("creating the HTTP request with a non-existent kubeflow-userid") req, err := http.NewRequest(http.MethodGet, NamespaceListPath, nil) ctx := context.WithValue(req.Context(), KubeflowUserIdKey, "nonexistent@example.com")