From 1a386d7341719cda9468e4e616b90bf42f78b16e Mon Sep 17 00:00:00 2001 From: Lucas Fernandez Date: Thu, 9 Jan 2025 13:07:22 +0100 Subject: [PATCH 01/10] Add env.test file (#691) Signed-off-by: lucferbux --- clients/ui/.env.test | 1 + clients/ui/.gitignore | 2 ++ clients/ui/frontend/.env.test | 1 + .../frontend/src/__tests__/cypress/cypress/utils/testConfig.ts | 1 + 4 files changed, 5 insertions(+) create mode 100644 clients/ui/.env.test create mode 100644 clients/ui/frontend/.env.test diff --git a/clients/ui/.env.test b/clients/ui/.env.test new file mode 100644 index 000000000..afd935233 --- /dev/null +++ b/clients/ui/.env.test @@ -0,0 +1 @@ +APP_ENV=test diff --git a/clients/ui/.gitignore b/clients/ui/.gitignore index 93251d2f4..a7dcdc1fc 100644 --- a/clients/ui/.gitignore +++ b/clients/ui/.gitignore @@ -6,3 +6,5 @@ .DS_Store .env*.local + +!*.test diff --git a/clients/ui/frontend/.env.test b/clients/ui/frontend/.env.test new file mode 100644 index 000000000..425074805 --- /dev/null +++ b/clients/ui/frontend/.env.test @@ -0,0 +1 @@ +APP_ENV=test \ No newline at end of file diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/utils/testConfig.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/utils/testConfig.ts index 630c911e7..c9edfeb4e 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/utils/testConfig.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/utils/testConfig.ts @@ -5,6 +5,7 @@ import dotenv from 'dotenv'; [ `.env.cypress${env.CY_MOCK ? '.mock' : ''}.local`, `.env.cypress${env.CY_MOCK ? '.mock' : ''}`, + '.env.test', '.env.local', '.env', ].forEach((file) => From 7c9c056a0be8a5a914bee2feb68eec25b5db9816 Mon Sep 17 00:00:00 2001 From: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> Date: Sun, 12 Jan 2025 17:38:07 -0500 Subject: [PATCH 02/10] Add Istio overlay for UI integration with Central Dashboard (#595) Signed-off-by: Griffin-Sullivan --- clients/ui/frontend/config/dotenv.js | 2 + clients/ui/frontend/config/webpack.common.js | 5 +- clients/ui/frontend/package.json | 2 +- .../frontend/src/__mocks__/mockNamespace.ts | 9 ++++ .../cypress/cypress/pages/modelRegistry.ts | 2 +- .../cypress/pages/modelRegistrySettings.ts | 2 +- .../modelRegistryView/modelVersionArchive.ts | 8 +-- .../modelRegistryView/modelVersionDetails.ts | 2 +- .../modelRegistryView/registerModelPage.ts | 2 +- .../registeredModelArchive.ts | 10 ++-- .../cypress/cypress/support/commands/api.ts | 7 ++- .../__tests__/cypress/cypress/support/e2e.ts | 11 ++++ .../tests/mocked/modelVersionArchive.cy.ts | 10 ++-- .../tests/mocked/modelVersionDetails.cy.ts | 2 +- .../cypress/tests/mocked/modelVersions.cy.ts | 12 ++--- .../tests/mocked/registeredModelArchive.cy.ts | 20 ++++---- clients/ui/frontend/src/app/App.tsx | 22 ++++---- clients/ui/frontend/src/app/AppRoutes.tsx | 10 ++-- .../src/app/context/ModelRegistryContext.tsx | 3 +- .../ModelVersionDetails.tsx | 2 +- .../screens/ModelVersions/ModelVersions.tsx | 2 +- .../ArchiveModelVersionDetailsBreadcrumb.tsx | 2 +- .../ModelVersionArchiveDetailsBreadcrumb.tsx | 2 +- .../ModelVersionsArchive.tsx | 2 +- ...egisteredModelArchiveDetailsBreadcrumb.tsx | 2 +- .../RegisteredModelsArchive.tsx | 2 +- .../pages/modelRegistry/screens/routeUtils.ts | 2 +- clients/ui/frontend/src/index.html | 2 +- clients/ui/frontend/src/shared/api/k8s.ts | 35 ++++++------- .../shared/hooks/useQueryParamNamespaces.ts | 4 +- .../ui/frontend/src/shared/utilities/const.ts | 12 ++++- clients/ui/manifests/{base => }/README.md | 5 ++ clients/ui/manifests/base/kustomization.yaml | 10 ++-- ...> model-registry-bff-service-account.yaml} | 3 +- .../base/model-registry-bff-service.yaml | 6 ++- .../base/model-registry-ui-deployment.yaml | 2 +- .../base/model-registry-ui-service.yaml | 8 ++- .../kubeflow/kubeflow-dashboard-rbac.yaml | 51 +++++++++++++++++++ .../ui/manifests/kubeflow/kustomization.yaml | 6 ++- .../model-registry-ui-deployment.yaml | 2 +- .../istio/authorization-policy-bff.yaml | 13 +++++ .../istio/authorization-policy-ui.yaml | 16 ++++++ .../overlays/istio/destination-rule-bff.yaml | 11 ++++ .../overlays/istio/destination-rule-ui.yaml | 11 ++++ .../overlays/istio/kustomization.yaml | 17 +++++++ .../istio/model-registry-ui-service.yaml | 3 ++ .../overlays/istio/virtual-service.yaml | 26 ++++++++++ .../manifests/standalone/kustomization.yaml | 2 +- 48 files changed, 307 insertions(+), 95 deletions(-) create mode 100644 clients/ui/frontend/src/__mocks__/mockNamespace.ts rename clients/ui/manifests/{base => }/README.md (92%) rename clients/ui/manifests/base/{model-registry-service-account.yaml => model-registry-bff-service-account.yaml} (59%) create mode 100644 clients/ui/manifests/kubeflow/kubeflow-dashboard-rbac.yaml create mode 100644 clients/ui/manifests/overlays/istio/authorization-policy-bff.yaml create mode 100644 clients/ui/manifests/overlays/istio/authorization-policy-ui.yaml create mode 100644 clients/ui/manifests/overlays/istio/destination-rule-bff.yaml create mode 100644 clients/ui/manifests/overlays/istio/destination-rule-ui.yaml create mode 100644 clients/ui/manifests/overlays/istio/kustomization.yaml create mode 100644 clients/ui/manifests/overlays/istio/model-registry-ui-service.yaml create mode 100644 clients/ui/manifests/overlays/istio/virtual-service.yaml diff --git a/clients/ui/frontend/config/dotenv.js b/clients/ui/frontend/config/dotenv.js index c0e765f70..ae601dfc8 100644 --- a/clients/ui/frontend/config/dotenv.js +++ b/clients/ui/frontend/config/dotenv.js @@ -157,6 +157,7 @@ const setupDotenvFilesForEnv = ({ env }) => { const PROXY_PORT = process.env.PROXY_PORT || process.env.PORT || 4000; const DEV_MODE = process.env.DEV_MODE || undefined; const OUTPUT_ONLY = process.env._OUTPUT_ONLY === 'true'; + const DEPLOYMENT_MODE = process.env.DEPLOYMENT_MODE || 'integrated'; process.env._RELATIVE_DIRNAME = RELATIVE_DIRNAME; process.env._IS_PROJECT_ROOT_DIR = IS_ROOT; @@ -172,6 +173,7 @@ const setupDotenvFilesForEnv = ({ env }) => { process.env._PROXY_PORT = PROXY_PORT; process.env._OUTPUT_ONLY = OUTPUT_ONLY; process.env._DEV_MODE = DEV_MODE; + process.env._DEPLOYMENT_MODE = DEPLOYMENT_MODE; }; module.exports = { setupWebpackDotenvFilesForEnv, setupDotenvFilesForEnv }; diff --git a/clients/ui/frontend/config/webpack.common.js b/clients/ui/frontend/config/webpack.common.js index 8b8a000ec..c88cd17e2 100644 --- a/clients/ui/frontend/config/webpack.common.js +++ b/clients/ui/frontend/config/webpack.common.js @@ -14,6 +14,8 @@ const OUTPUT_ONLY = process.env._OUTPUT_ONLY; const FAVICON = process.env.FAVICON; const PRODUCT_NAME = process.env.PRODUCT_NAME; const COVERAGE = process.env.COVERAGE; +const DEPLOYMENT_MODE = process.env._DEPLOYMENT_MODE; +const BASE_PATH = DEPLOYMENT_MODE === 'integrated' ? '/model-registry/' : PUBLIC_PATH; if (OUTPUT_ONLY !== 'true') { console.info( @@ -173,7 +175,7 @@ module.exports = (env) => { output: { filename: '[name].bundle.js', path: DIST_DIR, - publicPath: PUBLIC_PATH, + publicPath: BASE_PATH, }, plugins: [ ...setupWebpackDotenvFilesForEnv({ @@ -184,6 +186,7 @@ module.exports = (env) => { template: path.join(SRC_DIR, 'index.html'), title: PRODUCT_NAME, favicon: path.join(SRC_DIR, 'images', FAVICON), + baseUrl: BASE_PATH }), new CopyPlugin({ patterns: [ diff --git a/clients/ui/frontend/package.json b/clients/ui/frontend/package.json index 502fd92f0..56550f229 100644 --- a/clients/ui/frontend/package.json +++ b/clients/ui/frontend/package.json @@ -27,7 +27,7 @@ "cypress:open:mock": "CY_MOCK=1 npm run cypress:open -- ", "cypress:run": "cypress run -b chrome --project src/__tests__/cypress", "cypress:run:mock": "CY_MOCK=1 npm run cypress:run -- ", - "cypress:server:build": "DIST_DIR=./public-cypress POLL_INTERVAL=9999999 FAST_POLL_INTERVAL=9999999 npm run build", + "cypress:server:build": "DIST_DIR=./public-cypress POLL_INTERVAL=9999999 DEPLOYMENT_MODE=standalone MOCK_AUTH=true npm run build", "cypress:server": "serve ./public-cypress -p 9001 -s -L" }, "devDependencies": { diff --git a/clients/ui/frontend/src/__mocks__/mockNamespace.ts b/clients/ui/frontend/src/__mocks__/mockNamespace.ts new file mode 100644 index 000000000..90d86c0e2 --- /dev/null +++ b/clients/ui/frontend/src/__mocks__/mockNamespace.ts @@ -0,0 +1,9 @@ +import { Namespace } from '~/shared/types'; + +type MockNamespace = { + name?: string; +}; + +export const mockNamespace = ({ name = 'kubeflow' }: MockNamespace): Namespace => ({ + name, +}); diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts index 8984975ae..6e4b28591 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts @@ -59,7 +59,7 @@ class ModelRegistry { } visit() { - cy.visit(`/modelRegistry`); + cy.visit(`/model-registry`); this.wait(); } diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistrySettings.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistrySettings.ts index 1bac8692c..efa003802 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistrySettings.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistrySettings.ts @@ -28,7 +28,7 @@ export enum DatabaseDetailsTestId { class ModelRegistrySettings { visit(wait = true) { - cy.visit('/modelRegistrySettings'); + cy.visit('/model-registry-settings'); if (wait) { this.wait(); } diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/modelVersionArchive.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/modelVersionArchive.ts index 81afa0638..951b3b04c 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/modelVersionArchive.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/modelVersionArchive.ts @@ -57,7 +57,7 @@ class ModelVersionArchive { visit() { const rmId = '1'; const preferredModelRegistry = 'modelregistry-sample'; - cy.visit(`/modelRegistry/${preferredModelRegistry}/registeredModels/${rmId}/versions/archive`); + cy.visit(`/model-registry/${preferredModelRegistry}/registeredModels/${rmId}/versions/archive`); this.wait(); } @@ -66,14 +66,14 @@ class ModelVersionArchive { const rmId = '1'; const preferredModelRegistry = 'modelregistry-sample'; cy.visit( - `/modelRegistry/${preferredModelRegistry}/registeredModels/${rmId}/versions/archive/${mvId}`, + `/model-registry/${preferredModelRegistry}/registeredModels/${rmId}/versions/archive/${mvId}`, ); } visitModelVersionList() { const rmId = '1'; const preferredModelRegistry = 'modelregistry-sample'; - cy.visit(`/modelRegistry/${preferredModelRegistry}/registeredModels/${rmId}/versions`); + cy.visit(`/model-registry/${preferredModelRegistry}/registeredModels/${rmId}/versions`); this.wait(); } @@ -81,7 +81,7 @@ class ModelVersionArchive { const mvId = '3'; const rmId = '1'; const preferredModelRegistry = 'modelregistry-sample'; - cy.visit(`/modelRegistry/${preferredModelRegistry}/registeredModels/${rmId}/versions/${mvId}`); + cy.visit(`/model-registry/${preferredModelRegistry}/registeredModels/${rmId}/versions/${mvId}`); this.wait(); } diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/modelVersionDetails.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/modelVersionDetails.ts index b311fa896..c3d44c5f7 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/modelVersionDetails.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/modelVersionDetails.ts @@ -3,7 +3,7 @@ class ModelVersionDetails { const preferredModelRegistry = 'modelregistry-sample'; const rmId = '1'; const mvId = '1'; - cy.visit(`/modelRegistry/${preferredModelRegistry}/registeredModels/${rmId}/versions/${mvId}`); + cy.visit(`/model-registry/${preferredModelRegistry}/registeredModels/${rmId}/versions/${mvId}`); this.wait(); } diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/registerModelPage.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/registerModelPage.ts index 7b55feaec..80d38bd61 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/registerModelPage.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/registerModelPage.ts @@ -17,7 +17,7 @@ export enum FormFieldSelector { class RegisterModelPage { visit() { const preferredModelRegistry = 'modelregistry-sample'; - cy.visit(`/modelRegistry/${preferredModelRegistry}/registerModel`); + cy.visit(`/model-registry/${preferredModelRegistry}/registerModel`); this.wait(); } diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/registeredModelArchive.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/registeredModelArchive.ts index 05b1186ea..561db8e36 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/registeredModelArchive.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelRegistryView/registeredModelArchive.ts @@ -56,31 +56,31 @@ class ModelArchive { visit() { const preferredModelRegistry = 'modelregistry-sample'; - cy.visit(`/modelRegistry/${preferredModelRegistry}/registeredModels/archive`); + cy.visit(`/model-registry/${preferredModelRegistry}/registeredModels/archive`); this.wait(); } visitArchiveModelDetail() { const rmId = '2'; const preferredModelRegistry = 'modelregistry-sample'; - cy.visit(`/modelRegistry/${preferredModelRegistry}/registeredModels/archive/${rmId}`); + cy.visit(`/model-registry/${preferredModelRegistry}/registeredModels/archive/${rmId}`); } visitArchiveModelVersionList() { const rmId = '2'; const preferredModelRegistry = 'modelregistry-sample'; - cy.visit(`/modelRegistry/${preferredModelRegistry}/registeredModels/archive/${rmId}/versions`); + cy.visit(`/model-registry/${preferredModelRegistry}/registeredModels/archive/${rmId}/versions`); } visitModelList() { - cy.visit('/modelRegistry/modelregistry-sample'); + cy.visit('/model-registry/modelregistry-sample'); this.wait(); } visitModelDetails() { const rmId = '2'; const preferredModelRegistry = 'modelregistry-sample'; - cy.visit(`/modelRegistry/${preferredModelRegistry}/registeredModels/${rmId}`); + cy.visit(`/model-registry/${preferredModelRegistry}/registeredModels/${rmId}`); this.wait(); } diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/support/commands/api.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/support/commands/api.ts index 54cbb27e3..f4fc6018d 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/support/commands/api.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/support/commands/api.ts @@ -9,7 +9,7 @@ import type { RegisteredModel, RegisteredModelList, } from '~/app/types'; -import type { UserSettings } from '~/shared/types'; +import type { Namespace, UserSettings } from '~/shared/types'; const MODEL_REGISTRY_API_VERSION = 'v1'; export { MODEL_REGISTRY_API_VERSION }; @@ -108,6 +108,11 @@ declare global { type: 'GET /api/:apiVersion/user', options: { path: { apiVersion: string } }, response: ApiResponse, + ) => Cypress.Chainable) & + (( + type: 'GET /api/:apiVersion/namespaces', + options: { path: { apiVersion: string } }, + response: ApiResponse, ) => Cypress.Chainable); } } diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/support/e2e.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/support/e2e.ts index 5b196a706..f69a3b97c 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/support/e2e.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/support/e2e.ts @@ -18,6 +18,7 @@ import '@cypress/code-coverage/support'; import { mockUserSettings } from '~/__mocks__/mockUserSettings'; import 'cypress-mochawesome-reporter/register'; import './commands'; +import { mockNamespace } from '~/__mocks__/mockNamespace'; import { MODEL_REGISTRY_API_VERSION } from './commands/api'; chai.use(chaiSubset); @@ -40,5 +41,15 @@ beforeEach(() => { }, mockUserSettings({}), ); + + cy.interceptApi( + 'GET /api/:apiVersion/namespaces', + { + path: { + apiVersion: MODEL_REGISTRY_API_VERSION, + }, + }, + [mockNamespace({})], + ); } }); diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersionArchive.cy.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersionArchive.cy.ts index 291250583..eeed18eda 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersionArchive.cy.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersionArchive.cy.ts @@ -129,7 +129,7 @@ describe('Model version archive list', () => { it('No archive versions in the selected registered model', () => { initIntercepts({ modelVersions: [mockModelVersion({ id: '3', name: 'model version 2' })] }); modelVersionArchive.visitModelVersionList(); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/1/versions'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/1/versions'); modelVersionArchive .findModelVersionsTableKebab() .findDropdownItem('View archived versions') @@ -140,15 +140,15 @@ describe('Model version archive list', () => { it('Archived version details browser back button should lead to archived versions table', () => { initIntercepts({}); modelVersionArchive.visit(); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/1/versions/archive'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/1/versions/archive'); modelVersionArchive.findArchiveVersionBreadcrumbItem().contains('Archived version'); const archiveVersionRow = modelVersionArchive.getRow('model version 2'); archiveVersionRow.findName().contains('model version 2').click(); verifyRelativeURL( - '/modelRegistry/modelregistry-sample/registeredModels/1/versions/archive/2/details', + '/model-registry/modelregistry-sample/registeredModels/1/versions/archive/2/details', ); cy.go('back'); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/1/versions/archive'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/1/versions/archive'); modelVersionArchive.findArchiveVersionBreadcrumbItem().contains('Archived version'); archiveVersionRow.findName().contains('model version 2').should('exist'); }); @@ -156,7 +156,7 @@ describe('Model version archive list', () => { it('Archive version list', () => { initIntercepts({}); modelVersionArchive.visit(); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/1/versions/archive'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/1/versions/archive'); //breadcrumb modelVersionArchive.findArchiveVersionBreadcrumbItem().contains('Archived version'); diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersionDetails.cy.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersionDetails.cy.ts index c07827db9..58f28ac6a 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersionDetails.cy.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersionDetails.cy.ts @@ -139,7 +139,7 @@ describe('Model version details', () => { it('Model version details page header', () => { verifyRelativeURL( - '/modelRegistry/modelregistry-sample/registeredModels/1/versions/1/details', + '/model-registry/modelregistry-sample/registeredModels/1/versions/1/details', ); cy.findByTestId('app-page-title').should('have.text', 'Version 1'); cy.findByTestId('breadcrumb-version-name').should('have.text', 'Version 1'); diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersions.cy.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersions.cy.ts index 7ad00d4a0..842392f6b 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersions.cy.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersions.cy.ts @@ -110,7 +110,7 @@ describe('Model Versions', () => { modelRegistry.visit(); const registeredModelRow = modelRegistry.getRow('Fraud detection model'); registeredModelRow.findName().contains('Fraud detection model').click(); - verifyRelativeURL(`/modelRegistry/modelregistry-sample/registeredModels/1/versions`); + verifyRelativeURL(`/model-registry/modelregistry-sample/registeredModels/1/versions`); modelRegistry.shouldmodelVersionsEmpty(); }); @@ -122,9 +122,9 @@ describe('Model Versions', () => { modelRegistry.visit(); const registeredModelRow = modelRegistry.getRow('Fraud detection model'); registeredModelRow.findName().contains('Fraud detection model').click(); - verifyRelativeURL(`/modelRegistry/modelregistry-sample/registeredModels/1/versions`); + verifyRelativeURL(`/model-registry/modelregistry-sample/registeredModels/1/versions`); cy.go('back'); - verifyRelativeURL(`/modelRegistry/modelregistry-sample`); + verifyRelativeURL(`/model-registry/modelregistry-sample`); registeredModelRow.findName().contains('Fraud detection model').should('exist'); }); @@ -143,7 +143,7 @@ describe('Model Versions', () => { //cy.reload(); const registeredModelRow = modelRegistry.getRow('Fraud detection model'); registeredModelRow.findName().contains('Fraud detection model').click(); - verifyRelativeURL(`/modelRegistry/modelregistry-sample/registeredModels/1/versions`); + verifyRelativeURL(`/model-registry/modelregistry-sample/registeredModels/1/versions`); modelRegistry.findModelBreadcrumbItem().contains('test'); //modelRegistry.findModelVersionsTableKebab().findDropdownItem('View archived versions'); //modelRegistry.findModelVersionsHeaderAction().findDropdownItem('Archive model'); @@ -209,10 +209,10 @@ describe('Model Versions', () => { registeredModelRow.findName().contains('Fraud detection model').click(); const modelVersionRow = modelRegistry.getModelVersionRow('model version'); modelVersionRow.findModelVersionName().contains('model version').click(); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/1/versions/1/details'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/1/versions/1/details'); cy.findByTestId('app-page-title').should('have.text', 'model version'); cy.findByTestId('breadcrumb-version-name').should('have.text', 'model version'); cy.go('back'); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/1/versions'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/1/versions'); }); }); diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/registeredModelArchive.cy.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/registeredModelArchive.cy.ts index d07c89a1e..6601e3290 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/registeredModelArchive.cy.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/registeredModelArchive.cy.ts @@ -133,28 +133,28 @@ describe('Model archive list', () => { registeredModels: [], }); registeredModelArchive.visit(); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/archive'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/archive'); registeredModelArchive.shouldArchiveVersionsEmpty(); }); it('Archived model details browser back button should lead to archived models table', () => { initIntercepts({}); registeredModelArchive.visit(); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/archive'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/archive'); registeredModelArchive.findArchiveModelBreadcrumbItem().contains('Archived models'); const archiveModelRow = registeredModelArchive.getRow('model 2'); archiveModelRow.findName().contains('model 2').click(); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/archive/2/versions'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/archive/2/versions'); cy.findByTestId('app-page-title').should('have.text', 'model 2Archived'); cy.go('back'); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/archive'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/archive'); registeredModelArchive.findArchiveModelTable().should('be.visible'); }); it('Archived model with no versions', () => { initIntercepts({ modelVersions: [] }); registeredModelArchive.visit(); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/archive'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/archive'); registeredModelArchive.findArchiveModelBreadcrumbItem().contains('Archived models'); const archiveModelRow = registeredModelArchive.getRow('model 2'); archiveModelRow.findName().contains('model 2').click(); @@ -164,23 +164,23 @@ describe('Model archive list', () => { it('Archived model flow', () => { initIntercepts({}); registeredModelArchive.visitArchiveModelVersionList(); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/archive/2/versions'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/archive/2/versions'); modelRegistry.findModelVersionsTable().should('be.visible'); modelRegistry.findModelVersionsTableRows().should('have.length', 2); const version = modelRegistry.getModelVersionRow('model version'); version.findModelVersionName().contains('model version').click(); verifyRelativeURL( - '/modelRegistry/modelregistry-sample/registeredModels/archive/2/versions/1/details', + '/model-registry/modelregistry-sample/registeredModels/archive/2/versions/1/details', ); cy.go('back'); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/archive/2/versions'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/archive/2/versions'); }); it('Archive models list', () => { initIntercepts({}); registeredModelArchive.visit(); - verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/archive'); + verifyRelativeURL('/model-registry/modelregistry-sample/registeredModels/archive'); //breadcrumb registeredModelArchive.findArchiveModelBreadcrumbItem().contains('Archived models'); @@ -233,7 +233,7 @@ describe('Model archive list', () => { archiveModelRow.findKebabAction('View details').click(); cy.location('pathname').should( 'be.equals', - '/modelRegistry/modelregistry-sample/registeredModels/archive/2/details', + '/model-registry/modelregistry-sample/registeredModels/archive/2/details', ); }); }); diff --git a/clients/ui/frontend/src/app/App.tsx b/clients/ui/frontend/src/app/App.tsx index ed0785a64..4c56853bd 100644 --- a/clients/ui/frontend/src/app/App.tsx +++ b/clients/ui/frontend/src/app/App.tsx @@ -13,7 +13,7 @@ import { } from '@patternfly/react-core'; import ToastNotifications from '~/shared/components/ToastNotifications'; import { useSettings } from '~/shared/hooks/useSettings'; -import { isMUITheme, Theme, AUTH_HEADER, MOCK_AUTH } from '~/shared/utilities/const'; +import { isMUITheme, Theme, AUTH_HEADER, MOCK_AUTH, isStandalone } from '~/shared/utilities/const'; import { logout } from '~/shared/utilities/appUtils'; import { NamespaceSelectorContext } from '~/shared/context/NamespaceSelectorContext'; import NavSidebar from './NavSidebar'; @@ -108,15 +108,19 @@ const App: React.FC = () => { { - logout().then(() => window.location.reload()); - }} - /> + isStandalone() ? ( + { + logout().then(() => window.location.reload()); + }} + /> + ) : ( + '' + ) } - isManagedSidebar - sidebar={} + isManagedSidebar={isStandalone()} + sidebar={isStandalone() ? : ''} > diff --git a/clients/ui/frontend/src/app/AppRoutes.tsx b/clients/ui/frontend/src/app/AppRoutes.tsx index 1f0f6461c..60a69d480 100644 --- a/clients/ui/frontend/src/app/AppRoutes.tsx +++ b/clients/ui/frontend/src/app/AppRoutes.tsx @@ -32,7 +32,7 @@ export const useAdminSettings = (): NavDataItem[] => { return [ { label: 'Settings', - children: [{ label: 'Model Registry', path: '/modelRegistrySettings' }], + children: [{ label: 'Model Registry', path: '/model-registry-settings' }], }, ]; }; @@ -40,7 +40,7 @@ export const useAdminSettings = (): NavDataItem[] => { export const useNavData = (): NavDataItem[] => [ { label: 'Model Registry', - path: '/modelRegistry', + path: '/model-registry', }, ...useAdminSettings(), ]; @@ -50,12 +50,12 @@ const AppRoutes: React.FC = () => { return ( - } /> - } /> + } /> + } /> } /> {/* TODO: [Conditional render] Follow up add testing and conditional rendering when in standalone mode*/} {clusterAdmin && ( - } /> + } /> )} ); diff --git a/clients/ui/frontend/src/app/context/ModelRegistryContext.tsx b/clients/ui/frontend/src/app/context/ModelRegistryContext.tsx index 28ce82907..3b8c8de2c 100644 --- a/clients/ui/frontend/src/app/context/ModelRegistryContext.tsx +++ b/clients/ui/frontend/src/app/context/ModelRegistryContext.tsx @@ -4,6 +4,7 @@ import useQueryParamNamespaces from '~/shared/hooks/useQueryParamNamespaces'; import useModelRegistryAPIState, { ModelRegistryAPIState, } from '~/app/hooks/useModelRegistryAPIState'; +import { URL_PREFIX } from '~/shared/utilities/const'; export type ModelRegistryContextType = { apiState: ModelRegistryAPIState; @@ -26,7 +27,7 @@ export const ModelRegistryContextProvider: React.FC { const hostPath = modelRegistryName - ? `/api/${BFF_API_VERSION}/model_registry/${modelRegistryName}` + ? `${URL_PREFIX}/api/${BFF_API_VERSION}/model_registry/${modelRegistryName}` : null; const queryParams = useQueryParamNamespaces(); diff --git a/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionDetails.tsx b/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionDetails.tsx index c59fecab8..61523b501 100644 --- a/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionDetails.tsx +++ b/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionDetails.tsx @@ -57,7 +57,7 @@ const ModelVersionsDetails: React.FC = ({ tab, ...page ( - Model registry - {preferredModelRegistry?.name} + Model registry - {preferredModelRegistry?.name} )} /> = ({ tab, ...pageProps }) => { ( - Model registry - {preferredModelRegistry?.name} + Model registry - {preferredModelRegistry?.name} )} /> diff --git a/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionsArchive/ArchiveModelVersionDetailsBreadcrumb.tsx b/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionsArchive/ArchiveModelVersionDetailsBreadcrumb.tsx index 8356589be..bce6f43d7 100644 --- a/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionsArchive/ArchiveModelVersionDetailsBreadcrumb.tsx +++ b/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionsArchive/ArchiveModelVersionDetailsBreadcrumb.tsx @@ -20,7 +20,7 @@ const ArchiveModelVersionDetailsBreadcrumb: React.FC ( Model registry - {preferredModelRegistry}} + render={() => Model registry - {preferredModelRegistry}} /> ( diff --git a/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionArchiveDetailsBreadcrumb.tsx b/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionArchiveDetailsBreadcrumb.tsx index c706fef05..065bf0053 100644 --- a/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionArchiveDetailsBreadcrumb.tsx +++ b/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionArchiveDetailsBreadcrumb.tsx @@ -20,7 +20,7 @@ const ModelVersionArchiveDetailsBreadcrumb: React.FC ( Model registry - {preferredModelRegistry}} + render={() => Model registry - {preferredModelRegistry}} /> ( diff --git a/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionsArchive.tsx b/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionsArchive.tsx index e456773e9..3d57fe309 100644 --- a/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionsArchive.tsx +++ b/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionsArchive.tsx @@ -29,7 +29,7 @@ const ModelVersionsArchive: React.FC = ({ ...pageProp ( - Model registry - {preferredModelRegistry?.name} + Model registry - {preferredModelRegistry?.name} )} /> = ({ preferredModelRegistry, registeredModel }) => ( Model registry - {preferredModelRegistry}} + render={() => Model registry - {preferredModelRegistry}} /> ( diff --git a/clients/ui/frontend/src/app/pages/modelRegistry/screens/RegisteredModelsArchive/RegisteredModelsArchive.tsx b/clients/ui/frontend/src/app/pages/modelRegistry/screens/RegisteredModelsArchive/RegisteredModelsArchive.tsx index 17630f394..0311e5cbb 100644 --- a/clients/ui/frontend/src/app/pages/modelRegistry/screens/RegisteredModelsArchive/RegisteredModelsArchive.tsx +++ b/clients/ui/frontend/src/app/pages/modelRegistry/screens/RegisteredModelsArchive/RegisteredModelsArchive.tsx @@ -23,7 +23,7 @@ const RegisteredModelsArchive: React.FC = ({ ...pa ( - Model registry - {preferredModelRegistry?.name} + Model registry - {preferredModelRegistry?.name} )} /> diff --git a/clients/ui/frontend/src/app/pages/modelRegistry/screens/routeUtils.ts b/clients/ui/frontend/src/app/pages/modelRegistry/screens/routeUtils.ts index 5ff99f3b0..30d531ec5 100644 --- a/clients/ui/frontend/src/app/pages/modelRegistry/screens/routeUtils.ts +++ b/clients/ui/frontend/src/app/pages/modelRegistry/screens/routeUtils.ts @@ -1,5 +1,5 @@ export const modelRegistryUrl = (preferredModelRegistry?: string): string => - `/modelRegistry/${preferredModelRegistry}`; + `/model-registry/${preferredModelRegistry}`; export const registeredModelsUrl = (preferredModelRegistry?: string): string => `${modelRegistryUrl(preferredModelRegistry)}/registeredModels`; diff --git a/clients/ui/frontend/src/index.html b/clients/ui/frontend/src/index.html index 8c5c8cb1e..319e66d3e 100644 --- a/clients/ui/frontend/src/index.html +++ b/clients/ui/frontend/src/index.html @@ -10,7 +10,7 @@ - + diff --git a/clients/ui/frontend/src/shared/api/k8s.ts b/clients/ui/frontend/src/shared/api/k8s.ts index 1687740f5..e182ff793 100644 --- a/clients/ui/frontend/src/shared/api/k8s.ts +++ b/clients/ui/frontend/src/shared/api/k8s.ts @@ -3,13 +3,14 @@ import { handleRestFailures } from '~/shared/api/errorUtils'; import { isModelRegistryResponse, restGET } from '~/shared/api/apiUtils'; import { ModelRegistry } from '~/app/types'; import { BFF_API_VERSION } from '~/app/const'; +import { URL_PREFIX } from '~/shared/utilities/const'; import { Namespace, UserSettings } from '~/shared/types'; export const getListModelRegistries = (hostPath: string, queryParams: Record = {}) => (opts: APIOptions): Promise => handleRestFailures( - restGET(hostPath, `/api/${BFF_API_VERSION}/model_registry`, queryParams, opts), + restGET(hostPath, `${URL_PREFIX}/api/${BFF_API_VERSION}/model_registry`, queryParams, opts), ).then((response) => { if (isModelRegistryResponse(response)) { return response.data; @@ -20,23 +21,23 @@ export const getListModelRegistries = export const getUser = (hostPath: string) => (opts: APIOptions): Promise => - handleRestFailures(restGET(hostPath, `/api/${BFF_API_VERSION}/user`, {}, opts)).then( - (response) => { - if (isModelRegistryResponse(response)) { - return response.data; - } - throw new Error('Invalid response format'); - }, - ); + handleRestFailures( + restGET(hostPath, `${URL_PREFIX}/api/${BFF_API_VERSION}/user`, {}, opts), + ).then((response) => { + if (isModelRegistryResponse(response)) { + return response.data; + } + throw new Error('Invalid response format'); + }); export const getNamespaces = (hostPath: string) => (opts: APIOptions): Promise => - handleRestFailures(restGET(hostPath, `/api/${BFF_API_VERSION}/namespaces`, {}, opts)).then( - (response) => { - if (isModelRegistryResponse(response)) { - return response.data; - } - throw new Error('Invalid response format'); - }, - ); + handleRestFailures( + restGET(hostPath, `${URL_PREFIX}/api/${BFF_API_VERSION}/namespaces`, {}, opts), + ).then((response) => { + if (isModelRegistryResponse(response)) { + return response.data; + } + throw new Error('Invalid response format'); + }); diff --git a/clients/ui/frontend/src/shared/hooks/useQueryParamNamespaces.ts b/clients/ui/frontend/src/shared/hooks/useQueryParamNamespaces.ts index e24ce3de6..c9bb8636c 100644 --- a/clients/ui/frontend/src/shared/hooks/useQueryParamNamespaces.ts +++ b/clients/ui/frontend/src/shared/hooks/useQueryParamNamespaces.ts @@ -1,12 +1,12 @@ import React from 'react'; import { NamespaceSelectorContext } from '~/shared/context/NamespaceSelectorContext'; import { isStandalone } from '~/shared/utilities/const'; -import { getNamespaceQueryParam } from '~/shared/api/apiUtils'; import { useDeepCompareMemoize } from '~/shared/utilities/useDeepCompareMemoize'; const useQueryParamNamespaces = (): Record => { const { preferredNamespace: namespaceSelector } = React.useContext(NamespaceSelectorContext); - const namespace = isStandalone() ? namespaceSelector?.name : getNamespaceQueryParam(); + // TODO: Readd GetNamespaceQueryParam once it's working + const namespace = isStandalone() ? namespaceSelector?.name : 'kubeflow'; return useDeepCompareMemoize({ namespace }); }; diff --git a/clients/ui/frontend/src/shared/utilities/const.ts b/clients/ui/frontend/src/shared/utilities/const.ts index eef4905f4..ddcdd7b9e 100644 --- a/clients/ui/frontend/src/shared/utilities/const.ts +++ b/clients/ui/frontend/src/shared/utilities/const.ts @@ -22,5 +22,15 @@ const AUTH_HEADER = process.env.AUTH_HEADER || 'kubeflow-userid'; const USERNAME = process.env.USERNAME || 'user@example.com'; const IMAGE_DIR = process.env.IMAGE_DIR || 'images'; const LOGO_LIGHT = process.env.LOGO || 'logo-light-theme.svg'; +const URL_PREFIX = DEPLOYMENT_MODE === DeploymentMode.Integrated ? '/model-registry' : ''; -export { POLL_INTERVAL, DEV_MODE, AUTH_HEADER, USERNAME, IMAGE_DIR, LOGO_LIGHT, MOCK_AUTH }; +export { + POLL_INTERVAL, + DEV_MODE, + AUTH_HEADER, + USERNAME, + IMAGE_DIR, + LOGO_LIGHT, + MOCK_AUTH, + URL_PREFIX, +}; diff --git a/clients/ui/manifests/base/README.md b/clients/ui/manifests/README.md similarity index 92% rename from clients/ui/manifests/base/README.md rename to clients/ui/manifests/README.md index 33b8e414e..559ec779d 100644 --- a/clients/ui/manifests/base/README.md +++ b/clients/ui/manifests/README.md @@ -79,3 +79,8 @@ Events: To fix this, you'll need to increase the amount of memory available to the VM. This can be done through either the Podman Desktop or Docker Desktop GUI. 6-8GB of memory is generally a sufficient amount to use. +## Running with Kubeflow and Istio +Alternatively, if you'd like to run the UI and BFF pods with an Istio configuration for the KF Central Dashboard, you can apply the manifests by running: +```shell +kubectl apply -k overlays/kubeflow -n kubeflow +``` \ No newline at end of file diff --git a/clients/ui/manifests/base/kustomization.yaml b/clients/ui/manifests/base/kustomization.yaml index 6ad609840..0ab619941 100644 --- a/clients/ui/manifests/base/kustomization.yaml +++ b/clients/ui/manifests/base/kustomization.yaml @@ -5,14 +5,14 @@ resources: - model-registry-bff-role.yaml - model-registry-bff-service.yaml - model-registry-bff-deployment.yaml +- model-registry-bff-service-account.yaml - model-registry-ui-service.yaml - model-registry-ui-deployment.yaml -- model-registry-service-account.yaml images: -- name: model-registry-bff-image - newName: kubeflow/model-registry-bff - newTag: latest - name: model-registry-ui-image - newName: kubeflow/model-registry-ui + newName: docker.io/kubeflow/model-registry-ui + newTag: latest +- name: model-registry-bff-image + newName: docker.io/kubeflow/model-registry-bff newTag: latest diff --git a/clients/ui/manifests/base/model-registry-service-account.yaml b/clients/ui/manifests/base/model-registry-bff-service-account.yaml similarity index 59% rename from clients/ui/manifests/base/model-registry-service-account.yaml rename to clients/ui/manifests/base/model-registry-bff-service-account.yaml index 86cbfc9b6..5cbafe203 100644 --- a/clients/ui/manifests/base/model-registry-service-account.yaml +++ b/clients/ui/manifests/base/model-registry-bff-service-account.yaml @@ -1,4 +1,5 @@ +--- kind: ServiceAccount apiVersion: v1 metadata: - name: model-registry-bff \ No newline at end of file + name: model-registry-bff diff --git a/clients/ui/manifests/base/model-registry-bff-service.yaml b/clients/ui/manifests/base/model-registry-bff-service.yaml index 20c1e0df1..929022cdb 100644 --- a/clients/ui/manifests/base/model-registry-bff-service.yaml +++ b/clients/ui/manifests/base/model-registry-bff-service.yaml @@ -2,10 +2,14 @@ apiVersion: v1 kind: Service metadata: name: model-registry-bff-service + labels: + app: model-registry-bff + run: model-registry-bff spec: selector: app: model-registry-bff ports: - protocol: TCP port: 4000 - targetPort: 4000 \ No newline at end of file + targetPort: 4000 + name: http \ No newline at end of file diff --git a/clients/ui/manifests/base/model-registry-ui-deployment.yaml b/clients/ui/manifests/base/model-registry-ui-deployment.yaml index 23c55eb07..0ef0cd6c2 100644 --- a/clients/ui/manifests/base/model-registry-ui-deployment.yaml +++ b/clients/ui/manifests/base/model-registry-ui-deployment.yaml @@ -13,7 +13,7 @@ spec: metadata: labels: app: model-registry-ui - spec: + spec: containers: - name: model-registry-ui image: model-registry-ui-image diff --git a/clients/ui/manifests/base/model-registry-ui-service.yaml b/clients/ui/manifests/base/model-registry-ui-service.yaml index 10211cd1f..c907d5918 100644 --- a/clients/ui/manifests/base/model-registry-ui-service.yaml +++ b/clients/ui/manifests/base/model-registry-ui-service.yaml @@ -2,11 +2,15 @@ apiVersion: v1 kind: Service metadata: name: model-registry-ui-service + labels: + app: model-registry-ui + run: model-registry-ui spec: selector: app: model-registry-ui ports: - - protocol: TCP + - name: http port: 8080 + protocol: TCP targetPort: 8080 - name: http + type: ClusterIP diff --git a/clients/ui/manifests/kubeflow/kubeflow-dashboard-rbac.yaml b/clients/ui/manifests/kubeflow/kubeflow-dashboard-rbac.yaml new file mode 100644 index 000000000..0b5896d6d --- /dev/null +++ b/clients/ui/manifests/kubeflow/kubeflow-dashboard-rbac.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: model-registry-retrieve-clusterrolebindings +rules: +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + verbs: + - get + - list +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: model-registry-retrieve-clusterrolebindings-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: model-registry-retrieve-clusterrolebindings +subjects: +- kind: ServiceAccount + name: model-registry-bff + namespace: kubeflow +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: model-registry-create-sars +rules: +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: model-registry-create-sars-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: model-registry-create-sars +subjects: +- kind: ServiceAccount + name: model-registry-bff + namespace: kubeflow diff --git a/clients/ui/manifests/kubeflow/kustomization.yaml b/clients/ui/manifests/kubeflow/kustomization.yaml index 418e76423..64ebb3d52 100644 --- a/clients/ui/manifests/kubeflow/kustomization.yaml +++ b/clients/ui/manifests/kubeflow/kustomization.yaml @@ -1,6 +1,10 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization +resources: + - ../overlays/istio + - kubeflow-dashboard-rbac.yaml + patchesJson6902: - path: model-registry-ui-deployment.yaml target: @@ -8,7 +12,7 @@ patchesJson6902: version: v1 kind: Deployment name: model-registry-ui-deployment - - path: deployment.yaml + - path: model-registry-bff-deployment.yaml target: group: apps version: v1 diff --git a/clients/ui/manifests/kubeflow/model-registry-ui-deployment.yaml b/clients/ui/manifests/kubeflow/model-registry-ui-deployment.yaml index 1959e4bbb..ee036eea0 100644 --- a/clients/ui/manifests/kubeflow/model-registry-ui-deployment.yaml +++ b/clients/ui/manifests/kubeflow/model-registry-ui-deployment.yaml @@ -2,7 +2,7 @@ path: /spec/template/spec/containers/0/env value: - name: API_URL - value: "http://model-registry-bff-service:4000" + value: "http://model-registry-bff-service.kubeflow.svc.cluster.local:4000" - name: MOCK_AUTH value: "false" - name: DEPLOYMENT_MODE diff --git a/clients/ui/manifests/overlays/istio/authorization-policy-bff.yaml b/clients/ui/manifests/overlays/istio/authorization-policy-bff.yaml new file mode 100644 index 000000000..eed8522dd --- /dev/null +++ b/clients/ui/manifests/overlays/istio/authorization-policy-bff.yaml @@ -0,0 +1,13 @@ +apiVersion: security.istio.io/v1beta1 +kind: AuthorizationPolicy +metadata: + name: model-registry-bff + labels: + app: model-registry-bff +spec: + action: ALLOW + rules: + - {} + selector: + matchLabels: + app: model-registry-bff diff --git a/clients/ui/manifests/overlays/istio/authorization-policy-ui.yaml b/clients/ui/manifests/overlays/istio/authorization-policy-ui.yaml new file mode 100644 index 000000000..60dd145d9 --- /dev/null +++ b/clients/ui/manifests/overlays/istio/authorization-policy-ui.yaml @@ -0,0 +1,16 @@ +apiVersion: security.istio.io/v1beta1 +kind: AuthorizationPolicy +metadata: + name: model-registry-ui + labels: + app: model-registry-ui +spec: + action: ALLOW + rules: + - from: + - source: + principals: + - cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account + selector: + matchLabels: + app: model-registry-ui diff --git a/clients/ui/manifests/overlays/istio/destination-rule-bff.yaml b/clients/ui/manifests/overlays/istio/destination-rule-bff.yaml new file mode 100644 index 000000000..e149dc133 --- /dev/null +++ b/clients/ui/manifests/overlays/istio/destination-rule-bff.yaml @@ -0,0 +1,11 @@ +apiVersion: networking.istio.io/v1alpha3 +kind: DestinationRule +metadata: + name: model-registry-bff + labels: + app: model-registry-bff +spec: + host: model-registry-bff-service.kubeflow.svc.cluster.local + trafficPolicy: + tls: + mode: ISTIO_MUTUAL diff --git a/clients/ui/manifests/overlays/istio/destination-rule-ui.yaml b/clients/ui/manifests/overlays/istio/destination-rule-ui.yaml new file mode 100644 index 000000000..330366fc2 --- /dev/null +++ b/clients/ui/manifests/overlays/istio/destination-rule-ui.yaml @@ -0,0 +1,11 @@ +apiVersion: networking.istio.io/v1alpha3 +kind: DestinationRule +metadata: + name: model-registry-ui + labels: + app: model-registry-ui +spec: + host: model-registry-ui-service.kubeflow.svc.cluster.local + trafficPolicy: + tls: + mode: ISTIO_MUTUAL diff --git a/clients/ui/manifests/overlays/istio/kustomization.yaml b/clients/ui/manifests/overlays/istio/kustomization.yaml new file mode 100644 index 000000000..d40a96bdd --- /dev/null +++ b/clients/ui/manifests/overlays/istio/kustomization.yaml @@ -0,0 +1,17 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../../base +- virtual-service.yaml +- destination-rule-ui.yaml +- destination-rule-bff.yaml +- authorization-policy-ui.yaml +- authorization-policy-bff.yaml + +patchesJson6902: + - path: model-registry-ui-service.yaml + target: + version: v1 + kind: Service + name: model-registry-ui-service \ No newline at end of file diff --git a/clients/ui/manifests/overlays/istio/model-registry-ui-service.yaml b/clients/ui/manifests/overlays/istio/model-registry-ui-service.yaml new file mode 100644 index 000000000..9bcead7b8 --- /dev/null +++ b/clients/ui/manifests/overlays/istio/model-registry-ui-service.yaml @@ -0,0 +1,3 @@ +- op: replace + path: /spec/ports/0/port + value: 80 diff --git a/clients/ui/manifests/overlays/istio/virtual-service.yaml b/clients/ui/manifests/overlays/istio/virtual-service.yaml new file mode 100644 index 000000000..109160025 --- /dev/null +++ b/clients/ui/manifests/overlays/istio/virtual-service.yaml @@ -0,0 +1,26 @@ +apiVersion: networking.istio.io/v1alpha3 +kind: VirtualService +metadata: + name: model-registry-ui + labels: + app: model-registry-ui +spec: + gateways: + - kubeflow-gateway + hosts: + - '*' + http: + - headers: + request: + add: + x-forwarded-prefix: /model-registry + match: + - uri: + prefix: /model-registry/ + rewrite: + uri: / + route: + - destination: + host: model-registry-ui-service.kubeflow.svc.cluster.local + port: + number: 80 diff --git a/clients/ui/manifests/standalone/kustomization.yaml b/clients/ui/manifests/standalone/kustomization.yaml index fda80e7db..f60c1fa46 100644 --- a/clients/ui/manifests/standalone/kustomization.yaml +++ b/clients/ui/manifests/standalone/kustomization.yaml @@ -11,7 +11,7 @@ patchesJson6902: version: v1 kind: Deployment name: model-registry-bff-deployment - - path: deployment.yaml + - path: model-registry-bff-deployment.yaml target: group: apps version: v1 From b8dbf7bf1fc69908be03921424327a4425229eb7 Mon Sep 17 00:00:00 2001 From: Lucas Fernandez Date: Mon, 13 Jan 2025 14:09:08 +0100 Subject: [PATCH 03/10] Fix namespace selector (#692) Signed-off-by: lucferbux --- clients/ui/frontend/.env.cypress.mock | 2 + clients/ui/frontend/src/app/App.tsx | 5 +- clients/ui/frontend/src/index.tsx | 9 ++- .../ui/frontend/src/shared/api/apiUtils.ts | 5 -- .../shared/context/DashboardScriptLoader.tsx | 57 +++++++++++++++++++ .../context/NamespaceSelectorContext.tsx | 26 +++++++++ .../shared/hooks/useQueryParamNamespaces.ts | 4 +- 7 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 clients/ui/frontend/src/shared/context/DashboardScriptLoader.tsx diff --git a/clients/ui/frontend/.env.cypress.mock b/clients/ui/frontend/.env.cypress.mock index 60f8faee6..f645d56d1 100644 --- a/clients/ui/frontend/.env.cypress.mock +++ b/clients/ui/frontend/.env.cypress.mock @@ -1,2 +1,4 @@ # Test against prod build hosted by lightweight http server BASE_URL=http://localhost:9001 +MOCK_AUTH=true +DEPLOYMENT_MODE=standalone \ No newline at end of file diff --git a/clients/ui/frontend/src/app/App.tsx b/clients/ui/frontend/src/app/App.tsx index 4c56853bd..9dd18f386 100644 --- a/clients/ui/frontend/src/app/App.tsx +++ b/clients/ui/frontend/src/app/App.tsx @@ -74,11 +74,12 @@ const App: React.FC = () => {

+ {String(error)} {configError?.message || namespacesLoadError?.message || - 'Unknown error occurred during startup.'} + 'Unknown error occurred during startup!!!!!!'}

