diff --git a/README.md b/README.md index 37ae95883..f7faef0cb 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ For a high-level documentation of the Model Registry _logical model_, please che ## Model Registry Core The model registry core is the layer which implements the core/business logic by interacting with the underlying ml-metadata server. -It provides a model registry domain-specific [api](internal/core/api.go) that is in charge to proxy all, appropriately transformed, requests to ml-metadata using gRPC calls. +It provides a model registry domain-specific [api](pkg/api/api.go) that is in charge to proxy all, appropriately transformed, requests to ml-metadata using gRPC calls. ### Model registry library diff --git a/clients/python/poetry.lock b/clients/python/poetry.lock index 344cd26d3..71cce3eba 100644 --- a/clients/python/poetry.lock +++ b/clients/python/poetry.lock @@ -427,73 +427,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.9" +version = "7.6.10" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, - {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, - {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, - {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, - {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, - {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, - {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, - {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, - {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, - {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, - {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, - {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, - {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, - {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, - {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, - {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, + {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, + {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, ] [package.dependencies] @@ -515,13 +515,13 @@ files = [ [[package]] name = "eval-type-backport" -version = "0.2.0" +version = "0.2.2" description = "Like `typing._eval_type`, but lets older Python versions use newer typing features." optional = false python-versions = ">=3.8" files = [ - {file = "eval_type_backport-0.2.0-py3-none-any.whl", hash = "sha256:ac2f73d30d40c5a30a80b8739a789d6bb5e49fdffa66d7912667e2015d9c9933"}, - {file = "eval_type_backport-0.2.0.tar.gz", hash = "sha256:68796cfbc7371ebf923f03bdf7bef415f3ec098aeced24e054b253a0e78f7b37"}, + {file = "eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a"}, + {file = "eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1"}, ] [package.extras] @@ -708,13 +708,13 @@ files = [ [[package]] name = "huggingface-hub" -version = "0.27.0" +version = "0.27.1" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = true python-versions = ">=3.8.0" files = [ - {file = "huggingface_hub-0.27.0-py3-none-any.whl", hash = "sha256:8f2e834517f1f1ddf1ecc716f91b120d7333011b7485f665a9a412eacb1a2a81"}, - {file = "huggingface_hub-0.27.0.tar.gz", hash = "sha256:902cce1a1be5739f5589e560198a65a8edcfd3b830b1666f36e4b961f0454fac"}, + {file = "huggingface_hub-0.27.1-py3-none-any.whl", hash = "sha256:1c5155ca7d60b60c2e2fc38cbb3ffb7f7c3adf48f824015b219af9061771daec"}, + {file = "huggingface_hub-0.27.1.tar.gz", hash = "sha256:c004463ca870283909d715d20f066ebd6968c2207dae9393fdffb3c1d4d8f98b"}, ] [package.dependencies] @@ -1484,13 +1484,13 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-asyncio" -version = "0.25.0" +version = "0.25.2" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" files = [ - {file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"}, - {file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"}, + {file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"}, + {file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"}, ] [package.dependencies] @@ -1615,29 +1615,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.8.4" +version = "0.9.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.8.4-py3-none-linux_armv6l.whl", hash = "sha256:58072f0c06080276804c6a4e21a9045a706584a958e644353603d36ca1eb8a60"}, - {file = "ruff-0.8.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ffb60904651c00a1e0b8df594591770018a0f04587f7deeb3838344fe3adabac"}, - {file = "ruff-0.8.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ddf5d654ac0d44389f6bf05cee4caeefc3132a64b58ea46738111d687352296"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e248b1f0fa2749edd3350a2a342b67b43a2627434c059a063418e3d375cfe643"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf197b98ed86e417412ee3b6c893f44c8864f816451441483253d5ff22c0e81e"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c41319b85faa3aadd4d30cb1cffdd9ac6b89704ff79f7664b853785b48eccdf3"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9f8402b7c4f96463f135e936d9ab77b65711fcd5d72e5d67597b543bbb43cf3f"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4e56b3baa9c23d324ead112a4fdf20db9a3f8f29eeabff1355114dd96014604"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:736272574e97157f7edbbb43b1d046125fce9e7d8d583d5d65d0c9bf2c15addf"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fe710ab6061592521f902fca7ebcb9fabd27bc7c57c764298b1c1f15fff720"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:13e9ec6d6b55f6da412d59953d65d66e760d583dd3c1c72bf1f26435b5bfdbae"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:97d9aefef725348ad77d6db98b726cfdb075a40b936c7984088804dfd38268a7"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ab78e33325a6f5374e04c2ab924a3367d69a0da36f8c9cb6b894a62017506111"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8ef06f66f4a05c3ddbc9121a8b0cecccd92c5bf3dd43b5472ffe40b8ca10f0f8"}, - {file = "ruff-0.8.4-py3-none-win32.whl", hash = "sha256:552fb6d861320958ca5e15f28b20a3d071aa83b93caee33a87b471f99a6c0835"}, - {file = "ruff-0.8.4-py3-none-win_amd64.whl", hash = "sha256:f21a1143776f8656d7f364bd264a9d60f01b7f52243fbe90e7670c0dfe0cf65d"}, - {file = "ruff-0.8.4-py3-none-win_arm64.whl", hash = "sha256:9183dd615d8df50defa8b1d9a074053891ba39025cf5ae88e8bcb52edcc4bf08"}, - {file = "ruff-0.8.4.tar.gz", hash = "sha256:0d5f89f254836799af1615798caa5f80b7f935d7a670fad66c5007928e57ace8"}, + {file = "ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347"}, + {file = "ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00"}, + {file = "ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247"}, + {file = "ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e"}, + {file = "ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe"}, + {file = "ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb"}, + {file = "ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a"}, + {file = "ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145"}, + {file = "ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5"}, + {file = "ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6"}, + {file = "ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0"}, ] [[package]] @@ -2284,4 +2284,4 @@ hf = ["huggingface-hub"] [metadata] lock-version = "2.0" python-versions = ">= 3.9, < 4.0" -content-hash = "d827bd45dcd093cb2bb79340259a18c514cb7c94a038285128a3c07cdd3fe11c" +content-hash = "44699c95dc84d7d44cbc22d14d66f8f664f92a8b24bea9329a1479ca57e924f8" diff --git a/clients/python/pyproject.toml b/clients/python/pyproject.toml index 3cf6923b6..4f8e0ef09 100644 --- a/clients/python/pyproject.toml +++ b/clients/python/pyproject.toml @@ -45,7 +45,7 @@ sphinx-autobuild = ">=2021.3.14,<2025.0.0" pytest = ">=7.4.2,<9.0.0" coverage = { extras = ["toml"], version = "^7.3.2" } pytest-cov = ">=4.1,<7.0" -ruff = ">=0.5.2,<0.9.0" +ruff = ">=0.5.2,<0.10.0" mypy = "^1.7.0" pytest-asyncio = ">=0.23.7,<0.26.0" requests = "^2.32.2" diff --git a/clients/ui/bff/Makefile b/clients/ui/bff/Makefile index 96f26131c..4cbf7d79e 100644 --- a/clients/ui/bff/Makefile +++ b/clients/ui/bff/Makefile @@ -8,6 +8,8 @@ STANDALONE_MODE ?= true STATIC_ASSETS_DIR ?= ./static # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. ENVTEST_K8S_VERSION = 1.29.0 +LOG_LEVEL ?= info +ALLOWED_ORIGINS ?= "" .PHONY: all all: build @@ -48,7 +50,7 @@ build: fmt vet test ## Builds the project to produce a binary executable. .PHONY: run run: fmt vet envtest ## Runs the project. ENVTEST_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \ - go run ./cmd/main.go --port=$(PORT) --static-assets-dir=$(STATIC_ASSETS_DIR) --mock-k8s-client=$(MOCK_K8S_CLIENT) --mock-mr-client=$(MOCK_MR_CLIENT) --dev-mode=$(DEV_MODE) --dev-mode-port=$(DEV_MODE_PORT) --standalone-mode=$(STANDALONE_MODE) + go run ./cmd/main.go --port=$(PORT) --static-assets-dir=$(STATIC_ASSETS_DIR) --mock-k8s-client=$(MOCK_K8S_CLIENT) --mock-mr-client=$(MOCK_MR_CLIENT) --dev-mode=$(DEV_MODE) --dev-mode-port=$(DEV_MODE_PORT) --standalone-mode=$(STANDALONE_MODE) --log-level=$(LOG_LEVEL) --allowed-origins=$(ALLOWED_ORIGINS) ##@ Dependencies diff --git a/clients/ui/bff/README.md b/clients/ui/bff/README.md index c6adf2512..474bbde35 100644 --- a/clients/ui/bff/README.md +++ b/clients/ui/bff/README.md @@ -35,6 +35,11 @@ If you want to use a different port, mock kubernetes client or model registry cl ```shell make run PORT=8000 MOCK_K8S_CLIENT=true MOCK_MR_CLIENT=true ``` +If you want to change the log level on deployment, add the LOG_LEVEL argument when running, supported levels are: ERROR, WARN, INFO, DEBUG. The default level is INFO. +```shell +# Run with debug logging +make run LOG_LEVEL=DEBUG +``` # Building and Deploying @@ -282,3 +287,41 @@ Access to Model Registry List: Access to Specific Model Registry Endpoints: - For other endpoints (e.g., /v1/model_registry/{model_registry_id}/...), we perform a SAR check for get and list verbs on the specific service (identified by model_registry_id) within the namespace. - If the user or any group has permission to get or list the specific service, the request is authorized. + +#### 4. How do I allow CORS requests from other origins + +When serving the UI directly from the BFF there is no need for any CORS headers to be served, by default they are turned off for security reasons. + +If you need to enable CORS for any reasons you can add origins to the allow-list in several ways: + +##### Via the make command +Add the following parameter to your command: `ALLOWED_ORIGINS` this takes a comma separated list of origins to permit serving to, alterantively you can specify the value `*` to allow all origins, **Note this is not recommended in production deployments as it poses a security risk** + +Examples: + +```shell +# Allow only the origin http://example.com:8081 +make run ALLOWED_ORIGINS="http://example.com:8081" + +# Allow the origins http://example.com and http://very-nice.com +make run ALLOWED_ORIGINS="http://example.com,http://very-nice.com" + +# Allow all origins +make run ALLOWED_ORIGINS="*" + +# Explicitly disable CORS (default behaviour) +make run ALLOWED_ORIGINS="" +``` + +#### Via environment variable +Setting CORS via environment variable follows the same rules as using the Makefile, simply set the environment variable `ALLOWED_ORIGINS` with the same value as above. + +#### Via the command line arguments + +Setting CORS via command line arguments follows the same rules as using the Makefile. Simply add the `--allowed-origins=` flag to your command. + +Examples: +```shell +./bff --allowed-origins="http://my-domain.com,http://my-other-domain.com" +``` + diff --git a/clients/ui/bff/cmd/main.go b/clients/ui/bff/cmd/main.go index a38ae341c..eb2657ddd 100644 --- a/clients/ui/bff/cmd/main.go +++ b/clients/ui/bff/cmd/main.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "os/signal" + "strings" "syscall" "github.com/kubeflow/model-registry/ui/bff/internal/api" @@ -26,9 +27,13 @@ func main() { flag.IntVar(&cfg.DevModePort, "dev-mode-port", getEnvAsInt("DEV_MODE_PORT", 8080), "Use port when in development mode") flag.BoolVar(&cfg.StandaloneMode, "standalone-mode", false, "Use standalone mode for enabling endpoints in standalone mode") flag.StringVar(&cfg.StaticAssetsDir, "static-assets-dir", "./static", "Configure frontend static assets root directory") + flag.StringVar(&cfg.LogLevel, "log-level", getEnvAsString("LOG_LEVEL", "info"), "Sets server log level, possible values: debug, info, warn, error, fatal") + flag.StringVar(&cfg.AllowedOrigins, "allowed-origins", getEnvAsString("ALLOWED_ORIGINS", ""), "Sets allowed origins for CORS purposes, accepts a comma separated list of origins or * to allow all, default none") flag.Parse() - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: getLogLevelFromString(cfg.LogLevel), + })) app, err := api.NewApp(cfg, logger) if err != nil { @@ -88,3 +93,27 @@ func getEnvAsInt(name string, defaultVal int) int { } return defaultVal } + +func getEnvAsString(name string, defaultVal string) string { + if value, exists := os.LookupEnv(name); exists { + return value + } + return defaultVal +} + +func getLogLevelFromString(level string) slog.Level { + switch strings.ToLower(level) { + case "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + case "fatal": + return slog.LevelError + + } + return slog.LevelInfo +} diff --git a/clients/ui/bff/go.mod b/clients/ui/bff/go.mod index 58102e714..64204468a 100644 --- a/clients/ui/bff/go.mod +++ b/clients/ui/bff/go.mod @@ -4,10 +4,12 @@ go 1.22.2 require ( github.com/brianvoe/gofakeit/v7 v7.1.2 + github.com/google/uuid v1.6.0 github.com/julienschmidt/httprouter v1.3.0 github.com/kubeflow/model-registry v0.2.9 github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/gomega v1.35.1 + github.com/rs/cors v1.11.1 github.com/stretchr/testify v1.9.0 k8s.io/api v0.31.2 k8s.io/apimachinery v0.31.4 @@ -36,7 +38,6 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect - github.com/google/uuid v1.6.0 // indirect github.com/imdario/mergo v0.3.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -56,11 +57,11 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.30.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.26.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect diff --git a/clients/ui/bff/go.sum b/clients/ui/bff/go.sum index bc28ba1d6..94e1cc65c 100644 --- a/clients/ui/bff/go.sum +++ b/clients/ui/bff/go.sum @@ -97,6 +97,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg 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/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -131,8 +133,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -141,14 +143,14 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/clients/ui/bff/internal/api/app.go b/clients/ui/bff/internal/api/app.go index 129b41c8d..054434e08 100644 --- a/clients/ui/bff/internal/api/app.go +++ b/clients/ui/bff/internal/api/app.go @@ -138,5 +138,5 @@ func (app *App) Routes() http.Handler { http.ServeFile(w, r, path.Join(app.config.StaticAssetsDir, "index.html")) }) - return app.RecoverPanic(app.enableCORS(app.InjectUserHeaders(appMux))) + return app.RecoverPanic(app.EnableTelemetry(app.EnableCORS(app.InjectUserHeaders(appMux)))) } diff --git a/clients/ui/bff/internal/api/healthcheck__handler_test.go b/clients/ui/bff/internal/api/healthcheck__handler_test.go index 0212a58cd..e830afb21 100644 --- a/clients/ui/bff/internal/api/healthcheck__handler_test.go +++ b/clients/ui/bff/internal/api/healthcheck__handler_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "github.com/kubeflow/model-registry/ui/bff/internal/config" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" "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" @@ -26,7 +27,7 @@ func TestHealthCheckHandler(t *testing.T) { rr := httptest.NewRecorder() req, err := http.NewRequest(http.MethodGet, HealthCheckPath, nil) - ctx := context.WithValue(req.Context(), KubeflowUserIdKey, mocks.KubeflowUserIDHeaderValue) + ctx := context.WithValue(req.Context(), constants.KubeflowUserIdKey, mocks.KubeflowUserIDHeaderValue) req = req.WithContext(ctx) assert.NoError(t, err) diff --git a/clients/ui/bff/internal/api/healthcheck_handler.go b/clients/ui/bff/internal/api/healthcheck_handler.go index 6ee2049a2..4692d6be7 100644 --- a/clients/ui/bff/internal/api/healthcheck_handler.go +++ b/clients/ui/bff/internal/api/healthcheck_handler.go @@ -3,12 +3,13 @@ package api import ( "errors" "github.com/julienschmidt/httprouter" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" "net/http" ) func (app *App) HealthcheckHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - userId, ok := r.Context().Value(KubeflowUserIdKey).(string) + userId, ok := r.Context().Value(constants.KubeflowUserIdKey).(string) if !ok || userId == "" { app.serverErrorResponse(w, r, errors.New("failed to retrieve kubeflow-userid from context")) return diff --git a/clients/ui/bff/internal/api/helpers.go b/clients/ui/bff/internal/api/helpers.go index 4f0fb58b8..c11876676 100644 --- a/clients/ui/bff/internal/api/helpers.go +++ b/clients/ui/bff/internal/api/helpers.go @@ -106,3 +106,15 @@ func ParseURLTemplate(tmpl string, params map[string]string) string { return r.Replace(tmpl) } + +func ParseOriginList(origins string) ([]string, bool) { + if origins == "" { + return []string{}, false + } + + if origins == "*" { + return []string{"*"}, true + } + originsTrimmed := strings.ReplaceAll(origins, " ", "") + return strings.Split(originsTrimmed, ","), true +} diff --git a/clients/ui/bff/internal/api/helpers_test.go b/clients/ui/bff/internal/api/helpers_test.go index 4cf02821b..492693fe8 100644 --- a/clients/ui/bff/internal/api/helpers_test.go +++ b/clients/ui/bff/internal/api/helpers_test.go @@ -19,3 +19,42 @@ func TestParseURLTemplateWhenEmpty(t *testing.T) { actual := ParseURLTemplate("", nil) assert.Empty(t, actual) } + +func TestParseOriginListAllowAll(t *testing.T) { + expected := []string{"*"} + + actual, ok := ParseOriginList("*") + + assert.True(t, ok) + assert.Equal(t, expected, actual) +} + +func TestParseOriginListEmpty(t *testing.T) { + actual, ok := ParseOriginList("") + + assert.False(t, ok) + assert.Empty(t, actual) +} + +func TestParseOriginListSingle(t *testing.T) { + expected := []string{"http://test.com"} + + actual, ok := ParseOriginList("http://test.com") + + assert.True(t, ok) + assert.Equal(t, expected, actual) +} + +func TestParseOriginListMultiple(t *testing.T) { + expected := []string{"http://test.com", "http://test2.com"} + actual, ok := ParseOriginList("http://test.com,http://test2.com") + assert.True(t, ok) + assert.Equal(t, expected, actual) +} + +func TestParseOriginListMultipleAndSpaces(t *testing.T) { + expected := []string{"http://test.com", "http://test2.com"} + actual, ok := ParseOriginList("http://test.com, http://test2.com") + assert.True(t, ok) + assert.Equal(t, expected, actual) +} diff --git a/clients/ui/bff/internal/api/middleware.go b/clients/ui/bff/internal/api/middleware.go index d70fe9acb..cb0a8d08a 100644 --- a/clients/ui/bff/internal/api/middleware.go +++ b/clients/ui/bff/internal/api/middleware.go @@ -4,27 +4,16 @@ import ( "context" "errors" "fmt" - "net/http" - "strings" - + "github.com/google/uuid" "github.com/julienschmidt/httprouter" "github.com/kubeflow/model-registry/ui/bff/internal/config" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" "github.com/kubeflow/model-registry/ui/bff/internal/integrations" -) - -type contextKey string - -const ( - ModelRegistryHttpClientKey contextKey = "ModelRegistryHttpClientKey" - NamespaceHeaderParameterKey contextKey = "namespace" - - //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's supported 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" + "github.com/rs/cors" + "log/slog" + "net/http" + "runtime/debug" + "strings" ) func (app *App) RecoverPanic(next http.Handler) http.Handler { @@ -33,6 +22,7 @@ func (app *App) RecoverPanic(next http.Handler) http.Handler { if err := recover(); err != nil { w.Header().Set("Connection", "close") app.serverErrorResponse(w, r, fmt.Errorf("%s", err)) + app.logger.Error("Recover from panic: " + string(debug.Stack())) } }() @@ -49,8 +39,8 @@ func (app *App) InjectUserHeaders(next http.Handler) http.Handler { return } - userIdHeader := r.Header.Get(KubeflowUserIDHeader) - userGroupsHeader := r.Header.Get(KubeflowUserGroupsIdHeader) + userIdHeader := r.Header.Get(constants.KubeflowUserIDHeader) + userGroupsHeader := r.Header.Get(constants.KubeflowUserGroupsIdHeader) //`kubeflow-userid`: Contains the user's email address. if userIdHeader == "" { app.badRequestResponse(w, r, errors.New("missing required header: kubeflow-userid")) @@ -70,20 +60,54 @@ func (app *App) InjectUserHeaders(next http.Handler) http.Handler { } ctx := r.Context() - ctx = context.WithValue(ctx, KubeflowUserIdKey, userIdHeader) - ctx = context.WithValue(ctx, KubeflowUserGroupsKey, userGroups) + ctx = context.WithValue(ctx, constants.KubeflowUserIdKey, userIdHeader) + ctx = context.WithValue(ctx, constants.KubeflowUserGroupsKey, userGroups) next.ServeHTTP(w, r.WithContext(ctx)) }) } -func (app *App) enableCORS(next http.Handler) http.Handler { +func (app *App) EnableCORS(next http.Handler) http.Handler { + allowedOrigins, ok := ParseOriginList(app.config.AllowedOrigins) + + if !ok { + return next + } + + c := cors.New(cors.Options{ + AllowedOrigins: allowedOrigins, + AllowCredentials: true, + AllowedMethods: []string{"GET", "PUT", "POST", "PATCH", "DELETE"}, + AllowedHeaders: []string{constants.KubeflowUserIDHeader, constants.KubeflowUserGroupsIdHeader}, + Debug: strings.ToLower(app.config.LogLevel) == "debug", + OptionsPassthrough: false, + }) + + return c.Handler(next) +} + +func (app *App) EnableTelemetry(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. - // TODO(ederign) deal with preflight requests - w.Header().Set("Access-Control-Allow-Origin", "*") + // Adds a unique id to the context to allow tracing of requests + traceId := uuid.NewString() + ctx := context.WithValue(r.Context(), constants.TraceIdKey, traceId) + + // logger will only be nil in tests. + if app.logger != nil { + traceLogger := app.logger.With(slog.String("trace_id", traceId)) + ctx = context.WithValue(ctx, constants.TraceLoggerKey, traceLogger) + + if traceLogger.Enabled(ctx, slog.LevelDebug) { + cloneBody, err := integrations.CloneBody(r) + if err != nil { + traceLogger.Debug("Error reading request body for debug logging", "error", err) + } + ////TODO (Alex) Log headers, BUT we must ensure we don't log confidential data like tokens etc. + traceLogger.Debug("Incoming HTTP request", "method", r.Method, "url", r.URL.String(), "body", cloneBody) + } + } - next.ServeHTTP(w, r) + next.ServeHTTP(w, r.WithContext(ctx)) }) } @@ -92,30 +116,42 @@ func (app *App) AttachRESTClient(next func(http.ResponseWriter, *http.Request, h modelRegistryID := ps.ByName(ModelRegistryId) - namespace, ok := r.Context().Value(NamespaceHeaderParameterKey).(string) + namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string) if !ok || namespace == "" { app.badRequestResponse(w, r, fmt.Errorf("missing namespace in the context")) } - modelRegistryBaseURL, err := resolveModelRegistryURL(namespace, modelRegistryID, app.kubernetesClient, app.config) + modelRegistryBaseURL, err := resolveModelRegistryURL(r.Context(), namespace, modelRegistryID, app.kubernetesClient, app.config) if err != nil { app.notFoundResponse(w, r) return } - client, err := integrations.NewHTTPClient(modelRegistryID, modelRegistryBaseURL) + // Set up a child logger for the rest client that automatically adds the request id to all statements for + // tracing. + restClientLogger := app.logger + traceId, ok := r.Context().Value(constants.TraceIdKey).(string) + if app.logger != nil { + if ok { + restClientLogger = app.logger.With(slog.String("trace_id", traceId)) + } else { + app.logger.Warn("Failed to set trace_id for tracing") + } + } + + client, err := integrations.NewHTTPClient(restClientLogger, modelRegistryID, modelRegistryBaseURL) if err != nil { app.serverErrorResponse(w, r, fmt.Errorf("failed to create Kubernetes client: %v", err)) return } - ctx := context.WithValue(r.Context(), ModelRegistryHttpClientKey, client) + ctx := context.WithValue(r.Context(), constants.ModelRegistryHttpClientKey, client) next(w, r.WithContext(ctx), ps) } } -func resolveModelRegistryURL(namespace string, serviceName string, client integrations.KubernetesClientInterface, config config.EnvConfig) (string, error) { +func resolveModelRegistryURL(sessionCtx context.Context, namespace string, serviceName string, client integrations.KubernetesClientInterface, config config.EnvConfig) (string, error) { - serviceDetails, err := client.GetServiceDetailsByName(namespace, serviceName) + serviceDetails, err := client.GetServiceDetailsByName(sessionCtx, namespace, serviceName) if err != nil { return "", err } @@ -131,13 +167,13 @@ func resolveModelRegistryURL(namespace string, serviceName string, client integr func (app *App) AttachNamespace(next func(http.ResponseWriter, *http.Request, httprouter.Params)) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - namespace := r.URL.Query().Get(string(NamespaceHeaderParameterKey)) + namespace := r.URL.Query().Get(string(constants.NamespaceHeaderParameterKey)) if namespace == "" { - app.badRequestResponse(w, r, fmt.Errorf("missing required query parameter: %s", NamespaceHeaderParameterKey)) + app.badRequestResponse(w, r, fmt.Errorf("missing required query parameter: %s", constants.NamespaceHeaderParameterKey)) return } - ctx := context.WithValue(r.Context(), NamespaceHeaderParameterKey, namespace) + ctx := context.WithValue(r.Context(), constants.NamespaceHeaderParameterKey, namespace) r = r.WithContext(ctx) next(w, r, ps) @@ -146,19 +182,19 @@ func (app *App) AttachNamespace(next func(http.ResponseWriter, *http.Request, ht func (app *App) PerformSARonGetListServicesByNamespace(next func(http.ResponseWriter, *http.Request, httprouter.Params)) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - user, ok := r.Context().Value(KubeflowUserIdKey).(string) + user, ok := r.Context().Value(constants.KubeflowUserIdKey).(string) if !ok || user == "" { app.badRequestResponse(w, r, fmt.Errorf("missing user in context")) return } - namespace, ok := r.Context().Value(NamespaceHeaderParameterKey).(string) + namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string) if !ok || namespace == "" { app.badRequestResponse(w, r, fmt.Errorf("missing namespace in context")) return } var userGroups []string - if groups, ok := r.Context().Value(KubeflowUserGroupsKey).([]string); ok { + if groups, ok := r.Context().Value(constants.KubeflowUserGroupsKey).([]string); ok { userGroups = groups } else { userGroups = []string{} @@ -181,13 +217,13 @@ func (app *App) PerformSARonGetListServicesByNamespace(next func(http.ResponseWr func (app *App) PerformSARonSpecificService(next func(http.ResponseWriter, *http.Request, httprouter.Params)) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - user, ok := r.Context().Value(KubeflowUserIdKey).(string) + user, ok := r.Context().Value(constants.KubeflowUserIdKey).(string) if !ok || user == "" { app.badRequestResponse(w, r, fmt.Errorf("missing user in context")) return } - namespace, ok := r.Context().Value(NamespaceHeaderParameterKey).(string) + namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string) if !ok || namespace == "" { app.badRequestResponse(w, r, fmt.Errorf("missing namespace in context")) return @@ -200,7 +236,7 @@ func (app *App) PerformSARonSpecificService(next func(http.ResponseWriter, *http } var userGroups []string - if groups, ok := r.Context().Value(KubeflowUserGroupsKey).([]string); ok { + if groups, ok := r.Context().Value(constants.KubeflowUserGroupsKey).([]string); ok { userGroups = groups } else { userGroups = []string{} diff --git a/clients/ui/bff/internal/api/model_registry_handler.go b/clients/ui/bff/internal/api/model_registry_handler.go index 1600d0045..d5ee69e0c 100644 --- a/clients/ui/bff/internal/api/model_registry_handler.go +++ b/clients/ui/bff/internal/api/model_registry_handler.go @@ -3,6 +3,7 @@ package api import ( "fmt" "github.com/julienschmidt/httprouter" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" "github.com/kubeflow/model-registry/ui/bff/internal/models" "net/http" ) @@ -11,12 +12,12 @@ type ModelRegistryListEnvelope Envelope[[]models.ModelRegistryModel, None] func (app *App) ModelRegistryHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - namespace, ok := r.Context().Value(NamespaceHeaderParameterKey).(string) + namespace, ok := r.Context().Value(constants.NamespaceHeaderParameterKey).(string) if !ok || namespace == "" { app.badRequestResponse(w, r, fmt.Errorf("missing namespace in the context")) } - registries, err := app.repositories.ModelRegistry.GetAllModelRegistries(app.kubernetesClient, namespace) + registries, err := app.repositories.ModelRegistry.GetAllModelRegistries(r.Context(), app.kubernetesClient, namespace) 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 872121ce0..2e37d080c 100644 --- a/clients/ui/bff/internal/api/model_registry_handler_test.go +++ b/clients/ui/bff/internal/api/model_registry_handler_test.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" + "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" @@ -28,7 +30,8 @@ var _ = Describe("TestModelRegistryHandler", func() { requestPath := fmt.Sprintf(" %s?namespace=kubeflow", ModelRegistryListPath) req, err := http.NewRequest(http.MethodGet, requestPath, nil) - ctx := context.WithValue(req.Context(), NamespaceHeaderParameterKey, "kubeflow") + ctx := mocks.NewMockSessionContext(req.Context()) + ctx = context.WithValue(ctx, constants.NamespaceHeaderParameterKey, "kubeflow") req = req.WithContext(ctx) Expect(err).NotTo(HaveOccurred()) diff --git a/clients/ui/bff/internal/api/model_versions_handler.go b/clients/ui/bff/internal/api/model_versions_handler.go index 7c74377bf..27eedd61c 100644 --- a/clients/ui/bff/internal/api/model_versions_handler.go +++ b/clients/ui/bff/internal/api/model_versions_handler.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/julienschmidt/httprouter" "github.com/kubeflow/model-registry/pkg/openapi" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" "github.com/kubeflow/model-registry/ui/bff/internal/integrations" "github.com/kubeflow/model-registry/ui/bff/internal/validation" "net/http" @@ -19,7 +20,7 @@ type ModelArtifactListEnvelope Envelope[*openapi.ModelArtifactList, None] type ModelArtifactEnvelope Envelope[*openapi.ModelArtifact, None] func (app *App) GetAllModelVersionHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) + client, ok := r.Context().Value(constants.ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { app.serverErrorResponse(w, r, errors.New("REST client not found")) return @@ -43,7 +44,7 @@ func (app *App) GetAllModelVersionHandler(w http.ResponseWriter, r *http.Request } func (app *App) GetModelVersionHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) + client, ok := r.Context().Value(constants.ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { app.serverErrorResponse(w, r, errors.New("REST client not found")) return @@ -71,7 +72,7 @@ func (app *App) GetModelVersionHandler(w http.ResponseWriter, r *http.Request, p } func (app *App) CreateModelVersionHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) + client, ok := r.Context().Value(constants.ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { app.serverErrorResponse(w, r, errors.New("REST client not found")) return @@ -125,7 +126,7 @@ func (app *App) CreateModelVersionHandler(w http.ResponseWriter, r *http.Request } func (app *App) UpdateModelVersionHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) + client, ok := r.Context().Value(constants.ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { app.serverErrorResponse(w, r, errors.New("REST client not found")) return @@ -175,7 +176,7 @@ func (app *App) UpdateModelVersionHandler(w http.ResponseWriter, r *http.Request } func (app *App) GetAllModelArtifactsByModelVersionHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) + client, ok := r.Context().Value(constants.ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { app.serverErrorResponse(w, r, errors.New("REST client not found")) return @@ -198,7 +199,7 @@ func (app *App) GetAllModelArtifactsByModelVersionHandler(w http.ResponseWriter, } func (app *App) CreateModelArtifactByModelVersionHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) + client, ok := r.Context().Value(constants.ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { app.serverErrorResponse(w, r, errors.New("REST client not found")) return diff --git a/clients/ui/bff/internal/api/namespaces_handler.go b/clients/ui/bff/internal/api/namespaces_handler.go index 80fb47014..88550531a 100644 --- a/clients/ui/bff/internal/api/namespaces_handler.go +++ b/clients/ui/bff/internal/api/namespaces_handler.go @@ -2,6 +2,7 @@ package api import ( "errors" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" "github.com/kubeflow/model-registry/ui/bff/internal/models" "net/http" @@ -12,14 +13,14 @@ 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) + userId, ok := r.Context().Value(constants.KubeflowUserIdKey).(string) if !ok || userId == "" { app.serverErrorResponse(w, r, errors.New("failed to retrieve kubeflow-userid from context")) return } var userGroups []string - if groups, ok := r.Context().Value(KubeflowUserGroupsKey).([]string); ok { + if groups, ok := r.Context().Value(constants.KubeflowUserGroupsKey).([]string); ok { userGroups = groups } else { userGroups = []string{} diff --git a/clients/ui/bff/internal/api/namespaces_handler_test.go b/clients/ui/bff/internal/api/namespaces_handler_test.go index b4869058f..505956975 100644 --- a/clients/ui/bff/internal/api/namespaces_handler_test.go +++ b/clients/ui/bff/internal/api/namespaces_handler_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "github.com/kubeflow/model-registry/ui/bff/internal/config" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" "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" @@ -31,7 +32,7 @@ var _ = Describe("TestNamespacesHandler", func() { 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) + ctx := context.WithValue(req.Context(), constants.KubeflowUserIdKey, mocks.DoraNonAdminUser) req = req.WithContext(ctx) Expect(err).NotTo(HaveOccurred()) rr := httptest.NewRecorder() @@ -57,7 +58,7 @@ var _ = Describe("TestNamespacesHandler", func() { 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) + ctx := context.WithValue(req.Context(), constants.KubeflowUserIdKey, mocks.KubeflowUserIDHeaderValue) req = req.WithContext(ctx) Expect(err).NotTo(HaveOccurred()) req.Header.Set("kubeflow-userid", "user@example.com") @@ -87,7 +88,7 @@ var _ = Describe("TestNamespacesHandler", 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") + ctx := context.WithValue(req.Context(), constants.KubeflowUserIdKey, "nonexistent@example.com") req = req.WithContext(ctx) Expect(err).NotTo(HaveOccurred()) rr := httptest.NewRecorder() diff --git a/clients/ui/bff/internal/api/registered_models_handler.go b/clients/ui/bff/internal/api/registered_models_handler.go index 7f781f69b..ccd2469dd 100644 --- a/clients/ui/bff/internal/api/registered_models_handler.go +++ b/clients/ui/bff/internal/api/registered_models_handler.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/julienschmidt/httprouter" "github.com/kubeflow/model-registry/pkg/openapi" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" "github.com/kubeflow/model-registry/ui/bff/internal/integrations" "github.com/kubeflow/model-registry/ui/bff/internal/validation" "net/http" @@ -16,7 +17,7 @@ type RegisteredModelListEnvelope Envelope[*openapi.RegisteredModelList, None] type RegisteredModelUpdateEnvelope Envelope[*openapi.RegisteredModelUpdate, None] func (app *App) GetAllRegisteredModelsHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) + client, ok := r.Context().Value(constants.ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { app.serverErrorResponse(w, r, errors.New("REST client not found")) return @@ -39,7 +40,7 @@ func (app *App) GetAllRegisteredModelsHandler(w http.ResponseWriter, r *http.Req } func (app *App) CreateRegisteredModelHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) + client, ok := r.Context().Value(constants.ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { app.serverErrorResponse(w, r, errors.New("REST client not found")) return @@ -93,7 +94,7 @@ func (app *App) CreateRegisteredModelHandler(w http.ResponseWriter, r *http.Requ } func (app *App) GetRegisteredModelHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) + client, ok := r.Context().Value(constants.ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { app.serverErrorResponse(w, r, errors.New("REST client not found")) return @@ -121,7 +122,7 @@ func (app *App) GetRegisteredModelHandler(w http.ResponseWriter, r *http.Request } func (app *App) UpdateRegisteredModelHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) + client, ok := r.Context().Value(constants.ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { app.serverErrorResponse(w, r, errors.New("REST client not found")) return @@ -171,7 +172,7 @@ func (app *App) UpdateRegisteredModelHandler(w http.ResponseWriter, r *http.Requ } func (app *App) GetAllModelVersionsForRegisteredModelHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) + client, ok := r.Context().Value(constants.ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { app.serverErrorResponse(w, r, errors.New("REST client not found")) return @@ -195,7 +196,7 @@ func (app *App) GetAllModelVersionsForRegisteredModelHandler(w http.ResponseWrit } func (app *App) CreateModelVersionForRegisteredModelHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) + client, ok := r.Context().Value(constants.ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { app.serverErrorResponse(w, r, errors.New("REST client not found")) return diff --git a/clients/ui/bff/internal/api/test_utils.go b/clients/ui/bff/internal/api/test_utils.go index 952a03337..1f8f0cc93 100644 --- a/clients/ui/bff/internal/api/test_utils.go +++ b/clients/ui/bff/internal/api/test_utils.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" k8s "github.com/kubeflow/model-registry/ui/bff/internal/integrations" "github.com/kubeflow/model-registry/ui/bff/internal/mocks" "github.com/kubeflow/model-registry/ui/bff/internal/repositories" @@ -46,16 +47,16 @@ func setupApiTest[T any](method string, url string, body interface{}, k8sClient } // Set the kubeflow-userid header - req.Header.Set(KubeflowUserIDHeader, kubeflowUserIDHeaderValue) + req.Header.Set(constants.KubeflowUserIDHeader, kubeflowUserIDHeaderValue) - ctx := req.Context() - ctx = context.WithValue(ctx, ModelRegistryHttpClientKey, mockClient) - ctx = context.WithValue(ctx, KubeflowUserIdKey, kubeflowUserIDHeaderValue) - ctx = context.WithValue(ctx, NamespaceHeaderParameterKey, namespace) + ctx := mocks.NewMockSessionContext(req.Context()) + ctx = context.WithValue(ctx, constants.ModelRegistryHttpClientKey, mockClient) + ctx = context.WithValue(ctx, constants.KubeflowUserIdKey, kubeflowUserIDHeaderValue) + ctx = context.WithValue(ctx, constants.NamespaceHeaderParameterKey, namespace) mrHttpClient := k8s.HTTPClient{ ModelRegistryID: "model-registry", } - ctx = context.WithValue(ctx, ModelRegistryHttpClientKey, mrHttpClient) + ctx = context.WithValue(ctx, constants.ModelRegistryHttpClientKey, mrHttpClient) req = req.WithContext(ctx) rr := httptest.NewRecorder() diff --git a/clients/ui/bff/internal/api/user_handler.go b/clients/ui/bff/internal/api/user_handler.go index 9ec135ccc..1359ab18b 100644 --- a/clients/ui/bff/internal/api/user_handler.go +++ b/clients/ui/bff/internal/api/user_handler.go @@ -3,6 +3,7 @@ package api import ( "errors" "github.com/julienschmidt/httprouter" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" "github.com/kubeflow/model-registry/ui/bff/internal/models" "net/http" ) @@ -11,7 +12,7 @@ type UserEnvelope Envelope[*models.User, None] func (app *App) UserHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - userId, ok := r.Context().Value(KubeflowUserIdKey).(string) + userId, ok := r.Context().Value(constants.KubeflowUserIdKey).(string) if !ok || userId == "" { app.serverErrorResponse(w, r, errors.New("failed to retrieve kubeflow-userid from context")) return diff --git a/clients/ui/bff/internal/api/user_handler_test.go b/clients/ui/bff/internal/api/user_handler_test.go index 13cbf95a8..2927fa101 100644 --- a/clients/ui/bff/internal/api/user_handler_test.go +++ b/clients/ui/bff/internal/api/user_handler_test.go @@ -3,6 +3,7 @@ package api import ( "context" "encoding/json" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" "github.com/kubeflow/model-registry/ui/bff/internal/mocks" "io" "net/http" @@ -34,7 +35,7 @@ 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) + ctx := context.WithValue(req.Context(), constants.KubeflowUserIdKey, mocks.KubeflowUserIDHeaderValue) req = req.WithContext(ctx) Expect(err).NotTo(HaveOccurred()) @@ -62,7 +63,7 @@ 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) + ctx := context.WithValue(req.Context(), constants.KubeflowUserIdKey, DoraNonAdminUser) req = req.WithContext(ctx) Expect(err).NotTo(HaveOccurred()) @@ -92,7 +93,7 @@ var _ = Describe("TestUserHandler", func() { By("creating the http request") req, err := http.NewRequest(http.MethodGet, UserPath, nil) - ctx := context.WithValue(req.Context(), KubeflowUserIdKey, randomUser) + ctx := context.WithValue(req.Context(), constants.KubeflowUserIdKey, randomUser) req = req.WithContext(ctx) Expect(err).NotTo(HaveOccurred()) diff --git a/clients/ui/bff/internal/config/environment.go b/clients/ui/bff/internal/config/environment.go index 6017701c8..1efa5c8d2 100644 --- a/clients/ui/bff/internal/config/environment.go +++ b/clients/ui/bff/internal/config/environment.go @@ -8,4 +8,6 @@ type EnvConfig struct { StandaloneMode bool DevModePort int StaticAssetsDir string + LogLevel string + AllowedOrigins string } diff --git a/clients/ui/bff/internal/constants/keys.go b/clients/ui/bff/internal/constants/keys.go new file mode 100644 index 000000000..e4c9f1aba --- /dev/null +++ b/clients/ui/bff/internal/constants/keys.go @@ -0,0 +1,21 @@ +package constants + +type contextKey string + +// NOTE: If you are adding any HTTP headers, they need to also be added to the EnableCORS middleware +// to ensure requests are not blocked when using CORS. +const ( + ModelRegistryHttpClientKey contextKey = "ModelRegistryHttpClientKey" + NamespaceHeaderParameterKey contextKey = "namespace" + + //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's supported 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" + + TraceIdKey contextKey = "TraceIdKey" + TraceLoggerKey contextKey = "TraceLoggerKey" +) diff --git a/clients/ui/bff/internal/integrations/http.go b/clients/ui/bff/internal/integrations/http.go index 712b8556f..fea2bf54e 100644 --- a/clients/ui/bff/internal/integrations/http.go +++ b/clients/ui/bff/internal/integrations/http.go @@ -1,16 +1,18 @@ package integrations import ( + "context" "crypto/tls" "encoding/json" "fmt" + "github.com/google/uuid" "io" + "log/slog" "net/http" "strconv" ) type HTTPClientInterface interface { - GetModelRegistryID() (modelRegistryService string) GET(url string) ([]byte, error) POST(url string, body io.Reader) ([]byte, error) PATCH(url string, body io.Reader) ([]byte, error) @@ -20,6 +22,7 @@ type HTTPClient struct { client *http.Client baseURL string ModelRegistryID string + logger *slog.Logger } type ErrorResponse struct { @@ -36,7 +39,7 @@ func (e *HTTPError) Error() string { return fmt.Sprintf("HTTP %d: %s - %s", e.StatusCode, e.Code, e.Message) } -func NewHTTPClient(modelRegistryID string, baseURL string) (HTTPClientInterface, error) { +func NewHTTPClient(logger *slog.Logger, modelRegistryID string, baseURL string) (HTTPClientInterface, error) { return &HTTPClient{ client: &http.Client{Transport: &http.Transport{ @@ -44,6 +47,7 @@ func NewHTTPClient(modelRegistryID string, baseURL string) (HTTPClientInterface, }}, baseURL: baseURL, ModelRegistryID: modelRegistryID, + logger: logger, }, nil } @@ -52,12 +56,16 @@ func (c *HTTPClient) GetModelRegistryID() string { } func (c *HTTPClient) GET(url string) ([]byte, error) { + requestId := uuid.NewString() + fullURL := c.baseURL + url req, err := http.NewRequest("GET", fullURL, nil) if err != nil { return nil, err } + logUpstreamReq(c.logger, requestId, req) + response, err := c.client.Do(req) if err != nil { return nil, err @@ -65,6 +73,7 @@ func (c *HTTPClient) GET(url string) ([]byte, error) { defer response.Body.Close() body, err := io.ReadAll(response.Body) + logUpstreamResp(c.logger, requestId, response, body) if err != nil { return nil, fmt.Errorf("error reading response body: %w", err) } @@ -72,6 +81,8 @@ func (c *HTTPClient) GET(url string) ([]byte, error) { } func (c *HTTPClient) POST(url string, body io.Reader) ([]byte, error) { + requestId := uuid.NewString() + fullURL := c.baseURL + url fmt.Println(fullURL) req, err := http.NewRequest("POST", fullURL, body) @@ -81,6 +92,8 @@ func (c *HTTPClient) POST(url string, body io.Reader) ([]byte, error) { req.Header.Set("Content-Type", "application/json") + logUpstreamReq(c.logger, requestId, req) + response, err := c.client.Do(req) if err != nil { return nil, err @@ -88,6 +101,7 @@ func (c *HTTPClient) POST(url string, body io.Reader) ([]byte, error) { defer response.Body.Close() responseBody, err := io.ReadAll(response.Body) + logUpstreamResp(c.logger, requestId, response, responseBody) if err != nil { return nil, fmt.Errorf("error reading response body: %w", err) } @@ -120,8 +134,12 @@ func (c *HTTPClient) PATCH(url string, body io.Reader) ([]byte, error) { return nil, err } + requestId := uuid.NewString() + req.Header.Set("Content-Type", "application/json") + logUpstreamReq(c.logger, requestId, req) + response, err := c.client.Do(req) if err != nil { return nil, err @@ -129,6 +147,7 @@ func (c *HTTPClient) PATCH(url string, body io.Reader) ([]byte, error) { defer response.Body.Close() responseBody, err := io.ReadAll(response.Body) + logUpstreamResp(c.logger, requestId, response, responseBody) if err != nil { return nil, fmt.Errorf("error reading response body: %w", err) } @@ -152,3 +171,19 @@ func (c *HTTPClient) PATCH(url string, body io.Reader) ([]byte, error) { } return responseBody, nil } + +func logUpstreamReq(logger *slog.Logger, reqId string, req *http.Request) { + if logger.Enabled(context.TODO(), slog.LevelDebug) { + var body []byte + if req.Body != nil { + body, _ = CloneBody(req) + } + logger.Debug("Making upstream HTTP request", "request_id", reqId, "method", req.Method, "url", req.URL.String(), "body", body) + } +} + +func logUpstreamResp(logger *slog.Logger, reqId string, resp *http.Response, body []byte) { + if logger.Enabled(context.TODO(), slog.LevelDebug) { + logger.Debug("Received upstream HTTP response", "request_id", reqId, "status_code", resp.StatusCode, "body", body) + } +} diff --git a/clients/ui/bff/internal/integrations/http_helpers.go b/clients/ui/bff/internal/integrations/http_helpers.go new file mode 100644 index 000000000..214f9efba --- /dev/null +++ b/clients/ui/bff/internal/integrations/http_helpers.go @@ -0,0 +1,23 @@ +package integrations + +import ( + "bytes" + "fmt" + "io" + "net/http" +) + +func CloneBody(r *http.Request) ([]byte, error) { + if r.Body == nil { + return nil, fmt.Errorf("no body provided") + } + buf, _ := io.ReadAll(r.Body) + readerCopy := io.NopCloser(bytes.NewBuffer(buf)) + readerOriginal := io.NopCloser(bytes.NewBuffer(buf)) + r.Body = readerOriginal + + defer readerCopy.Close() + cloneBody, err := io.ReadAll(readerCopy) + + return cloneBody, err +} diff --git a/clients/ui/bff/internal/integrations/k8s.go b/clients/ui/bff/internal/integrations/k8s.go index 9b89bb02c..19e52ca51 100644 --- a/clients/ui/bff/internal/integrations/k8s.go +++ b/clients/ui/bff/internal/integrations/k8s.go @@ -3,6 +3,7 @@ package integrations import ( "context" "fmt" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" helper "github.com/kubeflow/model-registry/ui/bff/internal/helpers" authv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" @@ -22,9 +23,9 @@ import ( const ComponentLabelValue = "model-registry" type KubernetesClientInterface interface { - GetServiceNames(namespace string) ([]string, error) - GetServiceDetailsByName(namespace string, serviceName string) (ServiceDetails, error) - GetServiceDetails(namespace string) ([]ServiceDetails, error) + GetServiceNames(sessionCtx context.Context, namespace string) ([]string, error) + GetServiceDetailsByName(sessionCtx context.Context, namespace string, serviceName string) (ServiceDetails, error) + GetServiceDetails(sessionCtx context.Context, namespace string) ([]ServiceDetails, error) BearerToken() (string, error) Shutdown(ctx context.Context, logger *slog.Logger) error IsInCluster() bool @@ -152,8 +153,8 @@ func (kc *KubernetesClient) BearerToken() (string, error) { return kc.Token, nil } -func (kc *KubernetesClient) GetServiceNames(namespace string) ([]string, error) { - services, err := kc.GetServiceDetails(namespace) +func (kc *KubernetesClient) GetServiceNames(sessionCtx context.Context, namespace string) ([]string, error) { + services, err := kc.GetServiceDetails(sessionCtx, namespace) if err != nil { return nil, err } @@ -166,7 +167,7 @@ func (kc *KubernetesClient) GetServiceNames(namespace string) ([]string, error) return names, nil } -func (kc *KubernetesClient) GetServiceDetails(namespace string) ([]ServiceDetails, error) { +func (kc *KubernetesClient) GetServiceDetails(sessionCtx context.Context, namespace string) ([]ServiceDetails, error) { if namespace == "" { return nil, fmt.Errorf("namespace cannot be empty") @@ -175,6 +176,8 @@ func (kc *KubernetesClient) GetServiceDetails(namespace string) ([]ServiceDetail ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + sessionLogger := sessionCtx.Value(constants.TraceLoggerKey).(*slog.Logger) + serviceList := &corev1.ServiceList{} labelSelector := labels.SelectorFromSet(labels.Set{ @@ -202,12 +205,12 @@ func (kc *KubernetesClient) GetServiceDetails(namespace string) ([]ServiceDetail } } if !hasHTTPPort { - kc.Logger.Error("service missing HTTP port", "serviceName", service.Name) + sessionLogger.Error("service missing HTTP port", "serviceName", service.Name) continue } if service.Spec.ClusterIP == "" { - kc.Logger.Error("service missing valid ClusterIP", "serviceName", service.Name) + sessionLogger.Error("service missing valid ClusterIP", "serviceName", service.Name) continue } @@ -220,11 +223,11 @@ func (kc *KubernetesClient) GetServiceDetails(namespace string) ([]ServiceDetail } if displayName == "" { - kc.Logger.Warn("service missing displayName annotation", "serviceName", service.Name) + sessionLogger.Warn("service missing displayName annotation", "serviceName", service.Name) } if description == "" { - kc.Logger.Warn("service missing description annotation", "serviceName", service.Name) + sessionLogger.Warn("service missing description annotation", "serviceName", service.Name) } serviceDetails := ServiceDetails{ @@ -242,8 +245,8 @@ func (kc *KubernetesClient) GetServiceDetails(namespace string) ([]ServiceDetail return services, nil } -func (kc *KubernetesClient) GetServiceDetailsByName(namespace string, serviceName string) (ServiceDetails, error) { - services, err := kc.GetServiceDetails(namespace) +func (kc *KubernetesClient) GetServiceDetailsByName(sessionCtx context.Context, namespace string, serviceName string) (ServiceDetails, error) { + services, err := kc.GetServiceDetails(sessionCtx, namespace) if err != nil { return ServiceDetails{}, fmt.Errorf("failed to get service details: %w", err) } diff --git a/clients/ui/bff/internal/mocks/k8s_mock.go b/clients/ui/bff/internal/mocks/k8s_mock.go index ce2b3e61a..7a74e65df 100644 --- a/clients/ui/bff/internal/mocks/k8s_mock.go +++ b/clients/ui/bff/internal/mocks/k8s_mock.go @@ -185,8 +185,8 @@ func setupMock(mockK8sClient client.Client, ctx context.Context) error { return nil } -func (m *KubernetesClientMock) GetServiceDetails(namespace string) ([]k8s.ServiceDetails, error) { - originalServices, err := m.KubernetesClient.GetServiceDetails(namespace) +func (m *KubernetesClientMock) GetServiceDetails(sessionCtx context.Context, namespace string) ([]k8s.ServiceDetails, error) { + originalServices, err := m.KubernetesClient.GetServiceDetails(sessionCtx, namespace) if err != nil { return nil, fmt.Errorf("failed to get service details: %w", err) } @@ -199,8 +199,8 @@ func (m *KubernetesClientMock) GetServiceDetails(namespace string) ([]k8s.Servic return originalServices, nil } -func (m *KubernetesClientMock) GetServiceDetailsByName(namespace string, serviceName string) (k8s.ServiceDetails, error) { - originalService, err := m.KubernetesClient.GetServiceDetailsByName(namespace, serviceName) +func (m *KubernetesClientMock) GetServiceDetailsByName(sessionCtx context.Context, namespace string, serviceName string) (k8s.ServiceDetails, error) { + originalService, err := m.KubernetesClient.GetServiceDetailsByName(sessionCtx, namespace, serviceName) if err != nil { return k8s.ServiceDetails{}, fmt.Errorf("failed to get service details: %w", err) } diff --git a/clients/ui/bff/internal/mocks/k8s_mock_test.go b/clients/ui/bff/internal/mocks/k8s_mock_test.go index e236326aa..3f84783fa 100644 --- a/clients/ui/bff/internal/mocks/k8s_mock_test.go +++ b/clients/ui/bff/internal/mocks/k8s_mock_test.go @@ -11,7 +11,7 @@ var _ = Describe("Kubernetes ControllerRuntimeClient Test", func() { It("should retrieve the get all service successfully", func() { By("getting service details") - services, err := k8sClient.GetServiceDetails("kubeflow") + services, err := k8sClient.GetServiceDetails(NewMockSessionContextNoParent(), "kubeflow") Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request") By("checking that all services have the modified ClusterIP and HTTPPort") @@ -37,7 +37,7 @@ var _ = Describe("Kubernetes ControllerRuntimeClient Test", func() { It("should retrieve the service details by name", func() { By("getting service by name") - service, err := k8sClient.GetServiceDetailsByName("dora-namespace", "model-registry-dora") + service, err := k8sClient.GetServiceDetailsByName(NewMockSessionContextNoParent(), "dora-namespace", "model-registry-dora") Expect(err).NotTo(HaveOccurred(), "Failed to create k8s request") By("checking that service details are correct") @@ -49,7 +49,7 @@ var _ = Describe("Kubernetes ControllerRuntimeClient Test", func() { It("should retrieve the services names", func() { By("getting service by name") - services, err := k8sClient.GetServiceNames("kubeflow") + services, err := k8sClient.GetServiceNames(NewMockSessionContextNoParent(), "kubeflow") Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request") By("checking that service details are correct") diff --git a/clients/ui/bff/internal/mocks/static_data_mock.go b/clients/ui/bff/internal/mocks/static_data_mock.go index bb0408c27..252a285e8 100644 --- a/clients/ui/bff/internal/mocks/static_data_mock.go +++ b/clients/ui/bff/internal/mocks/static_data_mock.go @@ -1,7 +1,12 @@ package mocks import ( + "context" + "github.com/google/uuid" "github.com/kubeflow/model-registry/pkg/openapi" + "github.com/kubeflow/model-registry/ui/bff/internal/constants" + "log/slog" + "os" ) func GetRegisteredModelMocks() []openapi.RegisteredModel { @@ -200,3 +205,19 @@ func newCustomProperties() *map[string]openapi.MetadataValue { return &result } + +func NewMockSessionContext(parent context.Context) context.Context { + if parent == nil { + parent = context.TODO() + } + traceId := uuid.NewString() + ctx := context.WithValue(parent, constants.TraceIdKey, traceId) + + traceLogger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + ctx = context.WithValue(ctx, constants.TraceLoggerKey, traceLogger) + return ctx +} + +func NewMockSessionContextNoParent() context.Context { + return NewMockSessionContext(context.TODO()) +} diff --git a/clients/ui/bff/internal/repositories/model_registry.go b/clients/ui/bff/internal/repositories/model_registry.go index db4175952..60ec9bbb0 100644 --- a/clients/ui/bff/internal/repositories/model_registry.go +++ b/clients/ui/bff/internal/repositories/model_registry.go @@ -1,6 +1,7 @@ package repositories import ( + "context" "fmt" k8s "github.com/kubeflow/model-registry/ui/bff/internal/integrations" "github.com/kubeflow/model-registry/ui/bff/internal/models" @@ -13,9 +14,9 @@ func NewModelRegistryRepository() *ModelRegistryRepository { return &ModelRegistryRepository{} } -func (m *ModelRegistryRepository) GetAllModelRegistries(client k8s.KubernetesClientInterface, namespace string) ([]models.ModelRegistryModel, error) { +func (m *ModelRegistryRepository) GetAllModelRegistries(sessionCtx context.Context, client k8s.KubernetesClientInterface, namespace string) ([]models.ModelRegistryModel, error) { - resources, err := client.GetServiceDetails(namespace) + resources, err := client.GetServiceDetails(sessionCtx, namespace) if err != nil { return nil, fmt.Errorf("error fetching model registries: %w", err) } diff --git a/clients/ui/bff/internal/repositories/model_registry_test.go b/clients/ui/bff/internal/repositories/model_registry_test.go index a5a0d903b..06ef12621 100644 --- a/clients/ui/bff/internal/repositories/model_registry_test.go +++ b/clients/ui/bff/internal/repositories/model_registry_test.go @@ -1,6 +1,7 @@ package repositories import ( + "github.com/kubeflow/model-registry/ui/bff/internal/mocks" "github.com/kubeflow/model-registry/ui/bff/internal/models" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -13,7 +14,7 @@ var _ = Describe("TestFetchAllModelRegistry", func() { By("fetching all model registries in the repository") modelRegistryRepository := NewModelRegistryRepository() - registries, err := modelRegistryRepository.GetAllModelRegistries(k8sClient, "kubeflow") + registries, err := modelRegistryRepository.GetAllModelRegistries(mocks.NewMockSessionContextNoParent(), k8sClient, "kubeflow") Expect(err).NotTo(HaveOccurred()) By("should match the expected model registries") @@ -28,7 +29,7 @@ var _ = Describe("TestFetchAllModelRegistry", func() { By("fetching all model registries in the repository") modelRegistryRepository := NewModelRegistryRepository() - registries, err := modelRegistryRepository.GetAllModelRegistries(k8sClient, "dora-namespace") + registries, err := modelRegistryRepository.GetAllModelRegistries(mocks.NewMockSessionContextNoParent(), k8sClient, "dora-namespace") Expect(err).NotTo(HaveOccurred()) By("should match the expected model registries") @@ -42,7 +43,7 @@ var _ = Describe("TestFetchAllModelRegistry", func() { By("fetching all model registries in the repository") modelRegistryRepository := NewModelRegistryRepository() - registries, err := modelRegistryRepository.GetAllModelRegistries(k8sClient, "no-namespace") + registries, err := modelRegistryRepository.GetAllModelRegistries(mocks.NewMockSessionContextNoParent(), k8sClient, "no-namespace") Expect(err).NotTo(HaveOccurred()) By("should be empty") diff --git a/go.mod b/go.mod index 823874a65..1d56c5b3c 100644 --- a/go.mod +++ b/go.mod @@ -78,10 +78,10 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.31.0 // indirect + golang.org/x/crypto v0.32.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/term v0.27.0 // indirect + golang.org/x/term v0.28.0 // indirect golang.org/x/time v0.5.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/api v0.171.0 // indirect @@ -148,8 +148,8 @@ require ( github.com/yusufpapurcu/wmi v1.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa - golang.org/x/net v0.28.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 0851d86eb..42678cc5e 100644 --- a/go.sum +++ b/go.sum @@ -368,8 +368,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= @@ -400,8 +400,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -438,10 +438,10 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=