diff --git a/frontend/src/__mocks__/mock-custom-serving-runtime-add.yaml b/frontend/src/__mocks__/mock-custom-serving-runtime-add.yaml new file mode 100644 index 0000000000..a5380c1086 --- /dev/null +++ b/frontend/src/__mocks__/mock-custom-serving-runtime-add.yaml @@ -0,0 +1,45 @@ +apiVersion: serving.kserve.io/v1alpha1 +kind: ServingRuntime +metadata: + name: template-new + annotations: + openshift.io/display-name: 'New OVMS Server' + labels: + opendatahub.io/dashboard: 'true' +spec: + builtInAdapter: + memBufferBytes: 134217728 + modelLoadingTimeoutMillis: 90000 + runtimeManagementPort: 8888 + serverType: ovms + containers: + - args: + - '--port=8001' + - '--rest_port=8888' + - '--config_path=/models/model_config_list.json' + - '--file_system_poll_wait_seconds=0' + - '--grpc_bind_address=127.0.0.1' + - '--rest_bind_address=127.0.0.1' + image: >- + quay.io/opendatahub/openvino_model_server@sha256:c89f76386bc8b59f0748cf173868e5beef21ac7d2f78dada69089c4d37c44116 + name: ovms + resources: + limits: + cpu: '0' + memory: 0Gi + requests: + cpu: '0' + memory: 0Gi + grpcDataEndpoint: 'port:8001' + grpcEndpoint: 'port:8085' + multiModel: true + protocolVersions: + - grpc-v1 + replicas: 1 + supportedModelFormats: + - autoSelect: true + name: openvino_ir + version: opset1 + - autoSelect: true + name: onnx + version: '1' diff --git a/frontend/src/__mocks__/mock-custom-serving-runtime-edit.yaml b/frontend/src/__mocks__/mock-custom-serving-runtime-edit.yaml new file mode 100644 index 0000000000..7ef9382e8d --- /dev/null +++ b/frontend/src/__mocks__/mock-custom-serving-runtime-edit.yaml @@ -0,0 +1,44 @@ +apiVersion: serving.kserve.io/v1alpha1 +kind: ServingRuntime +metadata: + name: template-1 + annotations: + openshift.io/display-name: Updated Multi Platform + labels: + opendatahub.io/dashboard: 'true' +spec: + builtInAdapter: + memBufferBytes: 134217728 + modelLoadingTimeoutMillis: 90000 + runtimeManagementPort: 8888 + serverType: ovms + containers: + - args: + - --port=8001 + - --rest_port=8888 + - --config_path=/models/model_config_list.json + - --file_system_poll_wait_seconds=0 + - --grpc_bind_address=127.0.0.1 + - --rest_bind_address=127.0.0.1 + - --target_device=NVIDIA + image: quay.io/modh/openvino-model-server@sha256:c89f76386bc8b59f0748cf173868e5beef21ac7d2f78dada69089c4d37c44116 + name: ovms + resources: + limits: + cpu: '0' + memory: 0Gi + requests: + cpu: '0' + memory: 0Gi + grpcDataEndpoint: port:8001 + grpcEndpoint: port:8085 + multiModel: true + protocolVersions: + - grpc-v1 + supportedModelFormats: + - autoSelect: true + name: openvino_ir + version: opset1 + - autoSelect: true + name: onnx + version: '1' diff --git a/frontend/src/__tests__/cypress/cypress/e2e/customServingRuntimes/CustomServingRuntimes.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/customServingRuntimes/CustomServingRuntimes.cy.ts index 51f406096a..771caca3d3 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/customServingRuntimes/CustomServingRuntimes.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/customServingRuntimes/CustomServingRuntimes.cy.ts @@ -5,37 +5,40 @@ import { mockServingRuntimeTemplateK8sResource } from '~/__mocks__/mockServingRu import { mockStatus } from '~/__mocks__/mockStatus'; import { servingRuntimes } from '~/__tests__/cypress/cypress/pages/servingRuntimes'; import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; +import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; + +const addfilePath = '../../__mocks__/mock-custom-serving-runtime-add.yaml'; +const editfilePath = '../../__mocks__/mock-custom-serving-runtime-edit.yaml'; + +const initialMock = [ + mockServingRuntimeTemplateK8sResource({ + name: 'template-1', + displayName: 'Multi Platform', + platforms: [ServingRuntimePlatform.SINGLE], + }), + mockServingRuntimeTemplateK8sResource({ + name: 'template-2', + displayName: 'Caikit', + platforms: [ServingRuntimePlatform.SINGLE], + apiProtocol: ServingRuntimeAPIProtocol.GRPC, + }), + mockServingRuntimeTemplateK8sResource({ + name: 'template-3', + displayName: 'OVMS', + platforms: [ServingRuntimePlatform.MULTI], + }), + mockServingRuntimeTemplateK8sResource({ + name: 'template-4', + displayName: 'Serving Runtime with No Annotations', + }), +]; describe('Custom serving runtimes', () => { beforeEach(() => { cy.intercept('/api/status', mockStatus()); cy.intercept('/api/config', mockDashboardConfig({})); cy.intercept('/api/dashboardConfig/opendatahub/odh-dashboard-config', mockDashboardConfig({})); - cy.intercept( - { pathname: '/api/templates/opendatahub' }, - mockK8sResourceList([ - mockServingRuntimeTemplateK8sResource({ - name: 'template-1', - displayName: 'Multi Platform', - platforms: [ServingRuntimePlatform.SINGLE], - }), - mockServingRuntimeTemplateK8sResource({ - name: 'template-2', - displayName: 'Caikit', - platforms: [ServingRuntimePlatform.SINGLE], - apiProtocol: ServingRuntimeAPIProtocol.GRPC, - }), - mockServingRuntimeTemplateK8sResource({ - name: 'template-3', - displayName: 'OVMS', - platforms: [ServingRuntimePlatform.MULTI], - }), - mockServingRuntimeTemplateK8sResource({ - name: 'template-4', - displayName: 'Serving Runtime with No Annotations', - }), - ]), - ); + cy.intercept({ pathname: '/api/templates/opendatahub' }, mockK8sResourceList(initialMock)); cy.intercept( '/api/k8s/apis/project.openshift.io/v1/projects', mockK8sResourceList([mockProjectK8sResource({})]), @@ -62,7 +65,23 @@ describe('Custom serving runtimes', () => { servingRuntimes.getRowById('template-4').shouldHaveAPIProtocol(ServingRuntimeAPIProtocol.REST); }); - it('should add a new serving runtime', () => { + it('should add a new single model serving runtime', () => { + cy.intercept( + { + method: 'POST', + pathname: '/api/servingRuntimes', + }, + mockServingRuntimeTemplateK8sResource({}).objects, + ).as('createSingleModelServingRuntime'); + + cy.intercept( + { + method: 'POST', + pathname: '/api/templates/', + }, + mockServingRuntimeTemplateK8sResource({}), + ).as('createTemplate'); + servingRuntimes.findAddButton().click(); servingRuntimes.findAppTitle().should('contain', 'Add serving runtime'); @@ -73,38 +92,238 @@ describe('Custom serving runtimes', () => { ]); servingRuntimes.findSelectServingPlatformButton().click(); - // Create with single model - servingRuntimes.findCreateButton().should('be.disabled'); - servingRuntimes.shouldSelectPlatform('Single-model serving platform'); + servingRuntimes.findSubmitButton().should('be.disabled'); + servingRuntimes.selectPlatform('Single-model serving platform'); servingRuntimes.shouldDisplayAPIProtocolValues([ ServingRuntimeAPIProtocol.REST, ServingRuntimeAPIProtocol.GRPC, ]); - servingRuntimes.shouldSelectAPIProtocol(ServingRuntimeAPIProtocol.REST); + servingRuntimes.selectAPIProtocol(ServingRuntimeAPIProtocol.REST); servingRuntimes.findStartFromScratchButton().click(); - servingRuntimes.getDashboardCodeEditor().findInput().type('test'); - servingRuntimes.findCreateButton().should('be.enabled'); - servingRuntimes.findCancelButton().click(); + servingRuntimes.uploadYaml(addfilePath); + servingRuntimes.getDashboardCodeEditor().findInput().should('not.be.empty'); + + servingRuntimes.findSubmitButton().should('be.enabled'); + servingRuntimes.findSubmitButton().click(); + + cy.wait('@createSingleModelServingRuntime').then((interception) => { + expect(interception.request.url).to.include('?dryRun=All'); + expect(interception.request.body.metadata).to.eql({ + name: 'template-new', + annotations: { 'openshift.io/display-name': 'New OVMS Server' }, + labels: { 'opendatahub.io/dashboard': 'true' }, + namespace: 'opendatahub', + }); + }); + + cy.wait('@createTemplate').then((interception) => { + expect(interception.request.body.metadata.annotations).to.eql({ + 'opendatahub.io/modelServingSupport': '["single"]', + 'opendatahub.io/apiProtocol': 'REST', + }); + expect(interception.request.body.objects[0].metadata).to.eql({ + name: 'template-new', + annotations: { 'openshift.io/display-name': 'New OVMS Server' }, + labels: { 'opendatahub.io/dashboard': 'true' }, + }); + }); + }); + + it('should add a new multi model serving runtime', () => { + cy.intercept( + { + method: 'POST', + pathname: '/api/servingRuntimes', + }, + mockServingRuntimeTemplateK8sResource({}).objects, + ).as('createMultiModelServingRuntime'); + cy.intercept( + { + method: 'POST', + pathname: '/api/templates/', + }, + mockServingRuntimeTemplateK8sResource({}), + ).as('createTemplate'); servingRuntimes.findAddButton().click(); + servingRuntimes.findAppTitle().should('contain', 'Add serving runtime'); - // Create with multi model - servingRuntimes.findCreateButton().should('be.disabled'); - servingRuntimes.shouldSelectPlatform('Multi-model serving platform'); + // Check serving runtime dropdown list + servingRuntimes.shouldDisplayServingRuntimeValues([ + 'Single-model serving platform', + 'Multi-model serving platform', + ]); + servingRuntimes.findSelectServingPlatformButton().click(); + + servingRuntimes.findSubmitButton().should('be.disabled'); + servingRuntimes.selectPlatform('Multi-model serving platform'); servingRuntimes.findSelectAPIProtocolButton().should('not.be.enabled'); servingRuntimes.findSelectAPIProtocolButton().should('include.text', 'REST'); servingRuntimes.findStartFromScratchButton().click(); - servingRuntimes.getDashboardCodeEditor().findInput().type('test'); - servingRuntimes.findCreateButton().should('be.enabled'); + servingRuntimes.uploadYaml(addfilePath); + servingRuntimes.findSubmitButton().should('be.enabled'); + servingRuntimes.findSubmitButton().click(); + + cy.wait('@createMultiModelServingRuntime').then((interception) => { + expect(interception.request.url).to.include('?dryRun=All'); + expect(interception.request.body.metadata).to.eql({ + name: 'template-new', + annotations: { 'openshift.io/display-name': 'New OVMS Server' }, + labels: { 'opendatahub.io/dashboard': 'true' }, + namespace: 'opendatahub', + }); + }); + + cy.wait('@createTemplate').then((interception) => { + expect(interception.request.body.metadata.annotations).to.eql({ + 'opendatahub.io/modelServingSupport': '["multi"]', + 'opendatahub.io/apiProtocol': 'REST', + }); + expect(interception.request.body.objects[0].metadata).to.eql({ + name: 'template-new', + annotations: { 'openshift.io/display-name': 'New OVMS Server' }, + labels: { 'opendatahub.io/dashboard': 'true' }, + }); + }); }); it('should duplicate a serving runtime', () => { + cy.intercept( + { + method: 'POST', + pathname: '/api/servingRuntimes', + }, + mockServingRuntimeTemplateK8sResource({}).objects, + ).as('duplicateServingRuntime'); + cy.intercept( + { + method: 'POST', + pathname: '/api/templates/', + }, + mockServingRuntimeTemplateK8sResource({}), + ).as('duplicateTemplate'); + + const ServingRuntimeTemplateMock = mockServingRuntimeTemplateK8sResource({ + name: 'serving-runtime-template-1', + displayName: 'Multi platform', + platforms: [ServingRuntimePlatform.SINGLE], + apiProtocol: ServingRuntimeAPIProtocol.GRPC, + }); + + cy.intercept( + { pathname: '/api/templates/opendatahub' }, + mockK8sResourceList([...initialMock, ServingRuntimeTemplateMock]), + ).as('refreshServingRuntime'); + servingRuntimes.getRowById('template-1').find().findKebabAction('Duplicate').click(); servingRuntimes.findAppTitle().should('have.text', 'Duplicate serving runtime'); + cy.url().should('include', '/addServingRuntime'); + + servingRuntimes.shouldDisplayAPIProtocolValues([ + ServingRuntimeAPIProtocol.REST, + ServingRuntimeAPIProtocol.GRPC, + ]); + servingRuntimes.selectAPIProtocol(ServingRuntimeAPIProtocol.GRPC); + servingRuntimes.findSubmitButton().should('be.enabled'); + servingRuntimes.findSubmitButton().click(); + + cy.wait('@duplicateServingRuntime').then((interception) => { + expect(interception.request.body.metadata).to.eql({ + name: 'template-1-copy', + annotations: { 'openshift.io/display-name': 'Copy of Multi Platform' }, + labels: { 'opendatahub.io/dashboard': 'true' }, + namespace: 'opendatahub', + }); + }); + + cy.wait('@duplicateTemplate').then((interception) => { + expect(interception.request.body.metadata.annotations).to.eql({ + 'opendatahub.io/modelServingSupport': '["single"]', + 'opendatahub.io/apiProtocol': 'gRPC', + }); + expect(interception.request.body.objects[0].metadata).to.eql({ + name: 'template-1-copy', + annotations: { 'openshift.io/display-name': 'Copy of Multi Platform' }, + labels: { 'opendatahub.io/dashboard': 'true' }, + }); + }); + cy.wait('@refreshServingRuntime'); + + servingRuntimes + .getRowById('serving-runtime-template-1') + .shouldHaveAPIProtocol(ServingRuntimeAPIProtocol.GRPC); }); it('should edit a serving runtime', () => { + cy.intercept( + { + method: 'POST', + pathname: '/api/servingRuntimes', + }, + { + delay: 500, + body: mockServingRuntimeTemplateK8sResource({}).objects, + }, + ).as('editServingRuntime'); + cy.intercept( + { + method: 'PATCH', + pathname: '/api/templates/opendatahub/template-1', + }, + mockServingRuntimeTemplateK8sResource({}), + ).as('editTemplate'); + servingRuntimes.getRowById('template-1').find().findKebabAction('Edit').click(); - servingRuntimes.findAppTitle().should('have.text', 'Edit Multi Platform'); + servingRuntimes.findAppTitle().should('contain', 'Edit Multi Platform'); + cy.url().should('include', '/editServingRuntime/template-1'); + servingRuntimes.findSubmitButton().should('be.disabled'); + servingRuntimes.uploadYaml(editfilePath); + servingRuntimes.findSubmitButton().click(); + + cy.wait('@editServingRuntime').then((interception) => { + expect(interception.request.body.metadata).to.eql({ + name: 'template-1', + annotations: { 'openshift.io/display-name': 'Updated Multi Platform' }, + labels: { 'opendatahub.io/dashboard': 'true' }, + namespace: 'opendatahub', + }); + }); + + cy.wait('@editTemplate').then((interception) => { + expect(interception.request.body[0].value.metadata).to.eql({ + name: 'template-1', + annotations: { 'openshift.io/display-name': 'Updated Multi Platform' }, + labels: { 'opendatahub.io/dashboard': 'true' }, + }); + expect(interception.request.body[1]).to.eql({ + op: 'replace', + path: '/metadata/annotations/opendatahub.io~1modelServingSupport', + value: '["single"]', + }); + expect(interception.request.body[2]).to.eql({ + op: 'replace', + path: '/metadata/annotations/opendatahub.io~1apiProtocol', + value: 'REST', + }); + }); + }); + + it('delete serving runtime', () => { + cy.intercept( + { + method: 'DELETE', + pathname: '/api/templates/opendatahub/template-1', + }, + {}, + ).as('deleteServingRuntime'); + + servingRuntimes.getRowById('template-1').find().findKebabAction('Delete').click(); + deleteModal.findSubmitButton().should('be.disabled'); + + // test delete form is enabled after filling out required fields + deleteModal.findInput().type('Multi Platform'); + deleteModal.findSubmitButton().should('be.enabled').click(); + + cy.wait('@deleteServingRuntime'); }); }); diff --git a/frontend/src/__tests__/cypress/cypress/pages/components/DashboardCodeEditor.ts b/frontend/src/__tests__/cypress/cypress/pages/components/DashboardCodeEditor.ts index 2d1ac19841..5ee71c099e 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/components/DashboardCodeEditor.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/components/DashboardCodeEditor.ts @@ -4,4 +4,8 @@ export class DashboardCodeEditor extends Contextual { findInput(): Cypress.Chainable> { return this.find().find('.view-lines.monaco-mouse-cursor-text'); } + + findUpload(): Cypress.Chainable> { + return this.find().find('.pf-v5-c-code-editor__main input[type="file"]'); + } } diff --git a/frontend/src/__tests__/cypress/cypress/pages/servingRuntimes.ts b/frontend/src/__tests__/cypress/cypress/pages/servingRuntimes.ts index 715e93fd94..5079e2294b 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/servingRuntimes.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/servingRuntimes.ts @@ -69,7 +69,7 @@ class ServingRuntimes { return cy.findByRole('button', { name: 'Start from scratch' }); } - findCreateButton() { + findSubmitButton() { return cy.findByTestId('create-button'); } @@ -97,15 +97,19 @@ class ServingRuntimes { return this; } - shouldSelectPlatform(value: string) { + selectPlatform(value: string) { this.findSelectServingPlatformButton().click(); cy.findByRole('menuitem', { name: value }).click(); } - shouldSelectAPIProtocol(value: string) { + selectAPIProtocol(value: string) { cy.findByRole('menuitem', { name: value }).click(); } + uploadYaml(filePath: string) { + this.getDashboardCodeEditor().findUpload().selectFile([filePath], { force: true }); + } + getDashboardCodeEditor() { return new DashboardCodeEditor(() => cy.findByTestId('dashboard-code-editor')); }