From d53528cb1241c376e7aa446a7ee0aff9145ec2f1 Mon Sep 17 00:00:00 2001 From: Eder Ignatowicz Date: Thu, 17 Oct 2024 11:11:19 -0400 Subject: [PATCH] feat(bff): Use ctrl.NewManager from the controller-runtime as Kubernetes client (#482) * feat(bff): Use ctrl.NewManager from the controller-runtime as Kubernetes client Signed-off-by: Eder Ignatowicz * Update clients/ui/bff/cmd/main.go Co-authored-by: Alex Creasy Signed-off-by: Eder Ignatowicz --------- Signed-off-by: Eder Ignatowicz Co-authored-by: Alex Creasy --- clients/ui/bff/cmd/main.go | 39 ++- clients/ui/bff/go.mod | 14 ++ clients/ui/bff/go.sum | 44 +++- clients/ui/bff/internal/api/app.go | 29 ++- .../internal/api/healthcheck__handler_test.go | 16 +- .../bff/internal/api/healthcheck_handler.go | 2 +- .../internal/api/model_registry_handler.go | 6 +- .../api/model_registry_handler_test.go | 7 +- .../internal/api/model_versions_handler.go | 10 +- .../internal/api/registered_models_handler.go | 12 +- clients/ui/bff/internal/api/test_utils.go | 5 +- .../bff/internal/data/health_check_model.go | 22 -- .../ui/bff/internal/data/model_registry.go | 32 --- clients/ui/bff/internal/data/models.go | 7 - clients/ui/bff/internal/helpers/k8s.go | 28 +++ clients/ui/bff/internal/integrations/k8s.go | 228 ++++++++++++------ .../ui/bff/internal/integrations/k8s_test.go | 68 ------ clients/ui/bff/internal/mocks/k8s_mock.go | 6 + .../ui/bff/internal/models/health_check.go | 10 + .../ui/bff/internal/models/model_registry.go | 7 + .../bff/internal/repositories/health_check.go | 21 ++ .../{data => repositories}/helpers.go | 2 +- .../internal/repositories/model_registry.go | 34 +++ .../model_registry_client.go | 2 +- .../model_registry_test.go | 9 +- .../{data => repositories}/model_version.go | 2 +- .../model_version_test.go | 2 +- .../registered_model.go | 2 +- .../registered_model_test.go | 2 +- .../bff/internal/repositories/repositories.go | 16 ++ 30 files changed, 430 insertions(+), 254 deletions(-) delete mode 100644 clients/ui/bff/internal/data/health_check_model.go delete mode 100644 clients/ui/bff/internal/data/model_registry.go delete mode 100644 clients/ui/bff/internal/data/models.go create mode 100644 clients/ui/bff/internal/helpers/k8s.go delete mode 100644 clients/ui/bff/internal/integrations/k8s_test.go create mode 100644 clients/ui/bff/internal/models/health_check.go create mode 100644 clients/ui/bff/internal/models/model_registry.go create mode 100644 clients/ui/bff/internal/repositories/health_check.go rename clients/ui/bff/internal/{data => repositories}/helpers.go (97%) create mode 100644 clients/ui/bff/internal/repositories/model_registry.go rename clients/ui/bff/internal/{data => repositories}/model_registry_client.go (94%) rename clients/ui/bff/internal/{data => repositories}/model_registry_test.go (72%) rename clients/ui/bff/internal/{data => repositories}/model_version.go (99%) rename clients/ui/bff/internal/{data => repositories}/model_version_test.go (99%) rename clients/ui/bff/internal/{data => repositories}/registered_model.go (99%) rename clients/ui/bff/internal/{data => repositories}/registered_model_test.go (99%) create mode 100644 clients/ui/bff/internal/repositories/repositories.go diff --git a/clients/ui/bff/cmd/main.go b/clients/ui/bff/cmd/main.go index 0652194a1..25c367c3f 100644 --- a/clients/ui/bff/cmd/main.go +++ b/clients/ui/bff/cmd/main.go @@ -1,10 +1,13 @@ package main import ( + "context" "flag" "fmt" "github.com/kubeflow/model-registry/ui/bff/internal/api" "github.com/kubeflow/model-registry/ui/bff/internal/config" + "os/signal" + "syscall" "log/slog" "net/http" @@ -37,13 +40,39 @@ func main() { ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), } - logger.Info("starting server", "addr", srv.Addr) + // Start the server in a goroutine + go func() { + logger.Info("starting server", "addr", srv.Addr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logger.Error("HTTP server ListenAndServe", "error", err) + } + }() - err = srv.ListenAndServe() - if err != nil { - logger.Error(err.Error()) + // Graceful shutdown setup + shutdownCh := make(chan os.Signal, 1) + signal.Notify(shutdownCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + + // Wait for shutdown signal + <-shutdownCh + logger.Info("shutting down gracefully...") + + // Create a context with timeout for the shutdown process + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Shutdown the HTTP server gracefully + if err := srv.Shutdown(ctx); err != nil { + logger.Error("server shutdown failed", "error", err) + } + + // Shutdown the Kubernetes manager gracefully + if err := app.Shutdown(ctx, logger); err != nil { + logger.Error("failed to shutdown Kubernetes manager", "error", err) } - os.Exit(1) + + logger.Info("server stopped") + os.Exit(0) + } func getEnvAsInt(name string, defaultVal int) int { diff --git a/clients/ui/bff/go.mod b/clients/ui/bff/go.mod index 3daf74d3d..89b1cfb5a 100644 --- a/clients/ui/bff/go.mod +++ b/clients/ui/bff/go.mod @@ -10,17 +10,23 @@ require ( k8s.io/api v0.31.1 k8s.io/apimachinery v0.31.1 k8s.io/client-go v0.31.1 + sigs.k8s.io/controller-runtime v0.19.0 ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -33,20 +39,28 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.31.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect diff --git a/clients/ui/bff/go.sum b/clients/ui/bff/go.sum index adca7794c..575e3145f 100644 --- a/clients/ui/bff/go.sum +++ b/clients/ui/bff/go.sum @@ -1,5 +1,9 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/brianvoe/gofakeit/v7 v7.0.4 h1:Mkxwz9jYg8Ad8NvT9HA27pCMZGFQo08MK6jD0QTKEww= github.com/brianvoe/gofakeit/v7 v7.0.4/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -7,10 +11,18 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= @@ -22,6 +34,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -66,11 +80,21 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -90,9 +114,17 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -129,11 +161,15 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -144,6 +180,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= +k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= +k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= @@ -154,6 +192,8 @@ k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7F k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= +sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= diff --git a/clients/ui/bff/internal/api/app.go b/clients/ui/bff/internal/api/app.go index e8b3fda4e..6cf2ce6a1 100644 --- a/clients/ui/bff/internal/api/app.go +++ b/clients/ui/bff/internal/api/app.go @@ -1,10 +1,11 @@ package api import ( + "context" "fmt" "github.com/kubeflow/model-registry/ui/bff/internal/config" - "github.com/kubeflow/model-registry/ui/bff/internal/data" "github.com/kubeflow/model-registry/ui/bff/internal/integrations" + "github.com/kubeflow/model-registry/ui/bff/internal/repositories" "log/slog" "net/http" @@ -33,11 +34,10 @@ const ( ) type App struct { - config config.EnvConfig - logger *slog.Logger - models data.Models - kubernetesClient integrations.KubernetesClientInterface - modelRegistryClient data.ModelRegistryClientInterface + config config.EnvConfig + logger *slog.Logger + kubernetesClient integrations.KubernetesClientInterface + repositories *repositories.Repositories } func NewApp(cfg config.EnvConfig, logger *slog.Logger) (*App, error) { @@ -54,12 +54,13 @@ func NewApp(cfg config.EnvConfig, logger *slog.Logger) (*App, error) { return nil, fmt.Errorf("failed to create Kubernetes client: %w", err) } - var mrClient data.ModelRegistryClientInterface + var mrClient repositories.ModelRegistryClientInterface if cfg.MockMRClient { + //mock all model registry calls mrClient, err = mocks.NewModelRegistryClient(logger) } else { - mrClient, err = data.NewModelRegistryClient(logger) + mrClient, err = repositories.NewModelRegistryClient(logger) } if err != nil { @@ -67,14 +68,18 @@ func NewApp(cfg config.EnvConfig, logger *slog.Logger) (*App, error) { } app := &App{ - config: cfg, - logger: logger, - kubernetesClient: k8sClient, - modelRegistryClient: mrClient, + config: cfg, + logger: logger, + kubernetesClient: k8sClient, + repositories: repositories.NewRepositories(mrClient), } return app, nil } +func (app *App) Shutdown(ctx context.Context, logger *slog.Logger) error { + return app.kubernetesClient.Shutdown(ctx, logger) +} + func (app *App) Routes() http.Handler { router := httprouter.New() diff --git a/clients/ui/bff/internal/api/healthcheck__handler_test.go b/clients/ui/bff/internal/api/healthcheck__handler_test.go index e6350a85c..e6b93c93e 100644 --- a/clients/ui/bff/internal/api/healthcheck__handler_test.go +++ b/clients/ui/bff/internal/api/healthcheck__handler_test.go @@ -3,7 +3,9 @@ package api import ( "encoding/json" "github.com/kubeflow/model-registry/ui/bff/internal/config" - "github.com/kubeflow/model-registry/ui/bff/internal/data" + "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/stretchr/testify/assert" "io" "net/http" @@ -13,9 +15,13 @@ import ( func TestHealthCheckHandler(t *testing.T) { + mockMRClient, _ := mocks.NewModelRegistryClient(nil) + app := App{config: config.EnvConfig{ Port: 4000, - }} + }, + repositories: repositories.NewRepositories(mockMRClient), + } rr := httptest.NewRecorder() req, err := http.NewRequest(http.MethodGet, HealthCheckPath, nil) @@ -29,15 +35,15 @@ func TestHealthCheckHandler(t *testing.T) { body, err := io.ReadAll(rs.Body) assert.NoError(t, err) - var healthCheckRes data.HealthCheckModel + var healthCheckRes models.HealthCheckModel err = json.Unmarshal(body, &healthCheckRes) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) - expected := data.HealthCheckModel{ + expected := models.HealthCheckModel{ Status: "available", - SystemInfo: data.SystemInfo{ + SystemInfo: models.SystemInfo{ Version: Version, }, } diff --git a/clients/ui/bff/internal/api/healthcheck_handler.go b/clients/ui/bff/internal/api/healthcheck_handler.go index 2f8223a7f..57c6b9813 100644 --- a/clients/ui/bff/internal/api/healthcheck_handler.go +++ b/clients/ui/bff/internal/api/healthcheck_handler.go @@ -7,7 +7,7 @@ import ( func (app *App) HealthcheckHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - healthCheck, err := app.models.HealthCheck.HealthCheck(Version) + healthCheck, err := app.repositories.HealthCheck.HealthCheck(Version) if err != nil { app.serverErrorResponse(w, r, err) return diff --git a/clients/ui/bff/internal/api/model_registry_handler.go b/clients/ui/bff/internal/api/model_registry_handler.go index cfaa8e47d..8412d8f8b 100644 --- a/clients/ui/bff/internal/api/model_registry_handler.go +++ b/clients/ui/bff/internal/api/model_registry_handler.go @@ -2,15 +2,15 @@ package api import ( "github.com/julienschmidt/httprouter" - "github.com/kubeflow/model-registry/ui/bff/internal/data" + "github.com/kubeflow/model-registry/ui/bff/internal/models" "net/http" ) -type ModelRegistryListEnvelope Envelope[[]data.ModelRegistryModel, None] +type ModelRegistryListEnvelope Envelope[[]models.ModelRegistryModel, None] func (app *App) ModelRegistryHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - registries, err := app.models.ModelRegistry.FetchAllModelRegistries(app.kubernetesClient) + registries, err := app.repositories.ModelRegistry.FetchAllModelRegistries(app.kubernetesClient) if err != nil { app.serverErrorResponse(w, r, err) return diff --git a/clients/ui/bff/internal/api/model_registry_handler_test.go b/clients/ui/bff/internal/api/model_registry_handler_test.go index df3d1ae3d..e345fe9aa 100644 --- a/clients/ui/bff/internal/api/model_registry_handler_test.go +++ b/clients/ui/bff/internal/api/model_registry_handler_test.go @@ -2,8 +2,9 @@ package api import ( "encoding/json" - "github.com/kubeflow/model-registry/ui/bff/internal/data" "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/stretchr/testify/assert" "io" "net/http" @@ -13,9 +14,11 @@ import ( func TestModelRegistryHandler(t *testing.T) { mockK8sClient, _ := mocks.NewKubernetesClient(nil) + mockMRClient, _ := mocks.NewModelRegistryClient(nil) testApp := App{ kubernetesClient: mockK8sClient, + repositories: repositories.NewRepositories(mockMRClient), } req, err := http.NewRequest(http.MethodGet, ModelRegistryListPath, nil) @@ -36,7 +39,7 @@ func TestModelRegistryHandler(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) var expected = ModelRegistryListEnvelope{ - Data: []data.ModelRegistryModel{ + Data: []models.ModelRegistryModel{ {Name: "model-registry", Description: "Model registry description", DisplayName: "Model Registry"}, {Name: "model-registry-dora", Description: "Model registry dora description", DisplayName: "Model Registry Dora"}, {Name: "model-registry-bella", Description: "Model registry bella description", DisplayName: "Model Registry Bella"}, diff --git a/clients/ui/bff/internal/api/model_versions_handler.go b/clients/ui/bff/internal/api/model_versions_handler.go index 5372cef67..425ca3691 100644 --- a/clients/ui/bff/internal/api/model_versions_handler.go +++ b/clients/ui/bff/internal/api/model_versions_handler.go @@ -25,7 +25,7 @@ func (app *App) GetModelVersionHandler(w http.ResponseWriter, r *http.Request, p return } - model, err := app.modelRegistryClient.GetModelVersion(client, ps.ByName(ModelVersionId)) + model, err := app.repositories.ModelRegistryClient.GetModelVersion(client, ps.ByName(ModelVersionId)) if err != nil { app.serverErrorResponse(w, r, err) return @@ -72,7 +72,7 @@ func (app *App) CreateModelVersionHandler(w http.ResponseWriter, r *http.Request return } - createdVersion, err := app.modelRegistryClient.CreateModelVersion(client, jsonData) + createdVersion, err := app.repositories.ModelRegistryClient.CreateModelVersion(client, jsonData) if err != nil { var httpErr *integrations.HTTPError if errors.As(err, &httpErr) { @@ -123,7 +123,7 @@ func (app *App) UpdateModelVersionHandler(w http.ResponseWriter, r *http.Request return } - patchedModel, err := app.modelRegistryClient.UpdateModelVersion(client, ps.ByName(ModelVersionId), jsonData) + patchedModel, err := app.repositories.ModelRegistryClient.UpdateModelVersion(client, ps.ByName(ModelVersionId), jsonData) if err != nil { var httpErr *integrations.HTTPError if errors.As(err, &httpErr) { @@ -157,7 +157,7 @@ func (app *App) GetAllModelArtifactsByModelVersionHandler(w http.ResponseWriter, return } - data, err := app.modelRegistryClient.GetModelArtifactsByModelVersion(client, ps.ByName(ModelVersionId), r.URL.Query()) + data, err := app.repositories.ModelRegistryClient.GetModelArtifactsByModelVersion(client, ps.ByName(ModelVersionId), r.URL.Query()) if err != nil { app.serverErrorResponse(w, r, err) return @@ -199,7 +199,7 @@ func (app *App) CreateModelArtifactByModelVersionHandler(w http.ResponseWriter, return } - createdArtifact, err := app.modelRegistryClient.CreateModelArtifactByModelVersion(client, ps.ByName(ModelVersionId), jsonData) + createdArtifact, err := app.repositories.ModelRegistryClient.CreateModelArtifactByModelVersion(client, ps.ByName(ModelVersionId), jsonData) if err != nil { var httpErr *integrations.HTTPError if errors.As(err, &httpErr) { diff --git a/clients/ui/bff/internal/api/registered_models_handler.go b/clients/ui/bff/internal/api/registered_models_handler.go index e75d121f5..f1dedde55 100644 --- a/clients/ui/bff/internal/api/registered_models_handler.go +++ b/clients/ui/bff/internal/api/registered_models_handler.go @@ -22,7 +22,7 @@ func (app *App) GetAllRegisteredModelsHandler(w http.ResponseWriter, r *http.Req return } - modelList, err := app.modelRegistryClient.GetAllRegisteredModels(client, r.URL.Query()) + modelList, err := app.repositories.ModelRegistryClient.GetAllRegisteredModels(client, r.URL.Query()) if err != nil { app.serverErrorResponse(w, r, err) return @@ -64,7 +64,7 @@ func (app *App) CreateRegisteredModelHandler(w http.ResponseWriter, r *http.Requ return } - createdModel, err := app.modelRegistryClient.CreateRegisteredModel(client, jsonData) + createdModel, err := app.repositories.ModelRegistryClient.CreateRegisteredModel(client, jsonData) if err != nil { var httpErr *integrations.HTTPError if errors.As(err, &httpErr) { @@ -99,7 +99,7 @@ func (app *App) GetRegisteredModelHandler(w http.ResponseWriter, r *http.Request return } - model, err := app.modelRegistryClient.GetRegisteredModel(client, ps.ByName(RegisteredModelId)) + model, err := app.repositories.ModelRegistryClient.GetRegisteredModel(client, ps.ByName(RegisteredModelId)) if err != nil { app.serverErrorResponse(w, r, err) return @@ -143,7 +143,7 @@ func (app *App) UpdateRegisteredModelHandler(w http.ResponseWriter, r *http.Requ return } - patchedModel, err := app.modelRegistryClient.UpdateRegisteredModel(client, ps.ByName(RegisteredModelId), jsonData) + patchedModel, err := app.repositories.ModelRegistryClient.UpdateRegisteredModel(client, ps.ByName(RegisteredModelId), jsonData) if err != nil { var httpErr *integrations.HTTPError if errors.As(err, &httpErr) { @@ -177,7 +177,7 @@ func (app *App) GetAllModelVersionsForRegisteredModelHandler(w http.ResponseWrit return } - versionList, err := app.modelRegistryClient.GetAllModelVersions(client, ps.ByName(RegisteredModelId), r.URL.Query()) + versionList, err := app.repositories.ModelRegistryClient.GetAllModelVersions(client, ps.ByName(RegisteredModelId), r.URL.Query()) if err != nil { app.serverErrorResponse(w, r, err) @@ -218,7 +218,7 @@ func (app *App) CreateModelVersionForRegisteredModelHandler(w http.ResponseWrite app.serverErrorResponse(w, r, fmt.Errorf("error marshaling model to JSON: %w", err)) } - createdVersion, err := app.modelRegistryClient.CreateModelVersionForRegisteredModel(client, ps.ByName(RegisteredModelId), jsonData) + createdVersion, err := app.repositories.ModelRegistryClient.CreateModelVersionForRegisteredModel(client, ps.ByName(RegisteredModelId), jsonData) if err != nil { var httpErr *integrations.HTTPError if errors.As(err, &httpErr) { diff --git a/clients/ui/bff/internal/api/test_utils.go b/clients/ui/bff/internal/api/test_utils.go index d0bbe526e..ca144eaac 100644 --- a/clients/ui/bff/internal/api/test_utils.go +++ b/clients/ui/bff/internal/api/test_utils.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "github.com/kubeflow/model-registry/ui/bff/internal/mocks" + "github.com/kubeflow/model-registry/ui/bff/internal/repositories" "io" "net/http" "net/http/httptest" @@ -23,8 +24,8 @@ func setupApiTest[T any](method string, url string, body interface{}) (T, *http. mockClient := new(mocks.MockHTTPClient) testApp := App{ - modelRegistryClient: mockMRClient, - kubernetesClient: mockK8sClient, + repositories: repositories.NewRepositories(mockMRClient), + kubernetesClient: mockK8sClient, } var req *http.Request diff --git a/clients/ui/bff/internal/data/health_check_model.go b/clients/ui/bff/internal/data/health_check_model.go deleted file mode 100644 index b8b557e61..000000000 --- a/clients/ui/bff/internal/data/health_check_model.go +++ /dev/null @@ -1,22 +0,0 @@ -package data - -type SystemInfo struct { - Version string `json:"version"` -} - -type HealthCheckModel struct { - Status string `json:"status"` - SystemInfo SystemInfo `json:"system_info"` -} - -func (m HealthCheckModel) HealthCheck(version string) (HealthCheckModel, error) { - - var res = HealthCheckModel{ - Status: "available", - SystemInfo: SystemInfo{ - Version: version, - }, - } - - return res, nil -} diff --git a/clients/ui/bff/internal/data/model_registry.go b/clients/ui/bff/internal/data/model_registry.go deleted file mode 100644 index e27c47902..000000000 --- a/clients/ui/bff/internal/data/model_registry.go +++ /dev/null @@ -1,32 +0,0 @@ -package data - -import ( - "fmt" - k8s "github.com/kubeflow/model-registry/ui/bff/internal/integrations" -) - -type ModelRegistryModel struct { - Name string `json:"name"` - DisplayName string `json:"displayName"` - Description string `json:"description"` -} - -func (m ModelRegistryModel) FetchAllModelRegistries(client k8s.KubernetesClientInterface) ([]ModelRegistryModel, error) { - - resources, err := client.GetServiceDetails() - if err != nil { - return nil, fmt.Errorf("error fetching model registries: %w", err) - } - - var registries []ModelRegistryModel = []ModelRegistryModel{} - for _, item := range resources { - registry := ModelRegistryModel{ - Name: item.Name, - Description: item.Description, - DisplayName: item.DisplayName, - } - registries = append(registries, registry) - } - - return registries, nil -} diff --git a/clients/ui/bff/internal/data/models.go b/clients/ui/bff/internal/data/models.go deleted file mode 100644 index 5cb3c51c5..000000000 --- a/clients/ui/bff/internal/data/models.go +++ /dev/null @@ -1,7 +0,0 @@ -package data - -// Models struct is a single convenient container to hold and represent all our data. -type Models struct { - HealthCheck HealthCheckModel - ModelRegistry ModelRegistryModel -} diff --git a/clients/ui/bff/internal/helpers/k8s.go b/clients/ui/bff/internal/helpers/k8s.go new file mode 100644 index 000000000..766ab6c5f --- /dev/null +++ b/clients/ui/bff/internal/helpers/k8s.go @@ -0,0 +1,28 @@ +package helper + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + clientRest "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +// GetKubeconfig returns the current KUBECONFIG configuration based on the default loading rules. +func GetKubeconfig() (*clientRest.Config, error) { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + configOverrides := &clientcmd.ConfigOverrides{} + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) + return kubeConfig.ClientConfig() +} + +// BuildScheme builds a new runtime scheme with all the necessary types registered. +func BuildScheme() (*runtime.Scheme, error) { + scheme := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("failed to add Kubernetes types to scheme: %w", err) + } + + return scheme, nil +} diff --git a/clients/ui/bff/internal/integrations/k8s.go b/clients/ui/bff/internal/integrations/k8s.go index 126f962f3..3399b7637 100644 --- a/clients/ui/bff/internal/integrations/k8s.go +++ b/clients/ui/bff/internal/integrations/k8s.go @@ -3,18 +3,24 @@ package integrations import ( "context" "fmt" - "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/clientcmd" + helper "github.com/kubeflow/model-registry/ui/bff/internal/helpers" + corev1 "k8s.io/api/core/v1" "log/slog" + "os" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "time" ) +const componentName = "model-registry-server" + type KubernetesClientInterface interface { GetServiceNames() ([]string, error) GetServiceDetailsByName(serviceName string) (ServiceDetails, error) GetServiceDetails() ([]ServiceDetails, error) BearerToken() (string, error) + Shutdown(ctx context.Context, logger *slog.Logger) error } type ServiceDetails struct { @@ -26,62 +32,157 @@ type ServiceDetails struct { } type KubernetesClient struct { - ClientSet *kubernetes.Clientset - Namespace string - Token string - //TODO (ederign) How and on which frequency should we update this cache? - //dont forget about mutexes - ServiceCache map[string]ServiceDetails -} - -func (kc *KubernetesClient) BearerToken() (string, error) { - - return kc.Token, nil + Client client.Client + Mgr ctrl.Manager + Token string + Logger *slog.Logger + stopFn context.CancelFunc // Store a function to cancel the context for graceful shutdown + mgrStopped chan struct{} } func NewKubernetesClient(logger *slog.Logger) (KubernetesClientInterface, error) { - kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}) - restConfig, err := kubeConfig.ClientConfig() + // Create a context with a cancel function is used for shutdown the kubernetes client + ctx, cancel := context.WithCancel(ctrl.SetupSignalHandler()) + kubeconfig, err := helper.GetKubeconfig() if err != nil { - return nil, fmt.Errorf("failed to create Kubernetes restConfig: %w", err) + logger.Error("failed to get kubeconfig", "error", err) + os.Exit(1) } - namespace, _, err := kubeConfig.Namespace() + scheme, err := helper.BuildScheme() if err != nil { - return nil, fmt.Errorf("failed to create Kubernetes namespace: %w", err) + logger.Error("failed to build Kubernetes scheme", "error", err) + os.Exit(1) } - clientSet, err := kubernetes.NewForConfig(restConfig) + // Create the manager with caching capabilities + mgr, err := ctrl.NewManager(kubeconfig, ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: "0", // disable metrics serving + }, + HealthProbeBindAddress: "0", // disable health probe serving + LeaderElection: false, + //Namespace: "namespace", //TODO (ederign) do we need to specify the namespace to operate in + //There is also cache filters and Sync periods to assess later. + }) + if err != nil { - return nil, fmt.Errorf("failed to create Kubernetes clientset: %w", err) + logger.Error("unable to create manager", "error", err) + cancel() + os.Exit(1) } - //fetching services - services, err := clientSet.CoreV1().Services(namespace).List(context.TODO(), metav1.ListOptions{}) - if err != nil { - return nil, fmt.Errorf("failed to list model-registry-server services: %w", err) + + // Channel to signal when the manager has stopped + mgrStopped := make(chan struct{}) + + // Start the manager in a goroutine + go func() { + defer close(mgrStopped) // Signal that the manager has stopped + if err := mgr.Start(ctx); err != nil { + logger.Error("problem running manager", "error", err) + os.Exit(1) + } + }() + + // Wait for the cache to sync before using the client + if !mgr.GetCache().WaitForCacheSync(ctx) { + cancel() + return nil, fmt.Errorf("failed to wait for cache to sync") } - //building serviceCache - serviceCache, err := buildModelRegistryServiceCache(logger, *services) + kc := &KubernetesClient{ + Client: mgr.GetClient(), + Mgr: mgr, + Token: kubeconfig.BearerToken, + Logger: logger, + stopFn: cancel, + mgrStopped: mgrStopped, // Store the stop channel + + //Namespace: namespace, //TODO (ederign) do we need to restrict service list by namespace? + } + return kc, nil +} + +func (kc *KubernetesClient) Shutdown(ctx context.Context, logger *slog.Logger) error { + logger.Info("shutting down Kubernetes manager...") + + // Use the saved cancel function to stop the manager + kc.stopFn() + + // Wait for the manager to stop or for the context to be canceled + select { + case <-kc.mgrStopped: + logger.Info("Kubernetes manager stopped successfully") + return nil + case <-ctx.Done(): + logger.Error("context canceled while waiting for Kubernetes manager to stop") + return ctx.Err() + case <-time.After(30 * time.Second): + logger.Error("timeout while waiting for Kubernetes manager to stop") + return fmt.Errorf("timeout while waiting for Kubernetes manager to stop") + } +} + +func (kc *KubernetesClient) BearerToken() (string, error) { + return kc.Token, nil +} + +func (kc *KubernetesClient) GetServiceNames() ([]string, error) { + //TODO (ederign) when we develop the front-end, implement subject access review here + // and check if the username has actually permissions to access that server + // currently on kf dashboard, the user name comes in kubeflow-userid + + //TODO (ederign) we should consider and rethinking listing all services on cluster + // what if we have thousand of those? + // we should consider label filtering for instance + + serviceList := &corev1.ServiceList{} + //TODO (ederign) review the context timeout + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() + + err := kc.Client.List(ctx, serviceList, &client.ListOptions{}) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to list services: %w", err) } - kc := &KubernetesClient{ - ClientSet: clientSet, - Namespace: namespace, - Token: restConfig.BearerToken, - ServiceCache: serviceCache, + var serviceNames []string + for _, service := range serviceList.Items { + if value, ok := service.Spec.Selector["component"]; ok && value == componentName { + serviceNames = append(serviceNames, service.Name) + } } - return kc, nil + if len(serviceNames) == 0 { + return nil, fmt.Errorf("no services found with component: %s", componentName) + } + + return serviceNames, nil } -func buildModelRegistryServiceCache(logger *slog.Logger, services v1.ServiceList) (map[string]ServiceDetails, error) { - serviceCache := make(map[string]ServiceDetails) - for _, service := range services.Items { - if svcComponent, exists := service.Spec.Selector["component"]; exists && svcComponent == "model-registry-server" { +func (kc *KubernetesClient) GetServiceDetails() ([]ServiceDetails, error) { + //TODO (ederign) review the context timeout + + //TODO (ederign) when we develop the front-end, implement subject access review here + // and check if the username has actually permissions to access that server + // currently on kf dashboard, the user name comes in kubeflow-userid + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() // Ensure the context is canceled to free up resources + + serviceList := &corev1.ServiceList{} + + err := kc.Client.List(ctx, serviceList, &client.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to list services: %w", err) + } + + var services []ServiceDetails + + for _, service := range serviceList.Items { + if svcComponent, exists := service.Spec.Selector["component"]; exists && svcComponent == componentName { var httpPort int32 hasHTTPPort := false for _, port := range service.Spec.Ports { @@ -92,62 +193,45 @@ func buildModelRegistryServiceCache(logger *slog.Logger, services v1.ServiceList } } if !hasHTTPPort { - logger.Error("service missing HTTP port", "serviceName", service.Name) + kc.Logger.Error("service missing HTTP port", "serviceName", service.Name) continue } + if service.Spec.ClusterIP == "" { - logger.Error("service missing valid ClusterIP", "serviceName", service.Name) + kc.Logger.Error("service missing valid ClusterIP", "serviceName", service.Name) continue } //TODO (acreasy) DisplayName and Description need to be included and not given a zero value once we // know how this will be implemented. - serviceCache[service.Name] = ServiceDetails{ + serviceDetails := ServiceDetails{ Name: service.Name, ClusterIP: service.Spec.ClusterIP, HTTPPort: httpPort, } + + services = append(services, serviceDetails) } } - return serviceCache, nil + + return services, nil } -func (kc *KubernetesClient) GetServiceNames() ([]string, error) { +func (kc *KubernetesClient) GetServiceDetailsByName(serviceName string) (ServiceDetails, error) { //TODO (ederign) when we develop the front-end, implement subject access review here // and check if the username has actually permissions to access that server // currently on kf dashboard, the user name comes in kubeflow-userid - var serviceNames []string - - for _, service := range kc.ServiceCache { - if service.Name != "" { - serviceNames = append(serviceNames, service.Name) - } + services, err := kc.GetServiceDetails() + if err != nil { + return ServiceDetails{}, fmt.Errorf("failed to get service details: %w", err) } - return serviceNames, nil -} -func (kc *KubernetesClient) GetServiceDetails() ([]ServiceDetails, error) { - var services []ServiceDetails - - for _, service := range kc.ServiceCache { - if service.Name != "" { - services = append(services, ServiceDetails{ - Name: service.Name, - DisplayName: service.DisplayName, - Description: service.Description, - }) + for _, service := range services { + if service.Name == serviceName { + return service, nil } } - return services, nil -} - -func (kc *KubernetesClient) GetServiceDetailsByName(serviceName string) (ServiceDetails, error) { - - service, exists := kc.ServiceCache[serviceName] - if !exists { - return ServiceDetails{}, fmt.Errorf("service %s not found in cache", serviceName) - } - return service, nil + return ServiceDetails{}, fmt.Errorf("service %s not found", serviceName) } diff --git a/clients/ui/bff/internal/integrations/k8s_test.go b/clients/ui/bff/internal/integrations/k8s_test.go deleted file mode 100644 index 3ac4f3d1e..000000000 --- a/clients/ui/bff/internal/integrations/k8s_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package integrations - -import ( - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "log/slog" - "os" - "testing" -) - -func TestBuildModelRegistryServiceCache(t *testing.T) { - services := v1.ServiceList{ - Items: []v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "service-dora", - }, - Spec: v1.ServiceSpec{ - ClusterIP: "10.0.0.1", - Ports: []v1.ServicePort{ - { - Name: "http-api", - Port: 80, - }, - }, - Selector: map[string]string{ - "component": "model-registry-server", - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "service-bella", - }, - Spec: v1.ServiceSpec{ - ClusterIP: "10.0.0.2", - Ports: []v1.ServicePort{ - { - Name: "http-api", - Port: 8080, - }, - }, - Selector: map[string]string{ - "component": "model-registry-server", - }, - }, - }, - }, - } - expectedServiceCache := map[string]ServiceDetails{ - "service-dora": { - Name: "service-dora", - ClusterIP: "10.0.0.1", - HTTPPort: 80, - }, - "service-bella": { - Name: "service-bella", - ClusterIP: "10.0.0.2", - HTTPPort: 8080, - }, - } - - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - serviceCache, err := buildModelRegistryServiceCache(logger, services) - assert.NoError(t, err, "unexpected error while building service cache") - assert.Equal(t, expectedServiceCache, serviceCache, "serviceCache does not match expected value") -} diff --git a/clients/ui/bff/internal/mocks/k8s_mock.go b/clients/ui/bff/internal/mocks/k8s_mock.go index 0fec4e03b..b1ac66bc8 100644 --- a/clients/ui/bff/internal/mocks/k8s_mock.go +++ b/clients/ui/bff/internal/mocks/k8s_mock.go @@ -1,6 +1,7 @@ package mocks import ( + "context" k8s "github.com/kubeflow/model-registry/ui/bff/internal/integrations" "github.com/stretchr/testify/mock" "log/slog" @@ -10,6 +11,11 @@ type KubernetesClientMock struct { mock.Mock } +func (m *KubernetesClientMock) Shutdown(ctx context.Context, logger *slog.Logger) error { + logger.Error("Shutdown was called in mock") + return nil +} + func NewKubernetesClient(_ *slog.Logger) (k8s.KubernetesClientInterface, error) { return &KubernetesClientMock{}, nil } diff --git a/clients/ui/bff/internal/models/health_check.go b/clients/ui/bff/internal/models/health_check.go new file mode 100644 index 000000000..daf9e72d2 --- /dev/null +++ b/clients/ui/bff/internal/models/health_check.go @@ -0,0 +1,10 @@ +package models + +type SystemInfo struct { + Version string `json:"version"` +} + +type HealthCheckModel struct { + Status string `json:"status"` + SystemInfo SystemInfo `json:"system_info"` +} diff --git a/clients/ui/bff/internal/models/model_registry.go b/clients/ui/bff/internal/models/model_registry.go new file mode 100644 index 000000000..8e1438c38 --- /dev/null +++ b/clients/ui/bff/internal/models/model_registry.go @@ -0,0 +1,7 @@ +package models + +type ModelRegistryModel struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Description string `json:"description"` +} diff --git a/clients/ui/bff/internal/repositories/health_check.go b/clients/ui/bff/internal/repositories/health_check.go new file mode 100644 index 000000000..2dedfc725 --- /dev/null +++ b/clients/ui/bff/internal/repositories/health_check.go @@ -0,0 +1,21 @@ +package repositories + +import "github.com/kubeflow/model-registry/ui/bff/internal/models" + +type HealthCheckRepository struct{} + +func NewHealthCheckRepository() *HealthCheckRepository { + return &HealthCheckRepository{} +} + +func (r *HealthCheckRepository) HealthCheck(version string) (models.HealthCheckModel, error) { + + var res = models.HealthCheckModel{ + Status: "available", + SystemInfo: models.SystemInfo{ + Version: version, + }, + } + + return res, nil +} diff --git a/clients/ui/bff/internal/data/helpers.go b/clients/ui/bff/internal/repositories/helpers.go similarity index 97% rename from clients/ui/bff/internal/data/helpers.go rename to clients/ui/bff/internal/repositories/helpers.go index 8c19f2c26..25c0931b0 100644 --- a/clients/ui/bff/internal/data/helpers.go +++ b/clients/ui/bff/internal/repositories/helpers.go @@ -1,4 +1,4 @@ -package data +package repositories import ( "fmt" diff --git a/clients/ui/bff/internal/repositories/model_registry.go b/clients/ui/bff/internal/repositories/model_registry.go new file mode 100644 index 000000000..a60b22790 --- /dev/null +++ b/clients/ui/bff/internal/repositories/model_registry.go @@ -0,0 +1,34 @@ +package repositories + +import ( + "fmt" + k8s "github.com/kubeflow/model-registry/ui/bff/internal/integrations" + "github.com/kubeflow/model-registry/ui/bff/internal/models" +) + +type ModelRegistryRepository struct { +} + +func NewModelRegistryRepository() *ModelRegistryRepository { + return &ModelRegistryRepository{} +} + +func (m *ModelRegistryRepository) FetchAllModelRegistries(client k8s.KubernetesClientInterface) ([]models.ModelRegistryModel, error) { + + resources, err := client.GetServiceDetails() + if err != nil { + return nil, fmt.Errorf("error fetching model registries: %w", err) + } + + var registries = []models.ModelRegistryModel{} + for _, item := range resources { + registry := models.ModelRegistryModel{ + Name: item.Name, + Description: item.Description, + DisplayName: item.DisplayName, + } + registries = append(registries, registry) + } + + return registries, nil +} diff --git a/clients/ui/bff/internal/data/model_registry_client.go b/clients/ui/bff/internal/repositories/model_registry_client.go similarity index 94% rename from clients/ui/bff/internal/data/model_registry_client.go rename to clients/ui/bff/internal/repositories/model_registry_client.go index 0f0426a67..fe1a0158b 100644 --- a/clients/ui/bff/internal/data/model_registry_client.go +++ b/clients/ui/bff/internal/repositories/model_registry_client.go @@ -1,4 +1,4 @@ -package data +package repositories import ( "log/slog" diff --git a/clients/ui/bff/internal/data/model_registry_test.go b/clients/ui/bff/internal/repositories/model_registry_test.go similarity index 72% rename from clients/ui/bff/internal/data/model_registry_test.go rename to clients/ui/bff/internal/repositories/model_registry_test.go index c4ada6137..c86a2c218 100644 --- a/clients/ui/bff/internal/data/model_registry_test.go +++ b/clients/ui/bff/internal/repositories/model_registry_test.go @@ -1,7 +1,8 @@ -package data +package repositories import ( "github.com/kubeflow/model-registry/ui/bff/internal/mocks" + "github.com/kubeflow/model-registry/ui/bff/internal/models" "github.com/stretchr/testify/assert" "testing" ) @@ -9,13 +10,13 @@ import ( func TestFetchAllModelRegistry(t *testing.T) { mockK8sClient, _ := mocks.NewKubernetesClient(nil) - model := ModelRegistryModel{} + mrClient := NewModelRegistryRepository() - registries, err := model.FetchAllModelRegistries(mockK8sClient) + registries, err := mrClient.FetchAllModelRegistries(mockK8sClient) assert.NoError(t, err) - expectedRegistries := []ModelRegistryModel{ + expectedRegistries := []models.ModelRegistryModel{ {Name: "model-registry", Description: "Model registry description", DisplayName: "Model Registry"}, {Name: "model-registry-dora", Description: "Model registry dora description", DisplayName: "Model Registry Dora"}, {Name: "model-registry-bella", Description: "Model registry bella description", DisplayName: "Model Registry Bella"}, diff --git a/clients/ui/bff/internal/data/model_version.go b/clients/ui/bff/internal/repositories/model_version.go similarity index 99% rename from clients/ui/bff/internal/data/model_version.go rename to clients/ui/bff/internal/repositories/model_version.go index c149a177c..526b187b4 100644 --- a/clients/ui/bff/internal/data/model_version.go +++ b/clients/ui/bff/internal/repositories/model_version.go @@ -1,4 +1,4 @@ -package data +package repositories import ( "bytes" diff --git a/clients/ui/bff/internal/data/model_version_test.go b/clients/ui/bff/internal/repositories/model_version_test.go similarity index 99% rename from clients/ui/bff/internal/data/model_version_test.go rename to clients/ui/bff/internal/repositories/model_version_test.go index b56f8274e..e5c389ff7 100644 --- a/clients/ui/bff/internal/data/model_version_test.go +++ b/clients/ui/bff/internal/repositories/model_version_test.go @@ -1,4 +1,4 @@ -package data +package repositories import ( "encoding/json" diff --git a/clients/ui/bff/internal/data/registered_model.go b/clients/ui/bff/internal/repositories/registered_model.go similarity index 99% rename from clients/ui/bff/internal/data/registered_model.go rename to clients/ui/bff/internal/repositories/registered_model.go index bc082feb0..e93552726 100644 --- a/clients/ui/bff/internal/data/registered_model.go +++ b/clients/ui/bff/internal/repositories/registered_model.go @@ -1,4 +1,4 @@ -package data +package repositories import ( "bytes" diff --git a/clients/ui/bff/internal/data/registered_model_test.go b/clients/ui/bff/internal/repositories/registered_model_test.go similarity index 99% rename from clients/ui/bff/internal/data/registered_model_test.go rename to clients/ui/bff/internal/repositories/registered_model_test.go index a3117a71f..d94aed1db 100644 --- a/clients/ui/bff/internal/data/registered_model_test.go +++ b/clients/ui/bff/internal/repositories/registered_model_test.go @@ -1,4 +1,4 @@ -package data +package repositories import ( "encoding/json" diff --git a/clients/ui/bff/internal/repositories/repositories.go b/clients/ui/bff/internal/repositories/repositories.go new file mode 100644 index 000000000..e0e660d66 --- /dev/null +++ b/clients/ui/bff/internal/repositories/repositories.go @@ -0,0 +1,16 @@ +package repositories + +// Repositories struct is a single convenient container to hold and represent all our repositories. +type Repositories struct { + HealthCheck *HealthCheckRepository + ModelRegistry *ModelRegistryRepository + ModelRegistryClient ModelRegistryClientInterface +} + +func NewRepositories(modelRegistryClient ModelRegistryClientInterface) *Repositories { + return &Repositories{ + HealthCheck: NewHealthCheckRepository(), + ModelRegistry: NewModelRegistryRepository(), + ModelRegistryClient: modelRegistryClient, + } +}