-

Logging out and logging back in may solve the issue.

+

Logging out and logging back in may solve the issue!!!!!c.

diff --git a/clients/ui/frontend/src/index.tsx b/clients/ui/frontend/src/index.tsx index 69cb07279..5a3c1e658 100644 --- a/clients/ui/frontend/src/index.tsx +++ b/clients/ui/frontend/src/index.tsx @@ -6,6 +6,7 @@ import App from './app/App'; import { BrowserStorageContextProvider } from './shared/components/browserStorage/BrowserStorageContext'; import { NotificationContextProvider } from './app/context/NotificationContext'; import { NamespaceSelectorContextProvider } from './shared/context/NamespaceSelectorContext'; +import DashboardScriptLoader from './shared/context/DashboardScriptLoader'; const theme = createTheme({ cssVariables: true }); const root = ReactDOM.createRoot(document.getElementById('root')!); @@ -16,9 +17,11 @@ root.render( - - - + + + + + diff --git a/clients/ui/frontend/src/shared/api/apiUtils.ts b/clients/ui/frontend/src/shared/api/apiUtils.ts index 19d831ff4..c61ff3f57 100644 --- a/clients/ui/frontend/src/shared/api/apiUtils.ts +++ b/clients/ui/frontend/src/shared/api/apiUtils.ts @@ -189,8 +189,3 @@ export const isModelRegistryResponse = (response: unknown): response is Model export const assembleModelRegistryBody = (data: T): ModelRegistryBody => ({ data, }); - -export const getNamespaceQueryParam = (): string | null => { - const params = new URLSearchParams(window.location.search); - return params.get('ns'); -}; diff --git a/clients/ui/frontend/src/shared/context/DashboardScriptLoader.tsx b/clients/ui/frontend/src/shared/context/DashboardScriptLoader.tsx new file mode 100644 index 000000000..0acd3d570 --- /dev/null +++ b/clients/ui/frontend/src/shared/context/DashboardScriptLoader.tsx @@ -0,0 +1,57 @@ +import React, { useEffect, useState } from 'react'; +import { Bullseye, Spinner } from '@patternfly/react-core'; +import { isIntegrated } from '~/shared/utilities/const'; + +type DashboardScriptLoaderProps = { + children: React.ReactNode; +}; + +const loadScript = (src: string, onLoad: () => void, onError: () => void) => { + const script = document.createElement('script'); + script.src = src; + script.async = true; + script.onload = onLoad; + script.onerror = onError; + document.head.appendChild(script); +}; + +/* eslint-disable no-console */ +const DashboardScriptLoader: React.FC = ({ children }) => { + const [scriptLoaded, setScriptLoaded] = useState(false); + + useEffect(() => { + const scriptUrl = '/dashboard_lib.bundle.js'; + + if (!isIntegrated()) { + console.warn( + 'DashboardScriptLoader: Script not loaded because deployment mode is not integrated', + ); + setScriptLoaded(true); + return; + } + + fetch(scriptUrl, { method: 'HEAD' }) + .then((response) => { + if (response.ok) { + loadScript( + scriptUrl, + () => setScriptLoaded(true), + () => console.error('Failed to load the script'), + ); + } else { + console.warn('Script not found'); + } + }) + .catch((error) => console.error('Error checking script existence', error)); + }, []); + + return !scriptLoaded ? ( + + + + ) : ( + <>{children} + ); +}; + +export default DashboardScriptLoader; diff --git a/clients/ui/frontend/src/shared/context/NamespaceSelectorContext.tsx b/clients/ui/frontend/src/shared/context/NamespaceSelectorContext.tsx index 372e93725..610052a36 100644 --- a/clients/ui/frontend/src/shared/context/NamespaceSelectorContext.tsx +++ b/clients/ui/frontend/src/shared/context/NamespaceSelectorContext.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import useNamespaces from '~/shared/hooks/useNamespaces'; import { Namespace } from '~/shared/types'; +import { isIntegrated } from '~/shared/utilities/const'; export type NamespaceSelectorContextType = { namespacesLoaded: boolean; @@ -31,6 +32,13 @@ export const NamespaceSelectorContextProvider: React.FC ); +declare global { + interface Window { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + centraldashboard: any; + } +} + const EnabledNamespaceSelectorContextProvider: React.FC = ({ children, }) => { @@ -40,6 +48,24 @@ const EnabledNamespaceSelectorContextProvider: React.FC 0 ? namespaces[0] : null; + React.useEffect(() => { + if (isIntegrated()) { + // Initialize the central dashboard client + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + window.centraldashboard.CentralDashboardEventHandler.init((cdeh: any) => { + // eslint-disable-next-line no-param-reassign + cdeh.onNamespaceSelected = (newNamespace: string) => { + setPreferredNamespace({ name: newNamespace }); + }; + }); + } catch (err) { + /* eslint-disable no-console */ + console.error('Failed to initialize central dashboard client', err); + } + } + }, []); + const contextValue = React.useMemo( () => ({ namespacesLoaded: isLoaded, diff --git a/clients/ui/frontend/src/shared/hooks/useQueryParamNamespaces.ts b/clients/ui/frontend/src/shared/hooks/useQueryParamNamespaces.ts index c9bb8636c..23f4900dc 100644 --- a/clients/ui/frontend/src/shared/hooks/useQueryParamNamespaces.ts +++ b/clients/ui/frontend/src/shared/hooks/useQueryParamNamespaces.ts @@ -1,12 +1,10 @@ import React from 'react'; import { NamespaceSelectorContext } from '~/shared/context/NamespaceSelectorContext'; -import { isStandalone } from '~/shared/utilities/const'; import { useDeepCompareMemoize } from '~/shared/utilities/useDeepCompareMemoize'; const useQueryParamNamespaces = (): Record => { const { preferredNamespace: namespaceSelector } = React.useContext(NamespaceSelectorContext); - // TODO: Readd GetNamespaceQueryParam once it's working - const namespace = isStandalone() ? namespaceSelector?.name : 'kubeflow'; + const namespace = namespaceSelector?.name; return useDeepCompareMemoize({ namespace }); }; From 4a15cace9d88ba5a9edb67bbbfc1ed1a3459302b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:22:08 +0000 Subject: [PATCH 04/10] build(deps-dev): bump cypress from 13.16.0 to 13.17.0 in /clients/ui/frontend (#696) Bumps [cypress](https://github.com/cypress-io/cypress) from 13.16.0 to 13.17.0. - [Release notes](https://github.com/cypress-io/cypress/releases) - [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md) - [Commits](https://github.com/cypress-io/cypress/compare/v13.16.0...v13.17.0) --- updated-dependencies: - dependency-name: cypress dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- clients/ui/frontend/package-lock.json | 9 ++++----- clients/ui/frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/clients/ui/frontend/package-lock.json b/clients/ui/frontend/package-lock.json index df35093f1..533e3b2c1 100644 --- a/clients/ui/frontend/package-lock.json +++ b/clients/ui/frontend/package-lock.json @@ -54,7 +54,7 @@ "core-js": "^3.37.1", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", - "cypress": "^13.16.0", + "cypress": "^13.17.0", "cypress-axe": "^1.5.0", "cypress-high-resolution": "^1.0.0", "cypress-mochawesome-reporter": "^3.8.2", @@ -7776,12 +7776,11 @@ "license": "MIT" }, "node_modules/cypress": { - "version": "13.16.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.16.0.tgz", - "integrity": "sha512-g6XcwqnvzXrqiBQR/5gN+QsyRmKRhls1y5E42fyOvsmU7JuY+wM6uHJWj4ZPttjabzbnRvxcik2WemR8+xT6FA==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", + "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", "dev": true, "hasInstallScript": true, - "license": "MIT", "dependencies": { "@cypress/request": "^3.0.6", "@cypress/xvfb": "^1.2.4", diff --git a/clients/ui/frontend/package.json b/clients/ui/frontend/package.json index 56550f229..c5b1f9bc9 100644 --- a/clients/ui/frontend/package.json +++ b/clients/ui/frontend/package.json @@ -57,7 +57,7 @@ "core-js": "^3.37.1", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", - "cypress": "^13.16.0", + "cypress": "^13.17.0", "cypress-axe": "^1.5.0", "cypress-high-resolution": "^1.0.0", "cypress-mochawesome-reporter": "^3.8.2", From eb21a6da17570ec0a28713b5673f7172aef143f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:23:08 +0000 Subject: [PATCH 05/10] build(deps-dev): bump core-js from 3.39.0 to 3.40.0 in /clients/ui/frontend (#695) Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.39.0 to 3.40.0. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/commits/v3.40.0/packages/core-js) --- updated-dependencies: - dependency-name: core-js dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- clients/ui/frontend/package-lock.json | 9 ++++----- clients/ui/frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/clients/ui/frontend/package-lock.json b/clients/ui/frontend/package-lock.json index 533e3b2c1..99e3eb0bc 100644 --- a/clients/ui/frontend/package-lock.json +++ b/clients/ui/frontend/package-lock.json @@ -51,7 +51,7 @@ "@types/showdown": "^2.0.3", "chai-subset": "^1.6.0", "copy-webpack-plugin": "^12.0.2", - "core-js": "^3.37.1", + "core-js": "^3.40.0", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", "cypress": "^13.17.0", @@ -7360,12 +7360,11 @@ } }, "node_modules/core-js": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", - "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", + "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==", "dev": true, "hasInstallScript": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" diff --git a/clients/ui/frontend/package.json b/clients/ui/frontend/package.json index c5b1f9bc9..bc5bd2e1c 100644 --- a/clients/ui/frontend/package.json +++ b/clients/ui/frontend/package.json @@ -54,7 +54,7 @@ "@types/showdown": "^2.0.3", "chai-subset": "^1.6.0", "copy-webpack-plugin": "^12.0.2", - "core-js": "^3.37.1", + "core-js": "^3.40.0", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", "cypress": "^13.17.0", From 4cedb36ac0c7958b841949a4f9261a79edb1802d Mon Sep 17 00:00:00 2001 From: Eder Ignatowicz Date: Mon, 13 Jan 2025 11:25:08 -0500 Subject: [PATCH 06/10] feat(bff): serving static assets on bff (#690) * feat(bff): serving static assets on bff Signed-off-by: Eder Ignatowicz * Update clients/ui/bff/Dockerfile Co-authored-by: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> Signed-off-by: Eder Ignatowicz --------- Signed-off-by: Eder Ignatowicz Co-authored-by: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> --- clients/ui/bff/Dockerfile | 5 +- clients/ui/bff/Makefile | 4 +- clients/ui/bff/cmd/main.go | 1 + clients/ui/bff/internal/api/app.go | 56 ++++++++------ clients/ui/bff/internal/api/app_test.go | 73 +++++++++++++++++++ clients/ui/bff/internal/api/middleware.go | 6 ++ clients/ui/bff/internal/api/test_utils.go | 30 ++++++++ clients/ui/bff/internal/config/environment.go | 13 ++-- clients/ui/bff/static/index.html | 10 +++ clients/ui/bff/static/sub/test.html | 10 +++ 10 files changed, 176 insertions(+), 32 deletions(-) create mode 100644 clients/ui/bff/internal/api/app_test.go create mode 100644 clients/ui/bff/static/index.html create mode 100644 clients/ui/bff/static/sub/test.html diff --git a/clients/ui/bff/Dockerfile b/clients/ui/bff/Dockerfile index c06ebe761..b8625294c 100644 --- a/clients/ui/bff/Dockerfile +++ b/clients/ui/bff/Dockerfile @@ -15,7 +15,8 @@ RUN go mod download COPY cmd/main.go cmd/main.go COPY internal/ internal/ - +# Copy the static assets +COPY $STATIC_ASSETS_DIR static/ # Build the Go application RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o bff ./cmd/main.go @@ -29,4 +30,4 @@ USER 65532:65532 # Expose port 4000 EXPOSE 4000 -ENTRYPOINT ["/bff"] +ENTRYPOINT ["/bff", "--static-assets-dir=/static"] diff --git a/clients/ui/bff/Makefile b/clients/ui/bff/Makefile index 987077181..4d8660bab 100644 --- a/clients/ui/bff/Makefile +++ b/clients/ui/bff/Makefile @@ -6,6 +6,8 @@ MOCK_MR_CLIENT ?= false DEV_MODE ?= false DEV_MODE_PORT ?= 8080 STANDALONE_MODE ?= true +#frontend static assets root directory +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 @@ -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) --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) .PHONY: docker-build docker-build: ## Builds a container for the project. diff --git a/clients/ui/bff/cmd/main.go b/clients/ui/bff/cmd/main.go index 5ed0979d4..e767515da 100644 --- a/clients/ui/bff/cmd/main.go +++ b/clients/ui/bff/cmd/main.go @@ -25,6 +25,7 @@ func main() { flag.BoolVar(&cfg.DevMode, "dev-mode", false, "Use development mode for access to local K8s cluster") 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.Parse() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) diff --git a/clients/ui/bff/internal/api/app.go b/clients/ui/bff/internal/api/app.go index c144c232e..bc8fe0fa0 100644 --- a/clients/ui/bff/internal/api/app.go +++ b/clients/ui/bff/internal/api/app.go @@ -3,12 +3,11 @@ package api import ( "context" "fmt" - "log/slog" - "net/http" - "github.com/kubeflow/model-registry/ui/bff/internal/config" "github.com/kubeflow/model-registry/ui/bff/internal/integrations" "github.com/kubeflow/model-registry/ui/bff/internal/repositories" + "log/slog" + "net/http" "github.com/julienschmidt/httprouter" "github.com/kubeflow/model-registry/ui/bff/internal/mocks" @@ -86,34 +85,45 @@ func (app *App) Shutdown(ctx context.Context, logger *slog.Logger) error { } func (app *App) Routes() http.Handler { - router := httprouter.New() + // Router for /api/v1/* + apiRouter := httprouter.New() - router.NotFound = http.HandlerFunc(app.notFoundResponse) - router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse) + apiRouter.NotFound = http.HandlerFunc(app.notFoundResponse) + apiRouter.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse) // HTTP client routes (requests that we forward to Model Registry API) // on those, we perform SAR on Specific Service on a given namespace - router.GET(HealthCheckPath, app.HealthcheckHandler) - router.GET(RegisteredModelListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetAllRegisteredModelsHandler)))) - router.GET(RegisteredModelPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetRegisteredModelHandler)))) - router.POST(RegisteredModelListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.CreateRegisteredModelHandler)))) - router.PATCH(RegisteredModelPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.UpdateRegisteredModelHandler)))) - router.GET(RegisteredModelVersionsPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetAllModelVersionsForRegisteredModelHandler)))) - router.POST(RegisteredModelVersionsPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.CreateModelVersionForRegisteredModelHandler)))) - router.GET(ModelVersionPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient((app.GetModelVersionHandler))))) - router.POST(ModelVersionListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.CreateModelVersionHandler)))) - router.PATCH(ModelVersionPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.UpdateModelVersionHandler)))) - router.GET(ModelVersionArtifactListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetAllModelArtifactsByModelVersionHandler)))) - router.POST(ModelVersionArtifactListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.CreateModelArtifactByModelVersionHandler)))) - router.PATCH(ModelRegistryPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.UpdateModelVersionHandler)))) + apiRouter.GET(HealthCheckPath, app.HealthcheckHandler) + apiRouter.GET(RegisteredModelListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetAllRegisteredModelsHandler)))) + apiRouter.GET(RegisteredModelPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetRegisteredModelHandler)))) + apiRouter.POST(RegisteredModelListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.CreateRegisteredModelHandler)))) + apiRouter.PATCH(RegisteredModelPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.UpdateRegisteredModelHandler)))) + apiRouter.GET(RegisteredModelVersionsPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetAllModelVersionsForRegisteredModelHandler)))) + apiRouter.POST(RegisteredModelVersionsPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.CreateModelVersionForRegisteredModelHandler)))) + apiRouter.GET(ModelVersionPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient((app.GetModelVersionHandler))))) + apiRouter.POST(ModelVersionListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.CreateModelVersionHandler)))) + apiRouter.PATCH(ModelVersionPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.UpdateModelVersionHandler)))) + apiRouter.GET(ModelVersionArtifactListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetAllModelArtifactsByModelVersionHandler)))) + apiRouter.POST(ModelVersionArtifactListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.CreateModelArtifactByModelVersionHandler)))) + apiRouter.PATCH(ModelRegistryPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.UpdateModelVersionHandler)))) // Kubernetes routes - router.GET(UserPath, app.UserHandler) + apiRouter.GET(UserPath, app.UserHandler) // Perform SAR to Get List Services by Namspace - router.GET(ModelRegistryListPath, app.AttachNamespace(app.PerformSARonGetListServicesByNamespace(app.ModelRegistryHandler))) + apiRouter.GET(ModelRegistryListPath, app.AttachNamespace(app.PerformSARonGetListServicesByNamespace(app.ModelRegistryHandler))) if app.config.StandaloneMode { - router.GET(NamespaceListPath, app.GetNamespacesHandler) + apiRouter.GET(NamespaceListPath, app.GetNamespacesHandler) } - return app.RecoverPanic(app.enableCORS(app.InjectUserHeaders(router))) + // App Router + appMux := http.NewServeMux() + // handler for api calls + appMux.Handle("/api/v1/", apiRouter) + + // file server for the frontend + staticAccessDir := http.Dir(app.config.StaticAssetsDir) + fileServer := http.FileServer(staticAccessDir) + appMux.Handle("/", fileServer) + + return app.RecoverPanic(app.enableCORS(app.InjectUserHeaders(appMux))) } diff --git a/clients/ui/bff/internal/api/app_test.go b/clients/ui/bff/internal/api/app_test.go new file mode 100644 index 000000000..e7cd23f81 --- /dev/null +++ b/clients/ui/bff/internal/api/app_test.go @@ -0,0 +1,73 @@ +package api + +import ( + "github.com/kubeflow/model-registry/ui/bff/internal/config" + "github.com/kubeflow/model-registry/ui/bff/internal/repositories" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "io" + "net/http" + httptest "net/http/httptest" +) + +var _ = Describe("Static File serving Test", func() { + var ( + server *httptest.Server + client *http.Client + ) + + Context("serving static files at /", Ordered, func() { + + BeforeAll(func() { + envConfig := config.EnvConfig{ + StaticAssetsDir: resolveStaticAssetsDirOnTests(), + } + app := &App{ + kubernetesClient: k8sClient, + repositories: repositories.NewRepositories(mockMRClient), + logger: logger, + config: envConfig, + } + + server = httptest.NewServer(app.Routes()) + client = server.Client() + }) + + AfterAll(func() { + server.Close() + }) + + It("should serve index.html from the root path", func() { + resp, err := client.Get(server.URL + "/") + Expect(err).NotTo(HaveOccurred()) + defer resp.Body.Close() + + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + + body, err := io.ReadAll(resp.Body) + Expect(err).NotTo(HaveOccurred()) + Expect(string(body)).To(ContainSubstring("BFF Stub Page")) + }) + + It("should serve subfolders from the root path", func() { + resp, err := client.Get(server.URL + "/sub/test.html") + Expect(err).NotTo(HaveOccurred()) + defer resp.Body.Close() + + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + + body, err := io.ReadAll(resp.Body) + Expect(err).NotTo(HaveOccurred()) + Expect(string(body)).To(ContainSubstring("BFF Stub Subfolder Page")) + }) + + It("should return 404 for a non-existent static file", func() { + resp, err := client.Get(server.URL + "/non-existent.html") + Expect(err).NotTo(HaveOccurred()) + defer resp.Body.Close() + + Expect(resp.StatusCode).To(Equal(http.StatusNotFound)) + }) + + }) +}) diff --git a/clients/ui/bff/internal/api/middleware.go b/clients/ui/bff/internal/api/middleware.go index 00a41b665..d70fe9acb 100644 --- a/clients/ui/bff/internal/api/middleware.go +++ b/clients/ui/bff/internal/api/middleware.go @@ -43,6 +43,12 @@ func (app *App) RecoverPanic(next http.Handler) http.Handler { func (app *App) InjectUserHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + //skip use headers check if we are not on /api/v1 + if !strings.HasPrefix(r.URL.Path, PathPrefix) { + next.ServeHTTP(w, r) + return + } + userIdHeader := r.Header.Get(KubeflowUserIDHeader) userGroupsHeader := r.Header.Get(KubeflowUserGroupsIdHeader) //`kubeflow-userid`: Contains the user's email address. diff --git a/clients/ui/bff/internal/api/test_utils.go b/clients/ui/bff/internal/api/test_utils.go index 945e7d0e4..952a03337 100644 --- a/clients/ui/bff/internal/api/test_utils.go +++ b/clients/ui/bff/internal/api/test_utils.go @@ -10,6 +10,8 @@ import ( "io" "net/http" "net/http/httptest" + "os" + "path/filepath" ) func setupApiTest[T any](method string, url string, body interface{}, k8sClient k8s.KubernetesClientInterface, kubeflowUserIDHeaderValue string, namespace string) (T, *http.Response, error) { @@ -79,3 +81,31 @@ func setupApiTest[T any](method string, url string, body interface{}, k8sClient return entity, rs, nil } + +func resolveStaticAssetsDirOnTests() string { + // Fall back to finding project root for testing + projectRoot, err := findProjectRootOnTests() + if err != nil { + panic("Failed to find project root: ") + } + + return filepath.Join(projectRoot, "static") +} + +// on tests findProjectRoot searches for the project root by locating go.mod +func findProjectRootOnTests() (string, error) { + currentDir, err := os.Getwd() + if err != nil { + return "", err + } + + // Traverse up until go.mod is found + for currentDir != "/" { + if _, err := os.Stat(filepath.Join(currentDir, "go.mod")); err == nil { + return currentDir, nil + } + currentDir = filepath.Dir(currentDir) + } + + return "", os.ErrNotExist +} diff --git a/clients/ui/bff/internal/config/environment.go b/clients/ui/bff/internal/config/environment.go index 7b905e121..6017701c8 100644 --- a/clients/ui/bff/internal/config/environment.go +++ b/clients/ui/bff/internal/config/environment.go @@ -1,10 +1,11 @@ package config type EnvConfig struct { - Port int - MockK8Client bool - MockMRClient bool - DevMode bool - StandaloneMode bool - DevModePort int + Port int + MockK8Client bool + MockMRClient bool + DevMode bool + StandaloneMode bool + DevModePort int + StaticAssetsDir string } diff --git a/clients/ui/bff/static/index.html b/clients/ui/bff/static/index.html new file mode 100644 index 000000000..41c73cf02 --- /dev/null +++ b/clients/ui/bff/static/index.html @@ -0,0 +1,10 @@ + + + + BFF Stub Page + + +

Welcome to the BFF Stub Page

+

This is a placeholder page for the serving frontend.

+ + diff --git a/clients/ui/bff/static/sub/test.html b/clients/ui/bff/static/sub/test.html new file mode 100644 index 000000000..a28fa30f2 --- /dev/null +++ b/clients/ui/bff/static/sub/test.html @@ -0,0 +1,10 @@ + + + + BFF Stub Subfolder Page + + +

Welcome to the BFF Stub Subfolder Page

+

This is a placeholder page for the serving frontend.

+ + From 6a052c12998189d39d25debd0a34ddc40b608ead Mon Sep 17 00:00:00 2001 From: Lucas Fernandez Date: Thu, 16 Jan 2025 17:00:55 +0100 Subject: [PATCH 07/10] Revamp deployment files to get UI in one container (#699) Signed-off-by: lucferbux --- .../workflows/build-and-push-ui-images.yml | 17 ---- Makefile | 8 +- clients/ui/.env | 5 +- clients/ui/.env.production | 3 + clients/ui/Dockerfile | 59 +++++++++++ clients/ui/Makefile | 64 ++++++++---- clients/ui/README.md | 98 ++++++++++++------- clients/ui/bff/.gitignore | 1 + clients/ui/bff/Dockerfile | 33 ------- clients/ui/bff/Makefile | 6 -- clients/ui/bff/README.md | 21 +--- clients/ui/bff/cmd/main.go | 2 +- clients/ui/bff/internal/api/app_test.go | 7 +- clients/ui/bff/static/index.html | 2 +- clients/ui/bff/static/sub/test.html | 2 +- clients/ui/docker-compose.yaml | 24 ----- clients/ui/docs/README.md | 11 +++ .../local-deployment-guide-ui.md} | 57 ++++++----- .../local-deployment-guide.md} | 37 ++++++- clients/ui/frontend/.env.development | 2 - clients/ui/frontend/CONTRIBUTING.md | 24 +++-- clients/ui/frontend/Dockerfile | 17 ---- clients/ui/frontend/Makefile | 13 --- clients/ui/frontend/docs/README.md | 4 +- clients/ui/frontend/docs/architecture.md | 2 +- clients/ui/frontend/docs/dev-setup.md | 2 - clients/ui/frontend/nginx.conf | 22 ----- clients/ui/frontend/src/app/App.tsx | 5 +- .../ui/frontend/src/shared/utilities/const.ts | 2 +- clients/ui/manifests/base/kustomization.yaml | 9 +- .../base/model-registry-bff-deployment.yaml | 29 ------ .../base/model-registry-bff-role.yaml | 23 ----- .../base/model-registry-bff-service.yaml | 15 --- .../base/model-registry-ui-deployment.yaml | 28 +++++- .../model-registry-ui-role.yaml} | 33 ++++++- ...=> model-registry-ui-service-account.yaml} | 2 +- .../ui/manifests/kubeflow/kustomization.yaml | 20 ---- .../model-registry-ui-deployment.yaml | 9 -- .../overlays/integrated/kustomization.yaml | 13 +++ .../model-registry-ui-deployment.yaml} | 1 + .../istio/authorization-policy-bff.yaml | 13 --- .../overlays/istio/destination-rule-bff.yaml | 11 --- .../overlays/istio/kustomization.yaml | 8 +- .../standalone/kubeflow-dashboard-rbac.yaml | 39 ++++++++ .../overlays/standalone/kustomization.yaml | 14 +++ .../model-registry-ui-deployment.yaml} | 1 + .../standalone/kubeflow-dashboard-rbac.yaml | 21 ---- .../manifests/standalone/kustomization.yaml | 19 ---- .../model-registry-ui-deployment.yaml | 9 -- clients/ui/scripts/deploy_kind_cluster.sh | 27 ++--- 50 files changed, 426 insertions(+), 468 deletions(-) create mode 100644 clients/ui/Dockerfile delete mode 100644 clients/ui/bff/Dockerfile delete mode 100644 clients/ui/docker-compose.yaml create mode 100644 clients/ui/docs/README.md rename clients/ui/{manifests/README.md => docs/local-deployment-guide-ui.md} (77%) rename clients/ui/{bff/docs/dev-guide.md => docs/local-deployment-guide.md} (95%) delete mode 100644 clients/ui/frontend/Dockerfile delete mode 100644 clients/ui/frontend/Makefile delete mode 100644 clients/ui/frontend/nginx.conf delete mode 100644 clients/ui/manifests/base/model-registry-bff-deployment.yaml delete mode 100644 clients/ui/manifests/base/model-registry-bff-role.yaml delete mode 100644 clients/ui/manifests/base/model-registry-bff-service.yaml rename clients/ui/manifests/{kubeflow/kubeflow-dashboard-rbac.yaml => base/model-registry-ui-role.yaml} (63%) rename clients/ui/manifests/base/{model-registry-bff-service-account.yaml => model-registry-ui-service-account.yaml} (64%) delete mode 100644 clients/ui/manifests/kubeflow/kustomization.yaml delete mode 100644 clients/ui/manifests/kubeflow/model-registry-ui-deployment.yaml create mode 100644 clients/ui/manifests/overlays/integrated/kustomization.yaml rename clients/ui/manifests/{kubeflow/model-registry-bff-deployment.yaml => overlays/integrated/model-registry-ui-deployment.yaml} (83%) delete mode 100644 clients/ui/manifests/overlays/istio/authorization-policy-bff.yaml delete mode 100644 clients/ui/manifests/overlays/istio/destination-rule-bff.yaml create mode 100644 clients/ui/manifests/overlays/standalone/kubeflow-dashboard-rbac.yaml create mode 100644 clients/ui/manifests/overlays/standalone/kustomization.yaml rename clients/ui/manifests/{standalone/model-registry-bff-deployment.yaml => overlays/standalone/model-registry-ui-deployment.yaml} (82%) delete mode 100644 clients/ui/manifests/standalone/kubeflow-dashboard-rbac.yaml delete mode 100644 clients/ui/manifests/standalone/kustomization.yaml delete mode 100644 clients/ui/manifests/standalone/model-registry-ui-deployment.yaml diff --git a/.github/workflows/build-and-push-ui-images.yml b/.github/workflows/build-and-push-ui-images.yml index b8c0d9727..0b98c127f 100644 --- a/.github/workflows/build-and-push-ui-images.yml +++ b/.github/workflows/build-and-push-ui-images.yml @@ -10,7 +10,6 @@ on: env: IMG_ORG: kubeflow IMG_UI_REPO: model-registry-ui - IMG_BFF_REPO: model-registry-bff DOCKER_USER: ${{ secrets.DOCKERHUB_USERNAME }} DOCKER_PWD: ${{ secrets.DOCKERHUB_TOKEN }} PUSH_IMAGE: true @@ -36,11 +35,6 @@ jobs: env: IMG_REPO: ${{ env.IMG_UI_REPO }} run: ./scripts/build_deploy.sh - - name: Build and Push BFF Image - shell: bash - env: - IMG_REPO: ${{ env.IMG_BFF_REPO }} - run: ./scripts/build_deploy.sh - name: Tag Latest UI Image if: env.BUILD_CONTEXT == 'main' shell: bash @@ -52,14 +46,3 @@ jobs: docker tag ${{ env.IMG }}:$VERSION ${{ env.IMG }}:latest # BUILD_IMAGE=false skip the build, just push the tag made above VERSION=latest ./scripts/build_deploy.sh - - name: Tag Latest BFF Image - if: env.BUILD_CONTEXT == 'main' - shell: bash - env: - IMG_REPO: ${{ env.IMG_BFF_REPO }} - IMG: ${{ env.IMG_ORG }}/${{ env.IMG_BFF_REPO }} - BUILD_IMAGE: false # image is already built in "Build and Push BFF Image" step - run: | - docker tag ${{ env.IMG }}:$VERSION ${{ env.IMG }}:latest - # BUILD_IMAGE=false skip the build, just push the tag made above - VERSION=latest ./scripts/build_deploy.sh \ No newline at end of file diff --git a/Makefile b/Makefile index 15e783cf1..ce7723725 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,7 @@ MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) PROJECT_PATH := $(patsubst %/,%,$(dir $(MKFILE_PATH))) PROJECT_BIN := $(PROJECT_PATH)/bin GO ?= "$(shell which go)" -BFF_PATH := $(PROJECT_PATH)/clients/ui/bff -UI_PATH := $(PROJECT_PATH)/clients/ui/frontend +UI_PATH := $(PROJECT_PATH)/clients/ui CSI_PATH := $(PROJECT_PATH)/cmd/csi # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. @@ -43,11 +42,6 @@ ifeq ($(IMG_REPO),model-registry-ui) BUILD_PATH := $(UI_PATH) endif -ifeq ($(IMG_REPO),model-registry-bff) - DOCKERFILE := $(BFF_PATH)/Dockerfile - BUILD_PATH := $(BFF_PATH) -endif - # The BUILD_PATH is still the root ifeq ($(IMG_REPO),model-registry-storage-initializer) DOCKERFILE := $(CSI_PATH)/Dockerfile.csi diff --git a/clients/ui/.env b/clients/ui/.env index 5e5fe0d8f..d2d6c7367 100644 --- a/clients/ui/.env +++ b/clients/ui/.env @@ -1,5 +1,6 @@ ############### Default settings ############### CONTAINER_TOOL=docker -IMG_BFF=kubeflow/model-registry-bff:latest -IMG_FRONTEND=kubeflow/model-registry-ui:latest +IMG_UI=kubeflow/model-registry-ui:latest +IMG_UI_STANDALONE=kubeflow/model-registry-ui-standalone:latest +PLATFORM=linux/amd64 diff --git a/clients/ui/.env.production b/clients/ui/.env.production index a904f4a9f..7dfa05b92 100644 --- a/clients/ui/.env.production +++ b/clients/ui/.env.production @@ -1 +1,4 @@ APP_ENV=production +MOCK_AUTH=false +DEPLOYMENT_MODE=integrated +STYLE_THEME=mui-theme diff --git a/clients/ui/Dockerfile b/clients/ui/Dockerfile new file mode 100644 index 000000000..21a9af3b3 --- /dev/null +++ b/clients/ui/Dockerfile @@ -0,0 +1,59 @@ +# Source code for the repos +ARG UI_SOURCE_CODE=./frontend +ARG BFF_SOURCE_CODE=./bff + +# Set the base images for the build stages +ARG NODE_BASE_IMAGE=node:20 +ARG GOLANG_BASE_IMAGE=golang:1.22.2 +ARG DISTROLESS_BASE_IMAGE=gcr.io/distroless/static:nonroot + +# UI build stage +FROM ${NODE_BASE_IMAGE} AS ui-builder + +ARG UI_SOURCE_CODE + +WORKDIR /usr/src/app + +# Copy the source code to the container +COPY ${UI_SOURCE_CODE} /usr/src/app + +# Install the dependencies and build +RUN npm cache clean --force +RUN npm ci --omit=optional +RUN npm run build:prod + +# BFF build stage +FROM ${GOLANG_BASE_IMAGE} AS bff-builder + +ARG BFF_SOURCE_CODE + +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /usr/src/app + +# Copy the Go Modules manifests +COPY ${BFF_SOURCE_CODE}/go.mod ${BFF_SOURCE_CODE}/go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy the go source files +COPY ${BFF_SOURCE_CODE}/cmd/main.go cmd/main.go +COPY ${BFF_SOURCE_CODE}/internal/ internal/ + +# Build the Go application +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o bff ./cmd/main.go + +# Final stage +# Use distroless as minimal base image to package the application binary +FROM ${DISTROLESS_BASE_IMAGE} +WORKDIR / +COPY --from=bff-builder /usr/src/app/bff ./ +COPY --from=ui-builder /usr/src/app/dist ./static/ +USER 65532:65532 + +# Expose port 8080 +EXPOSE 8080 + +ENTRYPOINT ["/bff"] diff --git a/clients/ui/Makefile b/clients/ui/Makefile index cf3dbc4db..06498fb53 100644 --- a/clients/ui/Makefile +++ b/clients/ui/Makefile @@ -44,36 +44,58 @@ dev-start: ############ Build ############ -.PHONY: build-bff -build-bff: - $(CONTAINER_TOOL) build -t ${IMG_BFF} ./bff +.PHONY: docker-build +docker-build: + $(CONTAINER_TOOL) build -t ${IMG_UI} . -.PHONY: build-frontend -build-frontend: - $(CONTAINER_TOOL) build -t ${IMG_FRONTEND} ./frontend +.PHONY: docker-build-standalone +docker-build-standalone: + MOCK_AUTH=true DEPLOYMENT_MODE=standalone $(CONTAINER_TOOL) build -t ${IMG_UI_STANDALONE} . -.PHONY: build -build: build-bff build-frontend +.PHONY: docker-buildx +docker-buildx: + docker buildx build --platform ${PLATFORM} -t ${IMG_UI} --push . -############ Push ############ +.PHONY: docker-buildx-standalone +docker-buildx-standalone: + MOCK_AUTH=true DEPLOYMENT_MODE=standalone docker buildx build --platform ${PLATFORM} -t ${IMG_UI_STANDALONE} --push . -.PHONY: push-bff -push-bff: - ${CONTAINER_TOOL} push ${IMG_BFF} +############ Push ############ -.PHONY: push-frontend -push-frontend: - ${CONTAINER_TOOL} push ${IMG_FRONTEND} +.PHONY: docker-push +docker-push: + ${CONTAINER_TOOL} push ${IMG_UI} -.PHONY: push -push: push-bff push-frontend +.PHONY: docker-push-standalone +docker-push-standalone: + ${CONTAINER_TOOL} push ${IMG_UI_STANDALONE} ############ Deployment ############ -.PHONY: docker-compose -docker-compose: - $(CONTAINER_TOOL) compose -f docker-compose.yaml up - .PHONY: kind-deployment kind-deployment: ./scripts/deploy_kind_cluster.sh + +############ Build ############ +.PHONY: frontend-build +frontend-build: + cd frontend && npm run build:prod + +.PHONY: frontend-build-standalone +frontend-build-standalone: + MOCK_AUTH=true DEPLOYMENT_MODE=standalone cd frontend && npm run build:prod + +.PHONY: bff-build +bff-build: + cd bff && make build + +.PHONY: build +build: frontend-build bff-build + +############ Run mocked ######## +.PHONY: run-local-mocked +run-local-mocked: frontend-build-standalone bff-build + rm -r ./bff/static-local-run && cp -r ./frontend/dist/ ./bff/static-local-run/ && cd bff && make run STATIC_ASSETS_DIR=./static-local-run MOCK_K8S_CLIENT=true DEV_MODE=true + + + \ No newline at end of file diff --git a/clients/ui/README.md b/clients/ui/README.md index 287b62450..e24055a19 100644 --- a/clients/ui/README.md +++ b/clients/ui/README.md @@ -2,6 +2,7 @@ [BFF requirements]: ./bff/README.md#pre-requisites [frontend dev setup]: ./frontend/docs/dev-setup.md#development [BFF dev setup]: ./bff/README.md#development +[Model registry UI]: ./docs/README.md # Model Registry UI @@ -24,14 +25,6 @@ To run the a mocked dev environment you can either: * Or follow the [frontend dev setup] and [BFF dev setup]. -### Docker deployment - -To build the Model Registry UI container, run the following command: - -```shell -make docker-compose -``` - ### Kubernetes Deployment For a in-depth guide on how to deploy the Model Registry UI, please refer to the [local kubernetes deployment](./bff/docs/dev-guide.md) documentation. @@ -47,6 +40,14 @@ make kind-deployment You can find the OpenAPI specification for the Model Registry UI in the [openapi](./api/openapi) directory. A live version of the OpenAPI specification can be found [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubeflow/model-registry/main/clients/ui/api/openapi/mod-arch.yaml). +## Targeted environments + +There's two main environments that the Model Registry UI is targeted for: + +1. **Standalone**: This is the default environment for local development. The UI is served by the BFF and the BFF is responsible for serving the API requests. The BFF exposes a `/namespace` endpoint that returns all the namespaces in the cluster and the UI sends a user header `kubeflow-user` to authenticate the calls. + +2. **Integrated**: This is the environment where the UI is served by the Kubeflow Ingress and the BFF is served by the Kubeflow API Gateway. The BFF is responsible for serving the API requests and namespace selection is leveraged from Kubeflow. + ## Environment Variables The following environment variables are used to configure the deployment and development environment for the Model Registry UI. These variables should be defined in a `.env.local` file in the `clients/ui` directory of the project. **This values will affect the build and push commands**. @@ -58,17 +59,35 @@ The following environment variables are used to configure the deployment and dev * **Possible Values**: `docker`, `podman`, etc. * **Example**: `CONTAINER_TOOL=docker` -### `IMG_BFF` +### `IMG_UI` -* **Description**: Specifies the image name and tag for the Backend For Frontend (BFF) service. -* **Default Value**: `model-registry-bff:latest` -* **Example**: `IMG_BFF=model-registry-bff:latest` +* **Description**: Specifies the image name and tag for the UI (with BFF). +* **Default Value**: `model-registry-ui:latest` +* **Example**: `IMG_UI=model-registry-bff:latest` -### `IMG_FRONTEND` +### `IMG_UI_STANDALONE` -* **Description**: Specifies the image name and tag for the frontend service. -* **Default Value**: `model-registry-frontend:latest` -* **Example**: `IMG_FRONTEND=model-registry-frontend:latest` +* **Description**: Specifies the image name and tag for the UI (with BFF) in **standalone mode**, used for local kind deployment. +* **Default Value**: `model-registry-ui-standalone:latest` +* **Example**: `IMG_UI_STANDALONE=model-registry-bff:latest` + +### `PLATFORM` + +* **Description**: Specifies the platform for a **docker buildx** build. +* **Default Value**: `linux/amd64` +* **Example**: `PLATFORM=linux/amd64` + +### `MOCK_AUTH` + +* **Description**: Specifies whether to mock authentication in the UI. +* **Default Value**: `true` (in dev mode) / `false` (in production mode) +* **Possible Values**: `true`, `false` + +### `DEPLOYMENT_MODE` + +* **Description**: Specifies the deployment mode for the UI. +* **Default Value**: `standalone` (in dev mode) / `integrated` (in production mode) +* **Possible Values**: `standalone`, `integrated` ### Example `.env.local` File @@ -76,38 +95,43 @@ Here is an example of what your `.env.local` file might look like: ```shell CONTAINER_TOOL=docker -IMG_BFF=model-registry-bff:latest -IMG_FRONTEND=model-registry-frontend:latest +IMG_UI=quay.io//model-registry-ui:latest +IMG_UI_STANDALONE=quay.io//model-registry-ui-standalone:latest +PLATFORM=linux/amd64 ``` ## Build and Push Commands -The following Makefile targets are used to build and push the Docker images for the Backend For Frontend (BFF) and frontend services. These targets utilize the environment variables defined in the `.env.local` file. +The following Makefile targets are used to build and push the Docker images the UI images. These targets utilize the environment variables defined in the `.env.local` file. ### Build Commands -* **`build-bff`**: Builds the Docker image for the BFF service. - * Command: `make build-bff` - * This command uses the `CONTAINER_TOOL` and `IMG_BFF` environment variables to build the image. +* **`docker-build`**: Builds the Docker image for the UI platform. + * Command: `make docker-build` + * This command uses the `CONTAINER_TOOL` and `IMG_UI` environment variables to push the image. -* **`build-frontend`**: Builds the Docker image for the frontend service. - * Command: `make build-frontend` - * This command uses the `CONTAINER_TOOL` and `IMG_FRONTEND` environment variables to build the image. +* **`docker-buildx`**: Builds the Docker image with buildX for multiarch support. + * Command: `make docker-buildx` + * This command uses the `CONTAINER_TOOL` and `IMG_UI` environment variables to push the image. -* **`build`**: Builds the Docker images for both the BFF and frontend services. - * Command: `make build` - * This command runs both `build-bff` and `build-frontend` targets. +* **`docker-build-standalone`**: Builds the Docker image for the UI platform **in standalone mode**. + * Command: `make docker-build-standalone` + * This command uses the `CONTAINER_TOOL` and `IMG_UI_STANDALONE` environment variables to push the image. + +* **`docker-buildx-standalone`**: Builds the Docker image with buildX for multiarch support **in standalone mode**. + * Command: `make docker-buildx-standalone` + * This command uses the `CONTAINER_TOOL` and `IMG_UI_STANDALONE` environment variables to push the image. ### Push Commands -* **`push-bff`**: Pushes the Docker image for the BFF service to the container registry. - * Command: `make push-bff` - * This command uses the `CONTAINER_TOOL` and `IMG_BFF` environment variables to push the image. +* **`docker-push`**: Pushes the Docker image for the UI service to the container registry. + * Command: `make docker-push` + * This command uses the `CONTAINER_TOOL` and `IMG_UI` environment variables to push the image. + +* **`docker-push-standalone`**: Pushes the Docker image for the UI service to the container registry **in standalone mode**. + * Command: `make docker-push-standalone` + * This command uses the `CONTAINER_TOOL` and `IMG_UI_STANDALONE` environment variables to push the image. -* **`push-frontend`**: Pushes the Docker image for the frontend service to the container registry. - * Command: `make push-frontend` - * This command uses the `CONTAINER_TOOL` and `IMG_FRONTEND` environment variables to push the image. +## Deployments -* **`push`**: Pushes the Docker images for both the BFF and frontend services to the container registry. - * Command: `make push` - * This command runs both `push-bff` and `push-frontend` targets. +For more information on how to deploy the Model Registry UI, please refer to the [Model registry UI] documentation. diff --git a/clients/ui/bff/.gitignore b/clients/ui/bff/.gitignore index 5e56e040e..ee5236afe 100644 --- a/clients/ui/bff/.gitignore +++ b/clients/ui/bff/.gitignore @@ -1 +1,2 @@ /bin +/static-local-run \ No newline at end of file diff --git a/clients/ui/bff/Dockerfile b/clients/ui/bff/Dockerfile deleted file mode 100644 index b8625294c..000000000 --- a/clients/ui/bff/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -# Use the golang image to build the application -FROM golang:1.22.2 AS builder -ARG TARGETOS -ARG TARGETARCH - -WORKDIR /ui - -# Copy the Go Modules manifests -COPY go.mod go.sum ./ - -# Download dependencies -RUN go mod download - -# Copy the go source files -COPY cmd/main.go cmd/main.go -COPY internal/ internal/ - -# Copy the static assets -COPY $STATIC_ASSETS_DIR static/ - -# Build the Go application -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o bff ./cmd/main.go - -# Use distroless as minimal base image to package the application binary -FROM gcr.io/distroless/static:nonroot -WORKDIR / -COPY --from=builder ui/bff ./ -USER 65532:65532 - -# Expose port 4000 -EXPOSE 4000 - -ENTRYPOINT ["/bff", "--static-assets-dir=/static"] diff --git a/clients/ui/bff/Makefile b/clients/ui/bff/Makefile index 4d8660bab..96f26131c 100644 --- a/clients/ui/bff/Makefile +++ b/clients/ui/bff/Makefile @@ -1,5 +1,3 @@ -CONTAINER_TOOL ?= docker -IMG ?= model-registry-bff:latest PORT ?= 4000 MOCK_K8S_CLIENT ?= false MOCK_MR_CLIENT ?= false @@ -52,10 +50,6 @@ 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) -.PHONY: docker-build -docker-build: ## Builds a container for the project. - $(CONTAINER_TOOL) build -t ${IMG} . - ##@ Dependencies ## Location to install dependencies to diff --git a/clients/ui/bff/README.md b/clients/ui/bff/README.md index 0befdc450..3886245e1 100644 --- a/clients/ui/bff/README.md +++ b/clients/ui/bff/README.md @@ -54,26 +54,7 @@ make docker-build ### Endpoints -| URL Pattern | Handler | Action | -|----------------------------------------------------------------------------------------------|----------------------------------------------|-------------------------------------------------------------| -| GET /v1/healthcheck | HealthcheckHandler | Show application information. | -| GET /v1/user | UserHandler | Show "kubeflow-user-id" from header information. | -| GET /v1/namespaces | NamespacesHandler | Get all user namespaces. (only enabled in devmode) | -| GET /v1/model_registry | ModelRegistryHandler | Get all model registries, | -| GET /v1/model_registry/{model_registry_id}/registered_models | GetAllRegisteredModelsHandler | Gets a list of all RegisteredModel entities. | -| POST /v1/model_registry/{model_registry_id}/registered_models | CreateRegisteredModelHandler | Create a RegisteredModel entity. | -| GET /v1/model_registry/{model_registry_id}/registered_models/{registered_model_id} | GetRegisteredModelHandler | Get a RegisteredModel entity by ID | -| PATCH /v1/model_registry/{model_registry_id}/registered_models/{registered_model_id} | UpdateRegisteredModelHandler | Update a RegisteredModel entity by ID | -| GET /api/v1/model_registry/{model_registry_id}/model_versions/{model_version_id} | GetModelVersionHandler | Get a ModelVersion by ID | -| POST /api/v1/model_registry/{model_registry_id}/model_versions | CreateModelVersionHandler | Create a ModelVersion entity | -| PATCH /api/v1/model_registry/{model_registry_id}/model_versions/{model_version_id} | UpdateModelVersionHandler | Update a ModelVersion entity by ID | -| GET /v1/model_registry/{model_registry_id}/registered_models/{registered_model_id}/versions | GetAllModelVersionsForRegisteredModelHandler | Get all ModelVersion entities by RegisteredModel ID | -| POST /v1/model_registry/{model_registry_id}/registered_models/{registered_model_id}/versions | CreateModelVersionForRegisteredModelHandler | Create a ModelVersion entity for a specific RegisteredModel | -| GET /api/v1/model_registry/{model_registry_id}/model_versions/{model_version_id}/artifacts | GetAllModelArtifactsByModelVersionHandler | Get all ModelArtifact entities by ModelVersion ID | -| POST /api/v1/model_registry/{model_registry_id}/model_versions/{model_version_id}/artifacts | CreateModelArtifactByModelVersion | Create a ModelArtifact entity for a specific ModelVersion | - -Note: Most API paths require the namespace parameter to be passed as a query parameter. -The only exceptions are the health check (/v1/healthcheck) and user (/v1/user) paths, which do not require the namespace parameter. +See the [OpenAPI specification](../api/openapi/mod-arch.yaml) for a complete list of endpoints. ### Sample local calls diff --git a/clients/ui/bff/cmd/main.go b/clients/ui/bff/cmd/main.go index e767515da..a38ae341c 100644 --- a/clients/ui/bff/cmd/main.go +++ b/clients/ui/bff/cmd/main.go @@ -19,7 +19,7 @@ import ( func main() { var cfg config.EnvConfig - flag.IntVar(&cfg.Port, "port", getEnvAsInt("PORT", 4000), "API server port") + flag.IntVar(&cfg.Port, "port", getEnvAsInt("PORT", 8080), "API server port") flag.BoolVar(&cfg.MockK8Client, "mock-k8s-client", false, "Use mock Kubernetes client") flag.BoolVar(&cfg.MockMRClient, "mock-mr-client", false, "Use mock Model Registry client") flag.BoolVar(&cfg.DevMode, "dev-mode", false, "Use development mode for access to local K8s cluster") diff --git a/clients/ui/bff/internal/api/app_test.go b/clients/ui/bff/internal/api/app_test.go index e7cd23f81..c03afb0b5 100644 --- a/clients/ui/bff/internal/api/app_test.go +++ b/clients/ui/bff/internal/api/app_test.go @@ -1,13 +1,14 @@ package api import ( + "io" + "net/http" + httptest "net/http/httptest" + "github.com/kubeflow/model-registry/ui/bff/internal/config" "github.com/kubeflow/model-registry/ui/bff/internal/repositories" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "io" - "net/http" - httptest "net/http/httptest" ) var _ = Describe("Static File serving Test", func() { diff --git a/clients/ui/bff/static/index.html b/clients/ui/bff/static/index.html index 41c73cf02..d335bc73a 100644 --- a/clients/ui/bff/static/index.html +++ b/clients/ui/bff/static/index.html @@ -7,4 +7,4 @@

Welcome to the BFF Stub Page

This is a placeholder page for the serving frontend.

- + \ No newline at end of file diff --git a/clients/ui/bff/static/sub/test.html b/clients/ui/bff/static/sub/test.html index a28fa30f2..9c7be1706 100644 --- a/clients/ui/bff/static/sub/test.html +++ b/clients/ui/bff/static/sub/test.html @@ -7,4 +7,4 @@

Welcome to the BFF Stub Subfolder Page

This is a placeholder page for the serving frontend.

- + \ No newline at end of file diff --git a/clients/ui/docker-compose.yaml b/clients/ui/docker-compose.yaml deleted file mode 100644 index 7fbc2c8ca..000000000 --- a/clients/ui/docker-compose.yaml +++ /dev/null @@ -1,24 +0,0 @@ -services: - frontend: - build: ./frontend - container_name: model-registry-ui - ports: - - 8080:8080 - environment: - API_URL: http://model-registry-bff:4000 - networks: - - model_registry - depends_on: - - bff - bff: - build: ./bff - container_name: model-registry-bff - command: - - "--mock-k8s-client=true" - - "--mock-mr-client=true" - networks: - - model_registry - -networks: - model_registry: - name: model_registry diff --git a/clients/ui/docs/README.md b/clients/ui/docs/README.md new file mode 100644 index 000000000..8889790a8 --- /dev/null +++ b/clients/ui/docs/README.md @@ -0,0 +1,11 @@ +[Local deployment]: ./local-deployment-guide.md +[Local deployment UI]: ./local-deployment-guide-ui.md + +# Model Registry UI Docs + +This directory contains documentation for the Model Registry UI. + +## Local Deployment Guide + +* [Local Deployment Guide][Local deployment] +* [Local Deployment Guide UI][Local deployment UI] diff --git a/clients/ui/manifests/README.md b/clients/ui/docs/local-deployment-guide-ui.md similarity index 77% rename from clients/ui/manifests/README.md rename to clients/ui/docs/local-deployment-guide-ui.md index 559ec779d..10e9298a9 100644 --- a/clients/ui/manifests/README.md +++ b/clients/ui/docs/local-deployment-guide-ui.md @@ -1,76 +1,87 @@ [Model registry server set up]: ../../bff/docs/dev-guide.md -## Deploying the Model Registry UI in a local cluster +# Deploying the Model Registry UI in a local cluster For this guide, we will be using kind for locally deploying our cluster. See the [Model registry server set up] guide for prerequisites on setting up kind and deploying the model registry server. -### Setup -#### 1. Create a kind cluster +## Setup + +### 1. Create a kind cluster + Create a local cluster for running the MR UI using the following command: + ```shell kind create cluster ``` -#### 2. Create kubeflow namespace +### 2. Create kubeflow namespace + Create a namespace for model registry to run in, by default this is kubeflow, run: + ```shell kubectl create namespace kubeflow ``` -#### 3. Deploy Model Registry UI to cluster +### 3. Deploy Model Registry UI to cluster + You can now deploy the UI and BFF to your newly created cluster using the kustomize configs in this directory: + ```shell cd clients/ui -kubectl apply -k manifests/base/ -n kubeflow +kubectl apply -k manifests/overlay/standalone -n kubeflow ``` After a few seconds you should see 2 pods running (1 for BFF and 1 for UI): + ```shell kubectl get pods -n kubeflow ``` -``` + +```shell NAME READY STATUS RESTARTS AGE -model-registry-bff-746f674b99-bfvgs 1/1 Running 0 11s model-registry-ui-58755c4754-zdrnr 1/1 Running 0 11s ``` -#### 4. Access the Model Registry UI running in the cluster +### 4. Access the Model Registry UI running in the cluster + Now that the pods are up and running you can access the UI. First you will need to port-forward the UI service by running the following in it's own terminal: + ```shell kubectl port-forward service/model-registry-ui-service 8080:8080 -n kubeflow ``` You can then access the UI running in your cluster locally at http://localhost:8080/ -To test the BFF separately you can also port-forward that service by running: -```shell -kubectl port-forward service/model-registry-bff-service 4000:4000 -n kubeflow -``` - You can now make API requests to the BFF endpoints like: + ```shell -curl http://localhost:4000/api/v1/model-registry -``` +curl http://localhost:8080/api/v1/model-registry ``` + +```json { "model_registry": null } ``` -### Troubleshooting +## Troubleshooting + +### Running on macOS -#### Running on macOS When running locally on macOS you may find the pods fail to deploy, with one or more stuck in the `pending` state. This is usually due to insufficient memory allocated to your docker / podman virtual machine. You can verify this by running: + ```shell kubectl describe pods -n kubeflow ``` + If you're experiencing this issue you'll see an output containing something similar to the following: -``` + +```shell Events: Type Reason Age From Message ---- ------ ---- ---- ------- @@ -79,8 +90,10 @@ Events: To fix this, you'll need to increase the amount of memory available to the VM. This can be done through either the Podman Desktop or Docker Desktop GUI. 6-8GB of memory is generally a sufficient amount to use. -## Running with Kubeflow and Istio +# Running with Kubeflow and Istio + Alternatively, if you'd like to run the UI and BFF pods with an Istio configuration for the KF Central Dashboard, you can apply the manifests by running: + ```shell -kubectl apply -k overlays/kubeflow -n kubeflow -``` \ No newline at end of file +kubectl apply -k overlays/istio -n kubeflow +``` diff --git a/clients/ui/bff/docs/dev-guide.md b/clients/ui/docs/local-deployment-guide.md similarity index 95% rename from clients/ui/bff/docs/dev-guide.md rename to clients/ui/docs/local-deployment-guide.md index 582c72896..58b71e2fd 100644 --- a/clients/ui/bff/docs/dev-guide.md +++ b/clients/ui/docs/local-deployment-guide.md @@ -1,72 +1,94 @@ -# Development Guide +# Local Deployment Guide ## Local kubernetes deployment of Model Registry + To test the BFF locally without mocking the k8s calls the Model Registry backend can be deployed locally using kind. -### Prerequisites +### Prerequisites + The following tools need to be installed in your local environment: -* Podman (Docker should also work) - [Podman Desktop Instructions](https://podman-desktop.io) + +* Docker - [Docker Instructions](https://www.docker.com) * kubectl - [Instructions](https://kubernetes.io/docs/tasks/tools/#kubectl) * kind - [Instructions](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) Note: all of the above tools can be installed using your OS package manager, this is the preferred method. ### Setup + #### 1. Create a kind cluster + Create a local cluster for running the MR backend using the following command: + ```shell kind create cluster ``` Kind will start creating a new local cluster for you to deploy, once it has completed verify you can access the cluster using kubectl by running: + ```shell kubectl cluster-info ``` If everything is working correctly you should see output similar to: + ``` Kubernetes control plane is running at https://127.0.0.1:58635 CoreDNS is running at https://127.0.0.1:58635/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy ``` #### 2. Create kubeflow namespace + Create a namespace for model registry to run in, by default this is kubeflow, run: + ```shell kubectl create namespace kubeflow ``` #### 3. Deploy Model Registry to cluster + You can now deploy the MR backend to your newly created cluster using the kustomize configs in the MR repository by running: + ```shell kubectl apply -k "https://github.com/kubeflow/model-registry/manifests/kustomize/overlays/db" ``` Wait for the model registry deployment to spin up, alternatively run: + ```shell kubectl wait --for=condition=available -n kubeflow deployment/model-registry-deployment --timeout=1m ``` + This command will return when the cluster is ready. To verify this now run: + ```shell kubectl get pods -n kubeflow ``` + Two pods should be listed, `model-registry-db-xxx` and `model-registry-deployment-yyy` both should have a status of `Running`. ##### NOTE: Issues running on arm64 architecture + There is currently an issue deploying to an arm64 device such as a Mac with an M-series chip. This is because the MySql image tag deployed by the manifests does not have an arm64 compatible image. To work around this you can use a modified manifest deployed in a fork of the repo - you can use this by running the below command instead of the first command in section 3 of this guide. + ```shell kubectl apply -k "https://github.com/alexcreasy/model-registry/manifests/kustomize/overlays/db?ref=kind" ``` -Note: an issue has been filed regarding this ticket here: + +Note: an issue has been filed regarding this ticket here: + * [#266 Cannot deploy to k8s on AArch64 nodes using manifests in repo](https://github.com/kubeflow/model-registry/issues/266) #### 4. Setup a port forward to the service + In order to access the MR REST API locally you need to forward a local port to 8080 on the MR service. Run the following command: + ```shell kubectl port-forward svc/model-registry-service -n kubeflow 8080:8080 ``` @@ -75,22 +97,29 @@ Note: you can change the local forwarded port by changing the first port value, to the MR service. #### 5. Test the service + In a separate terminal window to the previous step, test the service by querying one of the rest endpoints, for example: + ```shell curl http://localhost:8080/api/model_registry/v1alpha3/registered_models ``` + You should receive a 200 response if everything is working correctly, the body should look like: ```json {"items":[],"nextPageToken":"","pageSize":0,"size":0} ``` #### 6. Run BFF locally in Dev Mode + To access your local kind cluster when running the BFF locally, you can use the `DEV_MODE` option. This is useful for when you want to test live changes on real cluster. To do so, simply run: + ```shell make run DEV_MODE=true ``` + You can also specify the port you are forwarding to if it is something other than 8080: + ```shell make run DEV_MODE=true DEV_MODE_PORT=8081 ``` diff --git a/clients/ui/frontend/.env.development b/clients/ui/frontend/.env.development index 7d7cc8e82..005a0c6ca 100644 --- a/clients/ui/frontend/.env.development +++ b/clients/ui/frontend/.env.development @@ -1,3 +1 @@ APP_ENV=development -MOCK_AUTH=true -DEPLOYMENT_MODE=standalone \ No newline at end of file diff --git a/clients/ui/frontend/CONTRIBUTING.md b/clients/ui/frontend/CONTRIBUTING.md index 93bf6a1a0..c7901a0c7 100644 --- a/clients/ui/frontend/CONTRIBUTING.md +++ b/clients/ui/frontend/CONTRIBUTING.md @@ -22,20 +22,32 @@ And one for the "backend": ```bash cd ../bff -docker compose -f docker-compose.yaml up +go run ./cmd/main.go --port=4000 --static-assets-dir=./static --mock-k8s-client=true --mock-mr-client=true --dev-mode=true --dev-mode-port=8080 --standalone-mode=true ``` Once you have both services ready, you can open the dashboard locally at: `http://localhost:4010`. The dev server will reload automatically when you make changes. +You can also run an automated command to run both services: + +```bash +cd .. && make dev-start +``` ## Debugging and Testing See [frontend testing guidelines](docs/testing.md) for more information. -## Environment variables - -[TBD] +### Environment Variables -### Building your image +The following environment variables are used to configure the deployment and development environment for the Model Registry UI. These variables should be defined in a `.env.local` file in the `clients/ui` directory of the project. **These values will affect the build and push commands**. -[TBD] \ No newline at end of file +- `LOGO=logo-light-theme.svg` + - The file name for the logo used in the light theme. +- `LOGO_DARK=logo-dark-theme.svg` + - The file name for the logo used in the dark theme. +- `FAVICON=favicon.ico` + - The file name for the favicon of the application. +- `PRODUCT_NAME="Model Registry"` + - The name of the product displayed in the UI. +- `STYLE_THEME=mui-theme` + - The style theme used for the UI, in this case, Material-UI theme. diff --git a/clients/ui/frontend/Dockerfile b/clients/ui/frontend/Dockerfile deleted file mode 100644 index 448f724b9..000000000 --- a/clients/ui/frontend/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM node:20 AS build-stage - -WORKDIR /usr/src/app - -COPY . /usr/src/app - -RUN npm cache clean --force -RUN npm ci --omit=optional -RUN npm run build:prod - -FROM nginxinc/nginx-unprivileged - -ENV API_URL="http://localhost:4000" -ENV NGINX_ENVSUBST_FILTER="API_URL" - -COPY --from=build-stage /usr/src/app/dist/ "/usr/share/nginx/html" -COPY --from=build-stage /usr/src/app/nginx.conf "/etc/nginx/templates/default.conf.template" diff --git a/clients/ui/frontend/Makefile b/clients/ui/frontend/Makefile deleted file mode 100644 index f11445091..000000000 --- a/clients/ui/frontend/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -CONTAINER_TOOL ?= docker -IMG ?= model-registry-ui:latest - -.PHONY: all -all: docker-build - -.PHONY: help -help: ## Display this help. - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) - -.PHONY: docker-build -docker-build: - $(CONTAINER_TOOL) build -t ${IMG} . diff --git a/clients/ui/frontend/docs/README.md b/clients/ui/frontend/docs/README.md index 7a18ac343..48ecca522 100644 --- a/clients/ui/frontend/docs/README.md +++ b/clients/ui/frontend/docs/README.md @@ -1,5 +1,6 @@ [Dev setup & Requirements]: dev-setup.md [Architecture]: architecture.md +[Testing]: testing.md # Model Registry UI Documentation @@ -9,4 +10,5 @@ This is the general documentation of the Model Registry UI. ## Developer Readmes * [Dev setup & Requirements] -* [Architecture] \ No newline at end of file +* [Architecture] +* [Testing] \ No newline at end of file diff --git a/clients/ui/frontend/docs/architecture.md b/clients/ui/frontend/docs/architecture.md index 5ad39411b..f2a9f0383 100644 --- a/clients/ui/frontend/docs/architecture.md +++ b/clients/ui/frontend/docs/architecture.md @@ -4,4 +4,4 @@ ![Overview](./meta/arch-overview.png) -[TBD] +The Model Registry UI is a web application that provides a user interface for interacting with the Model Registry API. The UI is built using React and communicates with the Model Registry API using RESTful HTTP requests. The UI is designed to be responsive and accessible, providing a user-friendly experience for users interacting with the Model Registry. diff --git a/clients/ui/frontend/docs/dev-setup.md b/clients/ui/frontend/docs/dev-setup.md index 76981767c..2fe6e9653 100644 --- a/clients/ui/frontend/docs/dev-setup.md +++ b/clients/ui/frontend/docs/dev-setup.md @@ -32,8 +32,6 @@ npm run build This is the default context for running a local UI. Make sure you build the project using the instructions above prior to running the command below. -You will need to inject your requests with a kubeflow-userid header for authorization purposes. For example, you can use the [Header Editor](https://chromewebstore.google.com/detail/eningockdidmgiojffjmkdblpjocbhgh) extension in Chrome to set the kubeflow-userid header to user@example.com. - ```bash npm run start:dev ``` diff --git a/clients/ui/frontend/nginx.conf b/clients/ui/frontend/nginx.conf deleted file mode 100644 index becbbfd8c..000000000 --- a/clients/ui/frontend/nginx.conf +++ /dev/null @@ -1,22 +0,0 @@ -server { - listen 8080 default_server; - listen [::]:8080 default_server; - server_name _; - root /usr/share/nginx/html; - gzip on; - access_log /dev/stdout main; - - location / { - try_files $uri $uri/ /index.html; - } - - location /api/ { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass ${API_URL}; - proxy_http_version 1.1; - } - } - diff --git a/clients/ui/frontend/src/app/App.tsx b/clients/ui/frontend/src/app/App.tsx index 9dd18f386..625ae2c04 100644 --- a/clients/ui/frontend/src/app/App.tsx +++ b/clients/ui/frontend/src/app/App.tsx @@ -74,12 +74,11 @@ const App: React.FC = () => {

- {String(error)} {configError?.message || namespacesLoadError?.message || - 'Unknown error occurred during startup!!!!!!'} + 'Unknown error occurred during startup'}

-

Logging out and logging back in may solve the issue!!!!!c.

+

Logging out and logging back in may solve the issue

diff --git a/clients/ui/frontend/src/shared/utilities/const.ts b/clients/ui/frontend/src/shared/utilities/const.ts index ddcdd7b9e..747affa19 100644 --- a/clients/ui/frontend/src/shared/utilities/const.ts +++ b/clients/ui/frontend/src/shared/utilities/const.ts @@ -13,7 +13,7 @@ export const isMUITheme = (): boolean => STYLE_THEME === Theme.MUI; export const isStandalone = (): boolean => DEPLOYMENT_MODE === DeploymentMode.Standalone; export const isIntegrated = (): boolean => DEPLOYMENT_MODE === DeploymentMode.Integrated; -const STYLE_THEME = process.env.STYLE_THEME || Theme.MUI; +const STYLE_THEME = process.env.STYLE_THEME || Theme.Default; const DEV_MODE = process.env.APP_ENV === 'development'; const MOCK_AUTH = process.env.MOCK_AUTH === 'true'; const DEPLOYMENT_MODE = process.env.DEPLOYMENT_MODE || DeploymentMode.Integrated; diff --git a/clients/ui/manifests/base/kustomization.yaml b/clients/ui/manifests/base/kustomization.yaml index 0ab619941..03230eb6c 100644 --- a/clients/ui/manifests/base/kustomization.yaml +++ b/clients/ui/manifests/base/kustomization.yaml @@ -2,17 +2,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- model-registry-bff-role.yaml -- model-registry-bff-service.yaml -- model-registry-bff-deployment.yaml -- model-registry-bff-service-account.yaml +- model-registry-ui-role.yaml - model-registry-ui-service.yaml - model-registry-ui-deployment.yaml +- model-registry-ui-service-account.yaml images: - name: model-registry-ui-image newName: docker.io/kubeflow/model-registry-ui newTag: latest -- name: model-registry-bff-image - newName: docker.io/kubeflow/model-registry-bff - newTag: latest diff --git a/clients/ui/manifests/base/model-registry-bff-deployment.yaml b/clients/ui/manifests/base/model-registry-bff-deployment.yaml deleted file mode 100644 index 27a2b5998..000000000 --- a/clients/ui/manifests/base/model-registry-bff-deployment.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: model-registry-bff - labels: - app: model-registry-bff -spec: - replicas: 1 - selector: - matchLabels: - app: model-registry-bff - template: - metadata: - labels: - app: model-registry-bff - spec: - serviceAccountName: model-registry-bff - containers: - - name: model-registry-bff - image: model-registry-bff-image - resources: - limits: - cpu: 500m - memory: 2Gi - requests: - cpu: 500m - memory: 2Gi - ports: - - containerPort: 4000 diff --git a/clients/ui/manifests/base/model-registry-bff-role.yaml b/clients/ui/manifests/base/model-registry-bff-role.yaml deleted file mode 100644 index 82a210855..000000000 --- a/clients/ui/manifests/base/model-registry-bff-role.yaml +++ /dev/null @@ -1,23 +0,0 @@ ---- -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: bff-service-reader -rules: -- apiGroups: [""] - resources: ["services"] - verbs: ["get", "watch", "list"] - ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: bff-read-services -subjects: -- kind: ServiceAccount - name: model-registry-bff - namespace: kubeflow -roleRef: - kind: ClusterRole - name: bff-service-reader - apiGroup: rbac.authorization.k8s.io diff --git a/clients/ui/manifests/base/model-registry-bff-service.yaml b/clients/ui/manifests/base/model-registry-bff-service.yaml deleted file mode 100644 index 929022cdb..000000000 --- a/clients/ui/manifests/base/model-registry-bff-service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: model-registry-bff-service - labels: - app: model-registry-bff - run: model-registry-bff -spec: - selector: - app: model-registry-bff - ports: - - protocol: TCP - port: 4000 - targetPort: 4000 - name: http \ No newline at end of file diff --git a/clients/ui/manifests/base/model-registry-ui-deployment.yaml b/clients/ui/manifests/base/model-registry-ui-deployment.yaml index 0ef0cd6c2..41e1aa456 100644 --- a/clients/ui/manifests/base/model-registry-ui-deployment.yaml +++ b/clients/ui/manifests/base/model-registry-ui-deployment.yaml @@ -14,9 +14,32 @@ spec: labels: app: model-registry-ui spec: + serviceAccountName: model-registry-ui containers: - name: model-registry-ui image: model-registry-ui-image + imagePullPolicy: Always + livenessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 30 + timeoutSeconds: 15 + periodSeconds: 30 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /api/v1/healthcheck + port: 8080 + scheme: HTTP + httpHeaders: + - name: kubeflow-userid + value: user@example.com + initialDelaySeconds: 15 + timeoutSeconds: 15 + periodSeconds: 30 + successThreshold: 1 + failureThreshold: 3 resources: limits: cpu: 500m @@ -26,6 +49,5 @@ spec: memory: 2Gi ports: - containerPort: 8080 - env: - - name: API_URL - value: "http://model-registry-bff-service:4000" + args: + - "--port=8080" diff --git a/clients/ui/manifests/kubeflow/kubeflow-dashboard-rbac.yaml b/clients/ui/manifests/base/model-registry-ui-role.yaml similarity index 63% rename from clients/ui/manifests/kubeflow/kubeflow-dashboard-rbac.yaml rename to clients/ui/manifests/base/model-registry-ui-role.yaml index 0b5896d6d..d1dfef45d 100644 --- a/clients/ui/manifests/kubeflow/kubeflow-dashboard-rbac.yaml +++ b/clients/ui/manifests/base/model-registry-ui-role.yaml @@ -1,4 +1,30 @@ --- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: model-registry-ui-services-reader +rules: +- apiGroups: + - '' + resources: + - services + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: model-registry-ui-services-reader-binding +subjects: +- kind: ServiceAccount + name: model-registry-ui +roleRef: + kind: ClusterRole + name: model-registry-ui-services-reader + apiGroup: rbac.authorization.k8s.io +--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -11,6 +37,7 @@ rules: verbs: - get - list + - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -22,8 +49,7 @@ roleRef: name: model-registry-retrieve-clusterrolebindings subjects: - kind: ServiceAccount - name: model-registry-bff - namespace: kubeflow + name: model-registry-ui --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -47,5 +73,4 @@ roleRef: name: model-registry-create-sars subjects: - kind: ServiceAccount - name: model-registry-bff - namespace: kubeflow + name: model-registry-ui diff --git a/clients/ui/manifests/base/model-registry-bff-service-account.yaml b/clients/ui/manifests/base/model-registry-ui-service-account.yaml similarity index 64% rename from clients/ui/manifests/base/model-registry-bff-service-account.yaml rename to clients/ui/manifests/base/model-registry-ui-service-account.yaml index 5cbafe203..a35ae35c0 100644 --- a/clients/ui/manifests/base/model-registry-bff-service-account.yaml +++ b/clients/ui/manifests/base/model-registry-ui-service-account.yaml @@ -2,4 +2,4 @@ kind: ServiceAccount apiVersion: v1 metadata: - name: model-registry-bff + name: model-registry-ui diff --git a/clients/ui/manifests/kubeflow/kustomization.yaml b/clients/ui/manifests/kubeflow/kustomization.yaml deleted file mode 100644 index 64ebb3d52..000000000 --- a/clients/ui/manifests/kubeflow/kustomization.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - ../overlays/istio - - kubeflow-dashboard-rbac.yaml - -patchesJson6902: - - path: model-registry-ui-deployment.yaml - target: - group: apps - version: v1 - kind: Deployment - name: model-registry-ui-deployment - - path: model-registry-bff-deployment.yaml - target: - group: apps - version: v1 - kind: Deployment - name: model-registry-bff-deployment \ No newline at end of file diff --git a/clients/ui/manifests/kubeflow/model-registry-ui-deployment.yaml b/clients/ui/manifests/kubeflow/model-registry-ui-deployment.yaml deleted file mode 100644 index ee036eea0..000000000 --- a/clients/ui/manifests/kubeflow/model-registry-ui-deployment.yaml +++ /dev/null @@ -1,9 +0,0 @@ -- op: add - path: /spec/template/spec/containers/0/env - value: - - name: API_URL - value: "http://model-registry-bff-service.kubeflow.svc.cluster.local:4000" - - name: MOCK_AUTH - value: "false" - - name: DEPLOYMENT_MODE - value: "integrated" \ No newline at end of file diff --git a/clients/ui/manifests/overlays/integrated/kustomization.yaml b/clients/ui/manifests/overlays/integrated/kustomization.yaml new file mode 100644 index 000000000..95429edd7 --- /dev/null +++ b/clients/ui/manifests/overlays/integrated/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../base + +patches: + - path: model-registry-ui-deployment.yaml + target: + group: apps + version: v1 + kind: Deployment + name: model-registry-ui diff --git a/clients/ui/manifests/kubeflow/model-registry-bff-deployment.yaml b/clients/ui/manifests/overlays/integrated/model-registry-ui-deployment.yaml similarity index 83% rename from clients/ui/manifests/kubeflow/model-registry-bff-deployment.yaml rename to clients/ui/manifests/overlays/integrated/model-registry-ui-deployment.yaml index b7216756d..cb2fc6832 100644 --- a/clients/ui/manifests/kubeflow/model-registry-bff-deployment.yaml +++ b/clients/ui/manifests/overlays/integrated/model-registry-ui-deployment.yaml @@ -2,3 +2,4 @@ path: /spec/template/spec/containers/0/args value: - "--standalone-mode=false" + - "--port=8080" diff --git a/clients/ui/manifests/overlays/istio/authorization-policy-bff.yaml b/clients/ui/manifests/overlays/istio/authorization-policy-bff.yaml deleted file mode 100644 index eed8522dd..000000000 --- a/clients/ui/manifests/overlays/istio/authorization-policy-bff.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: security.istio.io/v1beta1 -kind: AuthorizationPolicy -metadata: - name: model-registry-bff - labels: - app: model-registry-bff -spec: - action: ALLOW - rules: - - {} - selector: - matchLabels: - app: model-registry-bff diff --git a/clients/ui/manifests/overlays/istio/destination-rule-bff.yaml b/clients/ui/manifests/overlays/istio/destination-rule-bff.yaml deleted file mode 100644 index e149dc133..000000000 --- a/clients/ui/manifests/overlays/istio/destination-rule-bff.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: networking.istio.io/v1alpha3 -kind: DestinationRule -metadata: - name: model-registry-bff - labels: - app: model-registry-bff -spec: - host: model-registry-bff-service.kubeflow.svc.cluster.local - trafficPolicy: - tls: - mode: ISTIO_MUTUAL diff --git a/clients/ui/manifests/overlays/istio/kustomization.yaml b/clients/ui/manifests/overlays/istio/kustomization.yaml index d40a96bdd..c54986243 100644 --- a/clients/ui/manifests/overlays/istio/kustomization.yaml +++ b/clients/ui/manifests/overlays/istio/kustomization.yaml @@ -2,16 +2,14 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- ../../base +- ../integrated - virtual-service.yaml - destination-rule-ui.yaml -- destination-rule-bff.yaml - authorization-policy-ui.yaml -- authorization-policy-bff.yaml -patchesJson6902: +patches: - path: model-registry-ui-service.yaml target: version: v1 kind: Service - name: model-registry-ui-service \ No newline at end of file + name: model-registry-ui-service diff --git a/clients/ui/manifests/overlays/standalone/kubeflow-dashboard-rbac.yaml b/clients/ui/manifests/overlays/standalone/kubeflow-dashboard-rbac.yaml new file mode 100644 index 000000000..9bfca02c7 --- /dev/null +++ b/clients/ui/manifests/overlays/standalone/kubeflow-dashboard-rbac.yaml @@ -0,0 +1,39 @@ +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: model-registry-ui-namespaces-reader +rules: +- apiGroups: + - '' + resources: + - namespaces + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: model-registry-ui-namespaces-reader-binding +subjects: +- kind: ServiceAccount + name: model-registry-ui +roleRef: + kind: ClusterRole + name: model-registry-ui-namespaces-reader + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: service-access-cluster-binding +subjects: + - kind: User + name: user@example.com + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io diff --git a/clients/ui/manifests/overlays/standalone/kustomization.yaml b/clients/ui/manifests/overlays/standalone/kustomization.yaml new file mode 100644 index 000000000..c32312d1b --- /dev/null +++ b/clients/ui/manifests/overlays/standalone/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../../base +- kubeflow-dashboard-rbac.yaml + +patches: +- path: model-registry-ui-deployment.yaml + target: + group: apps + kind: Deployment + name: model-registry-ui + version: v1 diff --git a/clients/ui/manifests/standalone/model-registry-bff-deployment.yaml b/clients/ui/manifests/overlays/standalone/model-registry-ui-deployment.yaml similarity index 82% rename from clients/ui/manifests/standalone/model-registry-bff-deployment.yaml rename to clients/ui/manifests/overlays/standalone/model-registry-ui-deployment.yaml index 38b5569a8..0d41529c8 100644 --- a/clients/ui/manifests/standalone/model-registry-bff-deployment.yaml +++ b/clients/ui/manifests/overlays/standalone/model-registry-ui-deployment.yaml @@ -2,3 +2,4 @@ path: /spec/template/spec/containers/0/args value: - "--standalone-mode=true" + - "--port=8080" diff --git a/clients/ui/manifests/standalone/kubeflow-dashboard-rbac.yaml b/clients/ui/manifests/standalone/kubeflow-dashboard-rbac.yaml deleted file mode 100644 index e81048d8e..000000000 --- a/clients/ui/manifests/standalone/kubeflow-dashboard-rbac.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: service-access-cluster-role -rules: - - apiGroups: [""] - resources: ["services"] - verbs: ["get", "list"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: service-access-cluster-binding -subjects: - - kind: User - name: user@example.com - apiGroup: rbac.authorization.k8s.io -roleRef: - kind: ClusterRole - name: service-access-cluster-role - apiGroup: rbac.authorization.k8s.io diff --git a/clients/ui/manifests/standalone/kustomization.yaml b/clients/ui/manifests/standalone/kustomization.yaml deleted file mode 100644 index f60c1fa46..000000000 --- a/clients/ui/manifests/standalone/kustomization.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - kubeflow-dashboard-rbac.yaml - -patchesJson6902: - - path: model-registry-ui-deployment.yaml - target: - group: apps - version: v1 - kind: Deployment - name: model-registry-bff-deployment - - path: model-registry-bff-deployment.yaml - target: - group: apps - version: v1 - kind: Deployment - name: model-registry-bff-deployment \ No newline at end of file diff --git a/clients/ui/manifests/standalone/model-registry-ui-deployment.yaml b/clients/ui/manifests/standalone/model-registry-ui-deployment.yaml deleted file mode 100644 index 5211d0b05..000000000 --- a/clients/ui/manifests/standalone/model-registry-ui-deployment.yaml +++ /dev/null @@ -1,9 +0,0 @@ -- op: add - path: /spec/template/spec/containers/0/env - value: - - name: API_URL - value: "http://model-registry-bff-service:4000" - - name: MOCK_AUTH - value: "true" - - name: DEPLOYMENT_MODE - value: "standalone" \ No newline at end of file diff --git a/clients/ui/scripts/deploy_kind_cluster.sh b/clients/ui/scripts/deploy_kind_cluster.sh index 57f365cf5..66d547e51 100755 --- a/clients/ui/scripts/deploy_kind_cluster.sh +++ b/clients/ui/scripts/deploy_kind_cluster.sh @@ -5,6 +5,12 @@ command -v docker >/dev/null 2>&1 || { echo >&2 "Docker is required but it's not command -v kubectl >/dev/null 2>&1 || { echo >&2 "kubectl is required but it's not installed. Aborting."; exit 1; } command -v kind >/dev/null 2>&1 || { echo >&2 "kind is required but it's not installed. Aborting."; exit 1; } +# Check if the script has rights to push an image into the registry +if ! docker push "${IMG_UI_STANDALONE}" --dry-run >/dev/null 2>&1; then + echo -e "\033[31mError: No rights to push the image to the registry ${IMG_UI_STANDALONE}, you can change the image in the env variable IMG_UI_STANDALONE\033[0m" + exit 1 +fi + if kubectl get deployment model-registry-deployment -n kubeflow >/dev/null 2>&1; then echo "Model Registry deployment already exists. Skipping to step 4." else @@ -33,28 +39,25 @@ else kubectl get pods -n kubeflow fi +# Step 4: Build Model Registry and push in standalone mode +echo "Building Model Registry UI..." +make docker-build-standalone +make docker-push-standalone + +echo "Editing kustomize image..." pushd ./manifests/base -kustomize edit set namespace kubeflow -kustomize edit set image model-registry-ui-image=${IMG_FRONTEND} -kustomize edit set image model-registry-bff-image=${IMG_BFF} +kustomize edit set image model-registry-ui-image=${IMG_UI_STANDALONE} +pushd ../overlays/standalone # Step 4: Deploy model registry UI echo "Deploying Model Registry UI..." +kustomize edit set namespace kubeflow kubectl apply -n kubeflow -k . # Wait for deployment to be available echo "Waiting Model Registry UI to be available..." kubectl wait --for=condition=available -n kubeflow deployment/model-registry-ui --timeout=1m -pushd ../user-rbac -# Step 5: Apply admin user service account in the cluster -echo "Applying admin user service account and rolebinding..." -kubectl apply -k . - -# Step 6: Generate token for admin user and display it -echo "In your browser, you will need to inject your requests with a kubeflow-userid header for authorization purposes." -echo "For example, you can use the Header Editor - https://chromewebstore.google.com/detail/eningockdidmgiojffjmkdblpjocbhgh extension in Chrome to set the kubeflow-userid header to user@example.com." - # Step 5: Port-forward the service echo "Port-forwarding Model Registry UI..." echo -e "\033[32mDashboard available in http://localhost:8080\033[0m" From 7dcb477ddcda31d8ecc13a5256995c66742d33d5 Mon Sep 17 00:00:00 2001 From: Eder Ignatowicz Date: Thu, 16 Jan 2025 15:28:17 -0500 Subject: [PATCH 08/10] feat(bff): proper support for SPA routes (#701) Signed-off-by: Eder Ignatowicz --- clients/ui/bff/internal/api/app.go | 20 ++++++++++++++++---- clients/ui/bff/internal/api/app_test.go | 9 +++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/clients/ui/bff/internal/api/app.go b/clients/ui/bff/internal/api/app.go index bc8fe0fa0..d9210a1c8 100644 --- a/clients/ui/bff/internal/api/app.go +++ b/clients/ui/bff/internal/api/app.go @@ -8,6 +8,7 @@ import ( "github.com/kubeflow/model-registry/ui/bff/internal/repositories" "log/slog" "net/http" + "path" "github.com/julienschmidt/httprouter" "github.com/kubeflow/model-registry/ui/bff/internal/mocks" @@ -117,13 +118,24 @@ func (app *App) Routes() http.Handler { // App Router appMux := http.NewServeMux() + // handler for api calls appMux.Handle("/api/v1/", apiRouter) - // file server for the frontend - staticAccessDir := http.Dir(app.config.StaticAssetsDir) - fileServer := http.FileServer(staticAccessDir) - appMux.Handle("/", fileServer) + // file server for the frontend file and SPA routes + staticDir := http.Dir(app.config.StaticAssetsDir) + fileServer := http.FileServer(staticDir) + appMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Check if the requested file exists + if _, err := staticDir.Open(r.URL.Path); err == nil { + // Serve the file if it exists + fileServer.ServeHTTP(w, r) + return + } + + // Fallback to index.html for SPA routes + http.ServeFile(w, r, path.Join(app.config.StaticAssetsDir, "index.html")) + }) return app.RecoverPanic(app.enableCORS(app.InjectUserHeaders(appMux))) } diff --git a/clients/ui/bff/internal/api/app_test.go b/clients/ui/bff/internal/api/app_test.go index c03afb0b5..bce813dd1 100644 --- a/clients/ui/bff/internal/api/app_test.go +++ b/clients/ui/bff/internal/api/app_test.go @@ -62,12 +62,17 @@ var _ = Describe("Static File serving Test", func() { Expect(string(body)).To(ContainSubstring("BFF Stub Subfolder Page")) }) - It("should return 404 for a non-existent static file", func() { + It("should return index.html for a non-existent static file", func() { resp, err := client.Get(server.URL + "/non-existent.html") Expect(err).NotTo(HaveOccurred()) defer resp.Body.Close() - Expect(resp.StatusCode).To(Equal(http.StatusNotFound)) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + + body, err := io.ReadAll(resp.Body) + Expect(err).NotTo(HaveOccurred()) + //BFF Stub page is the context of index.html + Expect(string(body)).To(ContainSubstring("BFF Stub Page")) }) }) From a7af392dc3edeaa2fa40ae4390bec06a941c5df3 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:21:37 -0500 Subject: [PATCH 09/10] Fix(frontend): Nav and Page Spacing (#704) Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> update margin-top to 24px --- clients/ui/frontend/src/app/App.tsx | 5 ++++- clients/ui/frontend/src/shared/style/MUI-theme.scss | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/clients/ui/frontend/src/app/App.tsx b/clients/ui/frontend/src/app/App.tsx index 625ae2c04..47a9bd40a 100644 --- a/clients/ui/frontend/src/app/App.tsx +++ b/clients/ui/frontend/src/app/App.tsx @@ -7,6 +7,7 @@ import { Button, Page, PageSection, + PageSidebar, Spinner, Stack, StackItem, @@ -95,6 +96,8 @@ const App: React.FC = () => { ); } + const sidebar = ; + // Waiting on the API to finish const loading = !configLoaded || !userSettings || !configSettings || !contextValue || !namespacesLoaded; @@ -120,7 +123,7 @@ const App: React.FC = () => { ) } isManagedSidebar={isStandalone()} - sidebar={isStandalone() ? : ''} + sidebar={isStandalone() ? : sidebar} > diff --git a/clients/ui/frontend/src/shared/style/MUI-theme.scss b/clients/ui/frontend/src/shared/style/MUI-theme.scss index 5596c0c25..ac651aeea 100644 --- a/clients/ui/frontend/src/shared/style/MUI-theme.scss +++ b/clients/ui/frontend/src/shared/style/MUI-theme.scss @@ -59,7 +59,7 @@ --kf-central-primary-background-color: #0a3b71; --kf-central-sidebar-default-color: #ffffff90; --kf-central-app-drawer-width: 240px; - --kf-central-app-bar-height: 64px; + --kf-central-app-bar-height: 24px; // Table --mui-table__button--BackgroundColor: none; From f9f78c34823dca087ca75c33199df0ad471873fc Mon Sep 17 00:00:00 2001 From: Eder Ignatowicz Date: Thu, 16 Jan 2025 17:39:41 -0500 Subject: [PATCH 10/10] feat(bff): create endpoint to list all model versions (#707) Signed-off-by: Eder Ignatowicz --- clients/ui/bff/README.md | 4 +++ clients/ui/bff/internal/api/app.go | 3 ++- .../internal/api/model_versions_handler.go | 24 +++++++++++++++++ .../api/model_versions_handler_test.go | 12 +++++++++ .../internal/api/registered_models_handler.go | 2 +- .../mocks/model_registry_client_mock.go | 7 ++++- .../internal/repositories/model_version.go | 16 ++++++++++++ .../repositories/model_version_test.go | 26 +++++++++++++++++++ .../internal/repositories/registered_model.go | 4 +-- .../repositories/registered_model_test.go | 6 ++--- 10 files changed, 96 insertions(+), 8 deletions(-) diff --git a/clients/ui/bff/README.md b/clients/ui/bff/README.md index 3886245e1..c6adf2512 100644 --- a/clients/ui/bff/README.md +++ b/clients/ui/bff/README.md @@ -126,6 +126,10 @@ curl -i -H "kubeflow-userid: user@example.com" -X PATCH "http://localhost:4000/a }}' ``` ``` +# GET /api/v1/model_registry/{model_registry_id}/model_versions +curl -i -H "kubeflow-userid: user@example.com" "http://localhost:4000/api/v1/model_registry/model-registry/model_versions?namespace=kubeflow" +``` +``` # GET /api/v1/model_registry/{model_registry_id}/model_versions/{model_version_id} curl -i -H "kubeflow-userid: user@example.com" "http://localhost:4000/api/v1/model_registry/model-registry/model_versions/1?namespace=kubeflow" ``` diff --git a/clients/ui/bff/internal/api/app.go b/clients/ui/bff/internal/api/app.go index d9210a1c8..129b41c8d 100644 --- a/clients/ui/bff/internal/api/app.go +++ b/clients/ui/bff/internal/api/app.go @@ -101,8 +101,9 @@ func (app *App) Routes() http.Handler { apiRouter.PATCH(RegisteredModelPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.UpdateRegisteredModelHandler)))) apiRouter.GET(RegisteredModelVersionsPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetAllModelVersionsForRegisteredModelHandler)))) apiRouter.POST(RegisteredModelVersionsPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.CreateModelVersionForRegisteredModelHandler)))) - apiRouter.GET(ModelVersionPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient((app.GetModelVersionHandler))))) apiRouter.POST(ModelVersionListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.CreateModelVersionHandler)))) + apiRouter.GET(ModelVersionListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetAllModelVersionHandler)))) + apiRouter.GET(ModelVersionPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetModelVersionHandler)))) apiRouter.PATCH(ModelVersionPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.UpdateModelVersionHandler)))) apiRouter.GET(ModelVersionArtifactListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.GetAllModelArtifactsByModelVersionHandler)))) apiRouter.POST(ModelVersionArtifactListPath, app.AttachNamespace(app.PerformSARonSpecificService(app.AttachRESTClient(app.CreateModelArtifactByModelVersionHandler)))) diff --git a/clients/ui/bff/internal/api/model_versions_handler.go b/clients/ui/bff/internal/api/model_versions_handler.go index a945d0492..7c74377bf 100644 --- a/clients/ui/bff/internal/api/model_versions_handler.go +++ b/clients/ui/bff/internal/api/model_versions_handler.go @@ -18,6 +18,30 @@ type ModelVersionUpdateEnvelope Envelope[*openapi.ModelVersionUpdate, None] 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) + if !ok { + app.serverErrorResponse(w, r, errors.New("REST client not found")) + return + } + + versionList, err := app.repositories.ModelRegistryClient.GetAllModelVersions(client) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + + responseBody := ModelVersionListEnvelope{ + Data: versionList, + } + + err = app.WriteJSON(w, http.StatusOK, responseBody, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + } + +} + func (app *App) GetModelVersionHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { client, ok := r.Context().Value(ModelRegistryHttpClientKey).(integrations.HTTPClientInterface) if !ok { diff --git a/clients/ui/bff/internal/api/model_versions_handler_test.go b/clients/ui/bff/internal/api/model_versions_handler_test.go index cce4cf022..c8cebc2a0 100644 --- a/clients/ui/bff/internal/api/model_versions_handler_test.go +++ b/clients/ui/bff/internal/api/model_versions_handler_test.go @@ -11,6 +11,18 @@ import ( var _ = Describe("TestGetModelVersionHandler", func() { Context("testing Model Version Handler", Ordered, func() { + It("should retrieve all model versions", func() { + By("fetching all model versions") + data := mocks.GetModelVersionListMock() + expected := ModelVersionListEnvelope{Data: &data} + actual, rs, err := setupApiTest[ModelVersionListEnvelope](http.MethodGet, "/api/v1/model_registry/model-registry/model_versions?namespace=kubeflow", nil, k8sClient, mocks.KubeflowUserIDHeaderValue, "kubeflow") + Expect(err).NotTo(HaveOccurred()) + By("should match the expected model versions") + Expect(rs.StatusCode).To(Equal(http.StatusOK)) + Expect(actual.Data.Size).To(Equal(expected.Data.Size)) + Expect(actual.Data.Items).To(Equal(expected.Data.Items)) + }) + It("should retrieve a model version", func() { By("fetching a model version") data := mocks.GetModelVersionMocks()[0] diff --git a/clients/ui/bff/internal/api/registered_models_handler.go b/clients/ui/bff/internal/api/registered_models_handler.go index 98daef1cd..7f781f69b 100644 --- a/clients/ui/bff/internal/api/registered_models_handler.go +++ b/clients/ui/bff/internal/api/registered_models_handler.go @@ -177,7 +177,7 @@ func (app *App) GetAllModelVersionsForRegisteredModelHandler(w http.ResponseWrit return } - versionList, err := app.repositories.ModelRegistryClient.GetAllModelVersions(client, ps.ByName(RegisteredModelId), r.URL.Query()) + versionList, err := app.repositories.ModelRegistryClient.GetAllModelVersionsForRegisteredModel(client, ps.ByName(RegisteredModelId), r.URL.Query()) if err != nil { app.serverErrorResponse(w, r, err) diff --git a/clients/ui/bff/internal/mocks/model_registry_client_mock.go b/clients/ui/bff/internal/mocks/model_registry_client_mock.go index 9a35e331e..d9fcfd48a 100644 --- a/clients/ui/bff/internal/mocks/model_registry_client_mock.go +++ b/clients/ui/bff/internal/mocks/model_registry_client_mock.go @@ -41,6 +41,11 @@ func (m *ModelRegistryClientMock) UpdateRegisteredModel(_ integrations.HTTPClien return &mockData, nil } +func (m *ModelRegistryClientMock) GetAllModelVersions(_ integrations.HTTPClientInterface) (*openapi.ModelVersionList, error) { + mockData := GetModelVersionListMock() + return &mockData, nil +} + func (m *ModelRegistryClientMock) GetModelVersion(_ integrations.HTTPClientInterface, id string) (*openapi.ModelVersion, error) { if id == "3" { mockData := GetModelVersionMocks()[2] @@ -61,7 +66,7 @@ func (m *ModelRegistryClientMock) UpdateModelVersion(_ integrations.HTTPClientIn return &mockData, nil } -func (m *ModelRegistryClientMock) GetAllModelVersions(_ integrations.HTTPClientInterface, _ string, _ url.Values) (*openapi.ModelVersionList, error) { +func (m *ModelRegistryClientMock) GetAllModelVersionsForRegisteredModel(_ integrations.HTTPClientInterface, _ string, _ url.Values) (*openapi.ModelVersionList, error) { mockData := GetModelVersionListMock() return &mockData, nil } diff --git a/clients/ui/bff/internal/repositories/model_version.go b/clients/ui/bff/internal/repositories/model_version.go index 526b187b4..b5a30278d 100644 --- a/clients/ui/bff/internal/repositories/model_version.go +++ b/clients/ui/bff/internal/repositories/model_version.go @@ -13,6 +13,7 @@ const modelVersionPath = "/model_versions" const artifactsByModelVersionPath = "/artifacts" type ModelVersionInterface interface { + GetAllModelVersions(client integrations.HTTPClientInterface) (*openapi.ModelVersionList, error) GetModelVersion(client integrations.HTTPClientInterface, id string) (*openapi.ModelVersion, error) CreateModelVersion(client integrations.HTTPClientInterface, jsonData []byte) (*openapi.ModelVersion, error) UpdateModelVersion(client integrations.HTTPClientInterface, id string, jsonData []byte) (*openapi.ModelVersion, error) @@ -24,6 +25,21 @@ type ModelVersion struct { ModelVersionInterface } +func (v ModelVersion) GetAllModelVersions(client integrations.HTTPClientInterface) (*openapi.ModelVersionList, error) { + response, err := client.GET(modelVersionPath) + + if err != nil { + return nil, fmt.Errorf("error fetching model versions: %w", err) + } + + var models openapi.ModelVersionList + if err := json.Unmarshal(response, &models); err != nil { + return nil, fmt.Errorf("error decoding response data: %w", err) + } + + return &models, nil +} + func (v ModelVersion) GetModelVersion(client integrations.HTTPClientInterface, id string) (*openapi.ModelVersion, error) { path, err := url.JoinPath(modelVersionPath, id) if err != nil { diff --git a/clients/ui/bff/internal/repositories/model_version_test.go b/clients/ui/bff/internal/repositories/model_version_test.go index e5c389ff7..40d0522ff 100644 --- a/clients/ui/bff/internal/repositories/model_version_test.go +++ b/clients/ui/bff/internal/repositories/model_version_test.go @@ -38,6 +38,32 @@ func TestGetModelVersion(t *testing.T) { mockClient.AssertExpectations(t) } +func TestGetAllModelVersions(t *testing.T) { + _ = gofakeit.Seed(0) + + expected := mocks.GenerateMockModelVersionList() + + mockData, err := json.Marshal(expected) + assert.NoError(t, err) + + modelVersion := ModelVersion{} + + mockClient := new(mocks.MockHTTPClient) + mockClient.On("GET", modelVersionPath).Return(mockData, nil) + + actual, err := modelVersion.GetAllModelVersions(mockClient) + assert.NoError(t, err) + assert.NotNil(t, actual) + assert.NoError(t, err) + assert.NotNil(t, actual) + assert.Equal(t, expected.NextPageToken, actual.NextPageToken) + assert.Equal(t, expected.PageSize, actual.PageSize) + assert.Equal(t, expected.Size, actual.Size) + assert.Equal(t, len(expected.Items), len(actual.Items)) + + mockClient.AssertExpectations(t) +} + func TestCreateModelVersion(t *testing.T) { _ = gofakeit.Seed(0) diff --git a/clients/ui/bff/internal/repositories/registered_model.go b/clients/ui/bff/internal/repositories/registered_model.go index e93552726..ba08cfe27 100644 --- a/clients/ui/bff/internal/repositories/registered_model.go +++ b/clients/ui/bff/internal/repositories/registered_model.go @@ -17,7 +17,7 @@ type RegisteredModelInterface interface { CreateRegisteredModel(client integrations.HTTPClientInterface, jsonData []byte) (*openapi.RegisteredModel, error) GetRegisteredModel(client integrations.HTTPClientInterface, id string) (*openapi.RegisteredModel, error) UpdateRegisteredModel(client integrations.HTTPClientInterface, id string, jsonData []byte) (*openapi.RegisteredModel, error) - GetAllModelVersions(client integrations.HTTPClientInterface, id string, pageValues url.Values) (*openapi.ModelVersionList, error) + GetAllModelVersionsForRegisteredModel(client integrations.HTTPClientInterface, id string, pageValues url.Values) (*openapi.ModelVersionList, error) CreateModelVersionForRegisteredModel(client integrations.HTTPClientInterface, id string, jsonData []byte) (*openapi.ModelVersion, error) } @@ -94,7 +94,7 @@ func (m RegisteredModel) UpdateRegisteredModel(client integrations.HTTPClientInt return &model, nil } -func (m RegisteredModel) GetAllModelVersions(client integrations.HTTPClientInterface, id string, pageValues url.Values) (*openapi.ModelVersionList, error) { +func (m RegisteredModel) GetAllModelVersionsForRegisteredModel(client integrations.HTTPClientInterface, id string, pageValues url.Values) (*openapi.ModelVersionList, error) { path, err := url.JoinPath(registeredModelPath, id, versionsPath) if err != nil { diff --git a/clients/ui/bff/internal/repositories/registered_model_test.go b/clients/ui/bff/internal/repositories/registered_model_test.go index d94aed1db..78aa0d3e1 100644 --- a/clients/ui/bff/internal/repositories/registered_model_test.go +++ b/clients/ui/bff/internal/repositories/registered_model_test.go @@ -134,7 +134,7 @@ func TestUpdateRegisteredModel(t *testing.T) { mockClient.AssertExpectations(t) } -func TestGetAllModelVersions(t *testing.T) { +func TestGetAllModelVersionsByRegisteredModel(t *testing.T) { _ = gofakeit.Seed(0) expected := mocks.GenerateMockModelVersionList() @@ -149,7 +149,7 @@ func TestGetAllModelVersions(t *testing.T) { assert.NoError(t, err) mockClient.On("GET", path).Return(mockData, nil) - actual, err := registeredModel.GetAllModelVersions(mockClient, "1", nil) + actual, err := registeredModel.GetAllModelVersionsForRegisteredModel(mockClient, "1", nil) assert.NoError(t, err) assert.NotNil(t, actual) assert.NoError(t, err) @@ -180,7 +180,7 @@ func TestGetAllModelVersionsWithPageParams(t *testing.T) { mockClient.On("GET", reqUrl).Return(mockData, nil) - actual, err := registeredModel.GetAllModelVersions(mockClient, "1", pageValues) + actual, err := registeredModel.GetAllModelVersionsForRegisteredModel(mockClient, "1", pageValues) assert.NoError(t, err) assert.NotNil(t, actual)