From c13fdc6254af2efa276dd882371b140335e752a4 Mon Sep 17 00:00:00 2001 From: Alex Creasy Date: Wed, 8 May 2024 14:08:38 +0100 Subject: [PATCH] [RHOAIENG-6641] Backport 2.9 SSRF fixes to 2.8.x (cherry picked from commit 4cae87820e837d3e38489a89983a3f04a83c192d) Signed-off-by: Alex Creasy --- backend/package-lock.json | 85 +++++++++++- backend/package.json | 1 + backend/src/routes/api/k8s/pass-through.ts | 2 +- backend/src/routes/api/proxy/index.ts | 77 ----------- .../src/routes/api/service/pipelines/index.ts | 23 ++++ .../src/routes/api/service/trustyai/index.ts | 23 ++++ backend/src/types.ts | 27 +++- backend/src/utils/proxy.ts | 102 ++++++++++++++ .../e2e/modelServing/ModelMetrics.cy.ts | 8 +- .../e2e/pipelines/PipelineCreateRuns.cy.ts | 58 ++++---- .../e2e/pipelines/PipelineDeleteRuns.cy.ts | 125 ++++++------------ .../cypress/e2e/pipelines/Pipelines.cy.ts | 90 +++++++------ .../cypress/e2e/pipelines/PipelinesList.cy.ts | 4 +- .../e2e/pipelines/PipelinesTopology.cy.ts | 33 +++-- .../cypress/e2e/projects/projectDetails.cy.ts | 4 +- .../cypress/pages/pipelines/cloneRunPage.ts | 27 ++-- .../cypress/pages/pipelines/createRunPage.ts | 19 ++- .../pages/pipelines/pipelineImportModal.ts | 4 +- .../pages/pipelines/pipelineRunTable.ts | 12 +- .../pipelines/pipelineVersionImportModal.ts | 4 +- .../cypress/pages/pipelines/pipelinesTable.ts | 10 +- frontend/src/api/__tests__/proxyUtils.spec.ts | 81 ++++++------ frontend/src/api/proxyUtils.ts | 59 +++++++-- .../pipelines/context/PipelinesContext.tsx | 7 +- .../trustyai/context/TrustyAIContext.tsx | 27 ++-- .../concepts/trustyai/useTrustyAIAPIRoute.ts | 57 -------- 26 files changed, 546 insertions(+), 423 deletions(-) delete mode 100644 backend/src/routes/api/proxy/index.ts create mode 100644 backend/src/routes/api/service/pipelines/index.ts create mode 100644 backend/src/routes/api/service/trustyai/index.ts create mode 100644 backend/src/utils/proxy.ts delete mode 100644 frontend/src/concepts/trustyai/useTrustyAIAPIRoute.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index b33afeefd1..76daec8898 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@fastify/accepts": "^4.3.0", "@fastify/autoload": "^5.7.1", + "@fastify/http-proxy": "^9.4.0", "@fastify/sensible": "^5.2.0", "@fastify/static": "^6.10.2", "@fastify/websocket": "^8.2.0", @@ -680,6 +681,14 @@ "pkg-up": "^3.1.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@fastify/deepmerge": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-1.3.0.tgz", @@ -698,6 +707,51 @@ "fast-json-stringify": "^5.7.0" } }, + "node_modules/@fastify/http-proxy": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@fastify/http-proxy/-/http-proxy-9.5.0.tgz", + "integrity": "sha512-1iqIdV10d5k9YtfHq9ylX5zt1NiM50fG+rIX40qt00R694sqWso3ukyTFZVk33SDoSiBW8roB7n11RUVUoN+Ag==", + "dependencies": { + "@fastify/reply-from": "^9.0.0", + "fast-querystring": "^1.1.2", + "fastify-plugin": "^4.5.0", + "ws": "^8.4.2" + } + }, + "node_modules/@fastify/http-proxy/node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@fastify/reply-from": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@fastify/reply-from/-/reply-from-9.8.0.tgz", + "integrity": "sha512-bPNVaFhEeNI0Lyl6404YZaPFokudCplidE3QoOcr78yOy6H9sYw97p5KPYvY/NJNUHfFtvxOaSAHnK+YSiv/Mg==", + "dependencies": { + "@fastify/error": "^3.0.0", + "end-of-stream": "^1.4.4", + "fast-content-type-parse": "^1.1.0", + "fast-querystring": "^1.0.0", + "fastify-plugin": "^4.0.0", + "toad-cache": "^3.7.0", + "undici": "^5.19.1" + } + }, "node_modules/@fastify/send": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.0.1.tgz", @@ -4314,9 +4368,9 @@ ] }, "node_modules/fast-content-type-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.0.0.tgz", - "integrity": "sha512-Xbc4XcysUXcsP5aHUU7Nq3OwvHq97C+WnbkeIefpeYLX+ryzFJlU6OStFJhs6Ol0LkUGpcK+wL0JwfM+FCU5IA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" }, "node_modules/fast-copy": { "version": "3.0.1", @@ -4401,9 +4455,9 @@ "devOptional": true }, "node_modules/fast-querystring": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.1.tgz", - "integrity": "sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", "dependencies": { "fast-decode-uri-component": "^1.0.1" } @@ -9859,6 +9913,14 @@ "node": ">=8.0" } }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "engines": { + "node": ">=12" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -10178,6 +10240,17 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", diff --git a/backend/package.json b/backend/package.json index 4042ecefcb..02a7ef6d27 100644 --- a/backend/package.json +++ b/backend/package.json @@ -39,6 +39,7 @@ "dependencies": { "@fastify/accepts": "^4.3.0", "@fastify/autoload": "^5.7.1", + "@fastify/http-proxy": "^9.4.0", "@fastify/sensible": "^5.2.0", "@fastify/static": "^6.10.2", "@fastify/websocket": "^8.2.0", diff --git a/backend/src/routes/api/k8s/pass-through.ts b/backend/src/routes/api/k8s/pass-through.ts index e985342dc3..9ca7b1e9d2 100644 --- a/backend/src/routes/api/k8s/pass-through.ts +++ b/backend/src/routes/api/k8s/pass-through.ts @@ -9,7 +9,7 @@ import { proxyCall, ProxyError, ProxyErrorType } from '../../../utils/httpUtils' export type PassThroughData = { method: string; - requestData: string; + requestData?: string; url: string; }; diff --git a/backend/src/routes/api/proxy/index.ts b/backend/src/routes/api/proxy/index.ts deleted file mode 100644 index e632385d0c..0000000000 --- a/backend/src/routes/api/proxy/index.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { FastifyReply } from 'fastify'; -import { KubeFastifyInstance, OauthFastifyRequest } from '../../../types'; -import { proxyCall } from '../../../utils/httpUtils'; -import { logRequestDetails } from '../../../utils/fileUtils'; -import { Buffer } from 'buffer'; - -export default async (fastify: KubeFastifyInstance): Promise => { - fastify.post( - '/*', - ( - req: OauthFastifyRequest<{ - Params: { '*': string }; - Body: { - method: string; - host: string; - data?: Record; - fileContents?: string; - queryParams?: Record; - }; - }>, - reply: FastifyReply, - ) => { - logRequestDetails(fastify, req); - - const { method, host, fileContents, data = {}, queryParams = {} } = req.body; - - let requestData: string | Buffer; - let contentType: string | undefined; - if (fileContents) { - fastify.log.info('File upload'); - const boundaryBlock = 'xxxxxxxxxx'; - - let prefixHeaders = ''; - prefixHeaders += '--' + boundaryBlock + '\r\n'; - - // TODO: Support non yaml files - prefixHeaders += - 'Content-Disposition: form-data; name="uploadfile"; filename="uploadedFile.yml"\r\n'; - prefixHeaders += 'Content-Type:application/x-yaml\r\n\r\n'; - - requestData = Buffer.concat([ - Buffer.from(prefixHeaders, 'utf8'), - Buffer.from(fileContents, 'binary'), - Buffer.from('\r\n--' + boundaryBlock + '--\r\n', 'utf8'), - ]); - contentType = `multipart/form-data; boundary=${boundaryBlock}`; - } else { - requestData = JSON.stringify(data); - } - - const queryParamString = Object.keys(queryParams) - .filter((key) => queryParams[key] !== undefined) - .map((key) => `${key}=${queryParams[key]}`) - .join('&'); - - const path = req.params['*']; - const url = `${host}/${path}${queryParamString ? `?${queryParamString}` : ''}`; - - return proxyCall(fastify, req, { - method, - url, - overrideContentType: contentType, - requestData, - }) - .then(([rawData]) => rawData) - .catch((error) => { - if (error.code && error.response) { - const { code, response } = error; - reply.code(code); - reply.send(response); - } else { - throw error; - } - }); - }, - ); -}; diff --git a/backend/src/routes/api/service/pipelines/index.ts b/backend/src/routes/api/service/pipelines/index.ts new file mode 100644 index 0000000000..fd16b10e92 --- /dev/null +++ b/backend/src/routes/api/service/pipelines/index.ts @@ -0,0 +1,23 @@ +import { DSPipelineKind } from '../../../../types'; +import { proxyService } from '../../../../utils/proxy'; + +export default proxyService( + { + apiGroup: 'datasciencepipelinesapplications.opendatahub.io', + apiVersion: 'v1alpha1', + kind: 'DataSciencepipelinesApplication', + plural: 'datasciencepipelinesapplications', + }, + { + port: 8443, + prefix: 'ds-pipeline-', + }, + { + // Use port forwarding for local development: + // kubectl port-forward -n svc/ds-pipeline-pipelines-definition 8443:8443 + host: process.env.DS_PIPELINE_DSPA_SERVICE_HOST, + port: process.env.DS_PIPELINE_DSPA_SERVICE_PORT, + }, + (resource) => + !!resource.status?.conditions?.find((c) => c.type === 'APIServerReady' && c.status === 'True'), +); diff --git a/backend/src/routes/api/service/trustyai/index.ts b/backend/src/routes/api/service/trustyai/index.ts new file mode 100644 index 0000000000..c78283abd9 --- /dev/null +++ b/backend/src/routes/api/service/trustyai/index.ts @@ -0,0 +1,23 @@ +import { TrustyAIKind } from '../../../../types'; +import { proxyService } from '../../../../utils/proxy'; + +export default proxyService( + { + apiGroup: 'trustyai.opendatahub.io', + apiVersion: 'v1alpha1', + kind: 'TrustyAIService', + plural: 'trustyaiservices', + }, + { + port: 443, + suffix: '-tls', + }, + { + // Use port forwarding for local development: + // kubectl port-forward -n svc/trustyai-service-tls 9443:443 + host: process.env.TRUSTYAI_TAIS_SERVICE_HOST, + port: process.env.TRUSTYAI_TAIS_SERVICE_PORT, + }, + (resource) => + !!resource.status?.conditions?.find((c) => c.type === 'Available' && c.status === 'True'), +); diff --git a/backend/src/types.ts b/backend/src/types.ts index 4207db3012..bde7b3a527 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -270,7 +270,32 @@ export type KubeFastifyInstance = FastifyInstance & { // TODO: constant-ize the x-forwarded header export type OauthFastifyRequest = - FastifyRequest<{ Headers: { 'x-forwarded-access-token': string } & Data['Headers'] } & Data>; + FastifyRequest<{ Headers?: { 'x-forwarded-access-token'?: string } & Data['Headers'] } & Data>; + +export type K8sCondition = { + type: string; + status: string; + reason?: string; + message?: string; + lastProbeTime?: string | null; + lastTransitionTime?: string; + lastHeartbeatTime?: string; +}; + +export type DSPipelineKind = K8sResourceCommon & { + spec: { + dspVersion: string; + }; + status?: { + conditions?: K8sCondition[]; + }; +}; + +export type TrustyAIKind = K8sResourceCommon & { + status?: { + conditions?: K8sCondition[]; + }; +}; /* * Common types, should be kept up to date with frontend types diff --git a/backend/src/utils/proxy.ts b/backend/src/utils/proxy.ts new file mode 100644 index 0000000000..61242a5956 --- /dev/null +++ b/backend/src/utils/proxy.ts @@ -0,0 +1,102 @@ +import { FastifyRequest } from 'fastify'; +import httpProxy from '@fastify/http-proxy'; +import { K8sResourceCommon, KubeFastifyInstance } from '../types'; +import { isK8sStatus, passThroughResource } from '../routes/api/k8s/pass-through'; +import { DEV_MODE } from './constants'; +import { createCustomError } from './requestUtils'; +import { getAccessToken, getDirectCallOptions } from './directCallUtils'; + +export const getParam = >(req: F, name: string): string => + (req.params as { [key: string]: string })[name]; + +export const setParam = (req: FastifyRequest, name: string, value: string): string => + ((req.params as { [key: string]: string })[name] = value); + +const notFoundError = (kind: string, name: string, e?: any, overrideMessage?: string) => { + const message = + e instanceof Error + ? e.message + : e && e.code && e.response + ? `${e.code}: ${e.response.message}` + : e; + return createCustomError( + 'Not Found', + `${kind} '${name}' ${overrideMessage || 'not found'}.${message ? ` ${message}` : ''}`, + 404, + ); +}; + +export const proxyService = + ( + model: { apiGroup: string; apiVersion: string; plural: string; kind: string }, + service: { + port: number | string; + prefix?: string; + suffix?: string; + }, + local: { + host: string; + port: number | string; + }, + statusCheck?: (resource: K) => boolean, + tls = true, + ) => + async (fastify: KubeFastifyInstance): Promise => { + fastify.register(httpProxy, { + upstream: '', + prefix: '/:namespace/:name', + rewritePrefix: '', + replyOptions: { + // preHandler must set the `upstream` param + getUpstream: (request) => getParam(request, 'upstream'), + }, + preHandler: (request, _, done) => { + const kc = fastify.kube.config; + const cluster = kc.getCurrentCluster(); + + // see `prefix` for named params + const namespace = getParam(request, 'namespace'); + const name = getParam(request, 'name'); + + // retreive the gating resource by name and namespace + passThroughResource(fastify, request, { + url: `${cluster.server}/apis/${model.apiGroup}/${model.apiVersion}/namespaces/${namespace}/${model.plural}/${name}`, + method: 'GET', + }) + .then((resource) => { + return getDirectCallOptions(fastify, request, request.url).then((requestOptions) => { + if (isK8sStatus(resource)) { + done(notFoundError(model.kind, name)); + } else if (!statusCheck || statusCheck(resource)) { + if (tls) { + const token = getAccessToken(requestOptions); + request.headers.authorization = `Bearer ${token}`; + } + + const scheme = tls ? 'https' : 'http'; + + const upstream = DEV_MODE + ? // Use port forwarding for local development: + // kubectl port-forward -n svc/ : + `${scheme}://${local.host}:${local.port}` + : // Construct service URL + `${scheme}://${service?.prefix || ''}${resource.metadata.name}${ + service?.suffix ?? '' + }.${resource.metadata.namespace}.svc.cluster.local:${service.port}`; + + // assign the `upstream` param so we can dynamically set the upstream URL for http-proxy + setParam(request, 'upstream', upstream); + + fastify.log.info(`Proxy ${request.method} request ${request.url} to ${upstream}`); + done(); + } else { + done(notFoundError(model.kind, name, undefined, 'service unavailable')); + } + }); + }) + .catch((e) => { + done(notFoundError(model.kind, name, e)); + }); + }, + }); + }; diff --git a/frontend/src/__tests__/cypress/cypress/e2e/modelServing/ModelMetrics.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/modelServing/ModelMetrics.cy.ts index a3e88b1405..546d05166a 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/modelServing/ModelMetrics.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/modelServing/ModelMetrics.cy.ts @@ -158,8 +158,8 @@ const initIntercepts = ({ ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/metrics/all/requests', + method: 'GET', + pathname: '/api/service/trustyai/test-project/trustyai-service/metrics/all/requests', }, mockMetricsRequest({ modelName: 'test-inference-service' }), ); @@ -656,7 +656,7 @@ describe('Model Metrics', () => { cy.intercept( { method: 'POST', - pathname: '/api/proxy/metrics/dir/request', + pathname: '/api/service/trustyai/test-project/trustyai-service/metrics/dir/request', }, {}, ).as('configureBiasMetric'); @@ -664,7 +664,7 @@ describe('Model Metrics', () => { configureBiasMetricModal.findSubmitButton().should('be.enabled').click(); cy.wait('@configureBiasMetric').then((interception) => { - expect(interception.request.body.data).to.eql({ + expect(interception.request.body).to.eql({ modelId: 'test-inference-service', requestName: 'Test Metric', protectedAttribute: 'customer_data_input-3', diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineCreateRuns.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineCreateRuns.cy.ts index 9c4f1a220b..d3abc45de6 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineCreateRuns.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineCreateRuns.cy.ts @@ -67,8 +67,10 @@ describe('Pipeline Runs Global', () => { }; // Mock pipelines & versions for form select dropdowns - createRunPage.mockGetPipelines([mockPipeline]).as('getPipelines'); - createRunPage.mockGetPipelineVersions([mockPipelineVersion]).as('getPipelinesVersions'); + createRunPage.mockGetPipelines([mockPipeline], projectName).as('getPipelines'); + createRunPage + .mockGetPipelineVersions([mockPipelineVersion], projectName) + .as('getPipelinesVersions'); // Navigate to the 'Create run' page pipelineRunsGlobal.findCreateRunButton().click(); @@ -82,7 +84,7 @@ describe('Pipeline Runs Global', () => { createRunPage.selectPipelineByName('Test pipeline'); createRunPage.findPipelineVersionSelect().should('not.be.disabled'); createRunPage.findTriggeredRunTypeRadioInput().click(); - createRunPage.mockCreateRun(mockPipelineVersion, createRunParams).as('createRun'); + createRunPage.mockCreateRun(mockPipelineVersion, createRunParams, projectName).as('createRun'); createRunPage.submit(); // Should be redirected to the run details page @@ -97,12 +99,14 @@ describe('Pipeline Runs Global', () => { }; // Mock pipelines & versions for form select dropdowns - createRunPage.mockGetPipelines([mockPipeline]).as('getPipelines'); - createRunPage.mockGetPipelineVersions([mockPipelineVersion]).as('getPipelinesVersions'); + createRunPage.mockGetPipelines([mockPipeline], projectName).as('getPipelines'); + createRunPage + .mockGetPipelineVersions([mockPipelineVersion], projectName) + .as('getPipelinesVersions'); // Mock jobs list with newly created job pipelineRunJobTable - .mockGetJobs([...initialJobs, buildMockJobKF(createJobParams)]) + .mockGetJobs([...initialJobs, buildMockJobKF(createJobParams)], projectName) .as('refreshRunJobs'); // Navigate to the 'Create run' page @@ -117,7 +121,7 @@ describe('Pipeline Runs Global', () => { createRunPage.selectPipelineByName('Test pipeline'); createRunPage.findPipelineVersionSelect().should('not.be.disabled'); createRunPage.findScheduledRunTypeRadioInput().click(); - createRunPage.mockCreateJob(mockPipelineVersion, createJobParams).as('createJob'); + createRunPage.mockCreateJob(mockPipelineVersion, createJobParams, projectName).as('createJob'); createRunPage.submit(); // Should show newly created scheduled job in the table @@ -134,15 +138,17 @@ describe('Pipeline Runs Global', () => { }; // Mock pipelines & versions for form select dropdowns - cloneRunPage.mockGetPipelines([mockPipeline]).as('getPipelines'); - cloneRunPage.mockGetPipelineVersions([mockPipelineVersion]).as('getPipelinesVersions'); - cloneRunPage.mockGetJob(mockJob); - cloneRunPage.mockGetPipelineVersion(mockPipelineVersion); - cloneRunPage.mockGetPipeline(mockPipeline); + cloneRunPage.mockGetPipelines([mockPipeline], projectName).as('getPipelines'); + cloneRunPage + .mockGetPipelineVersions([mockPipelineVersion], projectName) + .as('getPipelinesVersions'); + cloneRunPage.mockGetJob(mockJob, projectName); + cloneRunPage.mockGetPipelineVersion(mockPipelineVersion, projectName); + cloneRunPage.mockGetPipeline(mockPipeline, projectName); // Mock jobs list with newly cloned job pipelineRunJobTable - .mockGetJobs([...initialJobs, buildMockJobKF(duplicateJobParams)]) + .mockGetJobs([...initialJobs, buildMockJobKF(duplicateJobParams)], projectName) .as('refreshRunJobs'); // Navigate to clone run page for a given scheduled job @@ -152,7 +158,7 @@ describe('Pipeline Runs Global', () => { // Verify pipeline & pipeline version are pre-populated & submit cloneRunPage.findPipelineSelect().should('have.text', mockPipeline.name); cloneRunPage.findPipelineVersionSelect().should('have.text', mockPipelineVersion.name); - cloneRunPage.mockCreateJob(mockPipelineVersion, duplicateJobParams).as('cloneJob'); + cloneRunPage.mockCreateJob(mockPipelineVersion, duplicateJobParams, projectName).as('cloneJob'); cloneRunPage.submit(); // Should show newly cloned scheduled job in the table @@ -170,15 +176,17 @@ describe('Pipeline Runs Global', () => { }; // Mock pipelines & versions for form select dropdowns - cloneRunPage.mockGetPipelines([mockPipeline]).as('getPipelines'); - cloneRunPage.mockGetPipelineVersions([mockPipelineVersion]).as('getPipelinesVersions'); - cloneRunPage.mockGetRunResource(mockRunResource); - cloneRunPage.mockGetPipelineVersion(mockPipelineVersion); - cloneRunPage.mockGetPipeline(mockPipeline); + cloneRunPage.mockGetPipelines([mockPipeline], projectName).as('getPipelines'); + cloneRunPage + .mockGetPipelineVersions([mockPipelineVersion], projectName) + .as('getPipelinesVersions'); + cloneRunPage.mockGetRunResource(mockRunResource, projectName); + cloneRunPage.mockGetPipelineVersion(mockPipelineVersion, projectName); + cloneRunPage.mockGetPipeline(mockPipeline, projectName); // Mock runs list with newly cloned run pipelineRunTable - .mockGetRuns([...initialRuns, buildMockRunKF(duplicateRunParams)]) + .mockGetRuns([...initialRuns, buildMockRunKF(duplicateRunParams)], projectName) .as('refreshRuns'); // Navigate to clone run page for a given triggered run @@ -189,7 +197,7 @@ describe('Pipeline Runs Global', () => { // Verify pipeline & pipeline version are pre-populated & submit cloneRunPage.findPipelineSelect().should('have.text', mockPipeline.name); cloneRunPage.findPipelineVersionSelect().should('have.text', mockPipelineVersion.name); - cloneRunPage.mockCreateRun(mockPipelineVersion, duplicateRunParams).as('cloneRun'); + cloneRunPage.mockCreateRun(mockPipelineVersion, duplicateRunParams, projectName).as('cloneRun'); cloneRunPage.submit(); // Should redirect to the details of the newly cloned triggered run @@ -226,15 +234,15 @@ const initIntercepts = () => { ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/jobs', + method: 'GET', + pathname: '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/jobs', }, { jobs: initialJobs, total_size: initialJobs.length }, ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/runs', + method: 'GET', + pathname: '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/runs', }, { runs: initialRuns, total_size: initialRuns.length }, ); diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineDeleteRuns.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineDeleteRuns.cy.ts index 5223c03d65..4ba48d2f7b 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineDeleteRuns.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineDeleteRuns.cy.ts @@ -32,15 +32,16 @@ const initIntercepts = () => { ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines/test-pipeline', + method: 'GET', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/pipelines/test-pipeline', }, mockPipelineKF({}), ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/jobs', + method: 'GET', + pathname: '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/jobs', }, { jobs: [ @@ -51,8 +52,8 @@ const initIntercepts = () => { ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/runs', + method: 'GET', + pathname: '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/runs', }, { runs: [ @@ -63,15 +64,16 @@ const initIntercepts = () => { ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines', + method: 'GET', + pathname: '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/pipelines', }, mockPipelineKF({}), ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines/test-pipeline/templates', + method: 'GET', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/pipelines/test-pipeline/templates', }, mockPipelinesVersionTemplateResourceKF(), ); @@ -139,31 +141,23 @@ describe('Pipeline runs', () => { cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/jobs/test-pipeline', + method: 'DELETE', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/jobs/test-pipeline', }, mockStatus(), ).as('postJobPipeline'); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/jobs', + method: 'GET', + pathname: '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/jobs', }, { jobs: [buildMockJobKF({ id: 'other-pipeline', name: 'other-pipeline' })] }, ).as('getRuns'); scheduledRunDeleteModal.findSubmitButton().click(); - cy.wait('@postJobPipeline').then((intercept) => { - expect(intercept.request.body).to.eql({ - path: '/apis/v1beta1/jobs/test-pipeline', - method: 'DELETE', - host: 'https://ds-pipeline-pipelines-definition-test-project.apps.user.com', - queryParams: {}, - data: {}, - }); - }); cy.wait('@getRuns').then(() => { pipelineRunJobTable.findEmptyState().should('not.exist'); }); @@ -187,50 +181,32 @@ describe('Pipeline runs', () => { cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/jobs/test-pipeline', + method: 'DELETE', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/jobs/test-pipeline', }, mockStatus(), ).as('postJobPipeline-1'); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/jobs/other-pipeline', + method: 'DELETE', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/jobs/other-pipeline', }, mockStatus(), ).as('postJobPipeline-2'); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/jobs', + method: 'GET', + pathname: '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/jobs', }, { jobs: [] }, ).as('getRuns'); scheduledRunDeleteMultipleModal.findSubmitButton().click(); - cy.wait('@postJobPipeline-1').then((intercept) => { - expect(intercept.request.body).to.eql({ - path: '/apis/v1beta1/jobs/test-pipeline', - method: 'DELETE', - host: 'https://ds-pipeline-pipelines-definition-test-project.apps.user.com', - queryParams: {}, - data: {}, - }); - }); - - cy.wait('@postJobPipeline-2').then((intercept) => { - expect(intercept.request.body).to.eql({ - path: '/apis/v1beta1/jobs/other-pipeline', - method: 'DELETE', - host: 'https://ds-pipeline-pipelines-definition-test-project.apps.user.com', - queryParams: {}, - data: {}, - }); - }); - cy.wait('@getRuns').then(() => { pipelineRunJobTable.findEmptyState().should('exist'); }); @@ -251,31 +227,23 @@ describe('Pipeline runs', () => { cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/runs/test-pipeline', + method: 'DELETE', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/runs/test-pipeline', }, mockStatus(), ).as('postRunPipeline'); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/runs', + method: 'GET', + pathname: '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/runs', }, { runs: [buildMockRunKF({ id: 'other-pipeline', name: 'other-pipeline' })] }, ).as('getRuns'); triggeredRunDeleteModal.findSubmitButton().click(); - cy.wait('@postRunPipeline').then((intercept) => { - expect(intercept.request.body).to.eql({ - path: '/apis/v1beta1/runs/test-pipeline', - method: 'DELETE', - host: 'https://ds-pipeline-pipelines-definition-test-project.apps.user.com', - queryParams: {}, - data: {}, - }); - }); cy.wait('@getRuns').then(() => { pipelineRunTable.findEmptyState().should('not.exist'); }); @@ -299,49 +267,32 @@ describe('Pipeline runs', () => { cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/runs/test-pipeline', + method: 'DELETE', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/runs/test-pipeline', }, mockStatus(), ).as('postRunPipeline-1'); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/runs/other-pipeline', + method: 'DELETE', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/runs/other-pipeline', }, mockStatus(), ).as('postRunPipeline-2'); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/runs', + method: 'GET', + pathname: '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/runs', }, { runs: [] }, ).as('getRuns'); triggeredRunDeleteMultipleModal.findSubmitButton().click(); - cy.wait('@postRunPipeline-1').then((intercept) => { - expect(intercept.request.body).to.eql({ - path: '/apis/v1beta1/runs/test-pipeline', - method: 'DELETE', - host: 'https://ds-pipeline-pipelines-definition-test-project.apps.user.com', - queryParams: {}, - data: {}, - }); - }); - - cy.wait('@postRunPipeline-2').then((intercept) => { - expect(intercept.request.body).to.eql({ - path: '/apis/v1beta1/runs/other-pipeline', - method: 'DELETE', - host: 'https://ds-pipeline-pipelines-definition-test-project.apps.user.com', - queryParams: {}, - data: {}, - }); - }); cy.wait('@getRuns').then(() => { pipelineRunTable.findEmptyState().should('exist'); }); diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts index 4281c8454e..8260ccfa2e 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts @@ -51,9 +51,9 @@ describe('Pipelines', () => { const uploadedMockPipeline = buildMockPipeline(uploadPipelineParams); // Intercept upload/re-fetch of pipelines - pipelineImportModal.mockUploadPipeline(uploadPipelineParams).as('uploadPipeline'); + pipelineImportModal.mockUploadPipeline(uploadPipelineParams, projectName).as('uploadPipeline'); pipelinesTable - .mockGetPipelines([initialMockPipeline, uploadedMockPipeline]) + .mockGetPipelines([initialMockPipeline, uploadedMockPipeline], projectName) .as('refreshPipelines'); // Wait for the pipelines table to load @@ -91,12 +91,14 @@ describe('Pipelines', () => { pipelinesGlobal.findUploadVersionButton().click(); // Intercept upload/re-fetch of pipeline versions - pipelineVersionImportModal.mockUploadVersion(uploadVersionParams).as('uploadVersion'); + pipelineVersionImportModal + .mockUploadVersion(uploadVersionParams, projectName) + .as('uploadVersion'); pipelinesTable - .mockGetPipelineVersions([ - initialMockPipelineVersion, - buildMockPipelineVersion(uploadVersionParams), - ]) + .mockGetPipelineVersions( + [initialMockPipelineVersion, buildMockPipelineVersion(uploadVersionParams)], + projectName, + ) .as('refreshVersions'); // Fill out the "Upload new version" modal and submit @@ -120,15 +122,17 @@ describe('Pipelines', () => { initIntercepts(); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines', + method: 'GET', + pathname: + '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipelines', }, buildMockPipelines([initialMockPipeline]), ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipeline_versions', + method: 'GET', + pathname: + '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipeline_versions', }, buildMockPipelineVersions([initialMockPipelineVersion]), ); @@ -145,8 +149,9 @@ describe('Pipelines', () => { deleteModal.findInput().type(initialMockPipeline.name); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines', + method: 'GET', + pathname: + '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipelines', }, buildMockPipelines([]), ).as('refreshPipelines'); @@ -160,15 +165,17 @@ describe('Pipelines', () => { initIntercepts(); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines', + method: 'GET', + pathname: + '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipelines', }, buildMockPipelines([initialMockPipeline]), ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipeline_versions', + method: 'GET', + pathname: + '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipeline_versions', }, buildMockPipelineVersions([initialMockPipelineVersion]), ); @@ -186,8 +193,9 @@ describe('Pipelines', () => { deleteModal.findInput().type(initialMockPipelineVersion.name); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipeline_versions', + method: 'GET', + pathname: + '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipeline_versions', }, buildMockPipelineVersions([]), ).as('refreshVersions'); @@ -224,15 +232,17 @@ describe('Pipelines', () => { initIntercepts(); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines', + method: 'GET', + pathname: + '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipelines', }, buildMockPipelines([mockPipeline1, mockPipeline2]), ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipeline_versions', + method: 'GET', + pathname: + '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipeline_versions', }, (req) => { const response = { @@ -242,7 +252,7 @@ describe('Pipelines', () => { mockPipeline2Version2, ]), }; - req.reply(response[req.body.queryParams['resource_key.id']]); + req.reply(response[req.query['resource_key.id']]); }, ); createDeletePipelineIntercept(mockPipeline1.id).as('deletePipeline'); @@ -263,19 +273,21 @@ describe('Pipelines', () => { deleteModal.findInput().type('Delete 1 pipeline and 1 version'); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines', + method: 'GET', + pathname: + '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipelines', }, buildMockPipelines([mockPipeline2]), ).as('refreshPipelines'); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipeline_versions', + method: 'GET', + pathname: + '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipeline_versions', }, (req) => { const response = { [mockPipeline2.id]: buildMockPipelineVersions([mockPipeline2Version2]) }; - req.reply(response[req.body.queryParams['resource_key.id']]); + req.reply(response[req.query['resource_key.id']]); }, ).as('refreshVersions'); deleteModal.findSubmitButton().click(); @@ -328,16 +340,18 @@ const initIntercepts = () => { const initPipelineIntercepts = () => { cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines', + method: 'GET', + pathname: + '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipelines', }, buildMockPipelines([initialMockPipeline]), ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipeline_versions', + method: 'GET', + pathname: + '/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipeline_versions', }, buildMockPipelineVersions([initialMockPipelineVersion]), ); @@ -346,12 +360,11 @@ const initPipelineIntercepts = () => { const createDeletePipelineIntercept = (id: string) => cy.intercept( { - pathname: `/api/proxy/apis/v1beta1/pipelines/${id}`, - method: 'POST', + pathname: `/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipelines/${id}`, + method: 'DELETE', times: 1, }, (req) => { - expect(req.body.method).eq('DELETE'); req.reply({ body: {} }); }, ); @@ -359,12 +372,11 @@ const createDeletePipelineIntercept = (id: string) => const createDeleteVersionIntercept = (id: string) => cy.intercept( { - pathname: `/api/proxy/apis/v1beta1/pipeline_versions/${id}`, - method: 'POST', + pathname: `/api/service/pipelines/test-project-name/pipelines-definition/apis/v1beta1/pipeline_versions/${id}`, + method: 'DELETE', times: 1, }, (req) => { - expect(req.body.method).eq('DELETE'); req.reply({ body: {} }); }, ); diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesList.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesList.cy.ts index 518743f9e7..9b1fd692f4 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesList.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesList.cy.ts @@ -101,8 +101,8 @@ describe('PipelinesList', () => { ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines', + method: 'GET', + pathname: '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/pipelines', }, buildMockPipelines([]), ); diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesTopology.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesTopology.cy.ts index 0fde5557fe..ef5d7b727a 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesTopology.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesTopology.cy.ts @@ -32,15 +32,17 @@ const initIntercepts = () => { ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines/test-pipeline', + method: 'GET', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/pipelines/test-pipeline', }, mockPipelineKF({}), ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipeline_versions/test-pipeline', + method: 'GET', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/pipeline_versions/test-pipeline', }, buildMockPipelineVersion({ id: 'test-pipeline', @@ -57,8 +59,9 @@ const initIntercepts = () => { cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipeline_versions/test-pipeline/templates', + method: 'GET', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/pipeline_versions/test-pipeline/templates', }, mockPipelinesVersionTemplateResourceKF(), ); @@ -102,15 +105,16 @@ const initIntercepts = () => { ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/jobs/test-pipeline', + method: 'GET', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/jobs/test-pipeline', }, buildMockJobKF({ name: 'test-pipeline', id: 'test-pipeline' }), ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines', + method: 'GET', + pathname: '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/pipelines', }, mockPipelineKF({}), ); @@ -130,15 +134,16 @@ const initIntercepts = () => { cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/runs/test-pipeline-run-id', + method: 'GET', + pathname: + '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/runs/test-pipeline-run-id', }, getMockRunResource(mockRun), ); cy.intercept( { - method: 'POST', - pathname: `/api/proxy/apis/v1beta1/pipeline_versions/${mockRunVersionDetails.id}`, + method: 'GET', + pathname: `/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/pipeline_versions/${mockRunVersionDetails.id}`, }, buildMockPipelineVersion(mockRunVersionDetails), ); diff --git a/frontend/src/__tests__/cypress/cypress/e2e/projects/projectDetails.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/projects/projectDetails.cy.ts index 5147c7e37d..0ea14efb73 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/projects/projectDetails.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/projects/projectDetails.cy.ts @@ -157,8 +157,8 @@ const initIntercepts = ({ ); cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines', + method: 'GET', + pathname: '/api/service/pipelines/test-project/pipelines-definition/apis/v1beta1/pipelines', }, buildMockPipelines(isEmpty ? [] : [mockPipelineKF({})]), ); diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/cloneRunPage.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/cloneRunPage.ts index 092ca8d49b..5b7273e8bc 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/cloneRunPage.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/cloneRunPage.ts @@ -13,41 +13,44 @@ class CloneRunPage extends CreateRunPage { super(); } - mockGetRunResource(runResource: PipelineRunResourceKF) { + mockGetRunResource(runResource: PipelineRunResourceKF, namespace: string) { return cy.intercept( { - method: 'POST', - pathname: `/api/proxy/apis/v1beta1/runs/${runResource.run.id}`, + method: 'GET', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/runs/${runResource.run.id}`, }, runResource, ); } - mockGetJob(job: PipelineRunJobKF) { + mockGetJob(job: PipelineRunJobKF, namespace: string) { return cy.intercept( { - method: 'POST', - pathname: `/api/proxy/apis/v1beta1/jobs/${job.id}`, + method: 'GET', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/jobs/${job.id}`, }, job, ); } - mockGetPipelineVersion(pipelineVersion: PipelineVersionKF): Cypress.Chainable { + mockGetPipelineVersion( + pipelineVersion: PipelineVersionKF, + namespace: string, + ): Cypress.Chainable { return cy.intercept( { - method: 'POST', - pathname: `/api/proxy/apis/v1beta1/pipeline_versions/${pipelineVersion.id}`, + method: 'GET', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/pipeline_versions/${pipelineVersion.id}`, }, pipelineVersion, ); } - mockGetPipeline(pipeline: PipelineKF): Cypress.Chainable { + mockGetPipeline(pipeline: PipelineKF, namespace: string): Cypress.Chainable { return cy.intercept( { - method: 'POST', - pathname: `/api/proxy/apis/v1beta1/pipelines/${pipeline.id}`, + method: 'GET', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/pipelines/${pipeline.id}`, }, pipeline, ); diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/createRunPage.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/createRunPage.ts index d400e32405..2ec9b3f6d3 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/createRunPage.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/createRunPage.ts @@ -69,20 +69,23 @@ export class CreateRunPage { .click(); } - mockGetPipelines(pipelines: PipelineKF[]): Cypress.Chainable { + mockGetPipelines(pipelines: PipelineKF[], namespace: string): Cypress.Chainable { return cy.intercept( { - pathname: '/api/proxy/apis/v1beta1/pipelines', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/pipelines`, }, buildMockPipelines(pipelines), ); } - mockGetPipelineVersions(versions: PipelineVersionKF[]): Cypress.Chainable { + mockGetPipelineVersions( + versions: PipelineVersionKF[], + namespace: string, + ): Cypress.Chainable { return cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipeline_versions', + method: 'GET', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/pipeline_versions`, }, buildMockPipelineVersions(versions), ); @@ -91,11 +94,12 @@ export class CreateRunPage { mockCreateRun( pipelineVersion: PipelineVersionKF, { id, name, description }: Partial, + namespace: string, ): Cypress.Chainable { return cy.intercept( { method: 'POST', - pathname: '/api/proxy/apis/v1beta1/runs', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/runs`, times: 1, }, { @@ -134,11 +138,12 @@ export class CreateRunPage { mockCreateJob( pipelineVersion: PipelineVersionKF, { id, name, description }: Partial, + namespace: string, ): Cypress.Chainable { return cy.intercept( { method: 'POST', - pathname: '/api/proxy/apis/v1beta1/jobs', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/jobs`, times: 1, }, { diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts index bf1ea6824f..3ce1a62c3e 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts @@ -39,11 +39,11 @@ class PipelineImportModal extends Modal { this.findUploadPipelineInput().selectFile([filePath], { force: true }); } - mockUploadPipeline(params: Partial) { + mockUploadPipeline(params: Partial, namespace: string) { return cy.intercept( { method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines/upload', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/pipelines/upload`, times: 1, }, buildMockPipeline(params), diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineRunTable.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineRunTable.ts index 4c5311b27f..b104f908a4 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineRunTable.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineRunTable.ts @@ -36,11 +36,11 @@ class PipelineRunTable { cy.findByRole('menu').get('span').contains(actionName).parents('button').click(); } - mockGetRuns(runs: PipelineRunKF[]) { + mockGetRuns(runs: PipelineRunKF[], namespace: string) { return cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/runs', + method: 'GET', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/runs`, }, { runs, total_size: runs.length }, ); @@ -52,11 +52,11 @@ class PipelineRunJobTable extends PipelineRunTable { super(testId, toolbarTestId); } - mockGetJobs(jobs: PipelineRunJobKF[]) { + mockGetJobs(jobs: PipelineRunJobKF[], namespace: string) { return cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/jobs', + method: 'GET', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/jobs`, }, { jobs, total_size: jobs.length }, ); diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineVersionImportModal.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineVersionImportModal.ts index c2125b1df5..a16a9ff508 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineVersionImportModal.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineVersionImportModal.ts @@ -58,11 +58,11 @@ class PipelineImportModal extends Modal { this.findSubmitButton().click(); } - mockUploadVersion(params: Partial) { + mockUploadVersion(params: Partial, namespace: string) { return cy.intercept( { method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipelines/upload_version', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/pipelines/upload_version`, times: 1, }, buildMockPipelineVersion(params), diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesTable.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesTable.ts index 6732b4e2aa..d53be21ef1 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesTable.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesTable.ts @@ -26,20 +26,20 @@ class PipelinesTable { }; } - mockGetPipelines(pipelines: PipelineKF[]) { + mockGetPipelines(pipelines: PipelineKF[], namespace: string) { return cy.intercept( { - pathname: '/api/proxy/apis/v1beta1/pipelines', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/pipelines`, }, buildMockPipelines(pipelines), ); } - mockGetPipelineVersions(versions: PipelineVersionKF[]) { + mockGetPipelineVersions(versions: PipelineVersionKF[], namespace: string) { return cy.intercept( { - method: 'POST', - pathname: '/api/proxy/apis/v1beta1/pipeline_versions', + method: 'GET', + pathname: `/api/service/pipelines/${namespace}/pipelines-definition/apis/v1beta1/pipeline_versions`, }, buildMockPipelineVersions(versions), ); diff --git a/frontend/src/api/__tests__/proxyUtils.spec.ts b/frontend/src/api/__tests__/proxyUtils.spec.ts index efc4607e30..6baa0de630 100644 --- a/frontend/src/api/__tests__/proxyUtils.spec.ts +++ b/frontend/src/api/__tests__/proxyUtils.spec.ts @@ -24,10 +24,9 @@ describe('proxyGET', () => { const result = await proxyGET(host, path); expect(result).toStrictEqual(JSON.parse(textValue)); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"GET","host":"test","queryParams":{}}', - headers: { 'Content-Type': 'application/json;charset=UTF-8' }, - method: 'POST', + expect(mockFetch).toHaveBeenCalledWith('test/test', { + body: undefined, + method: 'GET', }); }); @@ -40,10 +39,9 @@ describe('proxyGET', () => { const result = await proxyGET(host, path, { dryRun: true }, { parseJSON: false }); expect(result).toStrictEqual(textValue); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"GET","host":"test","queryParams":{"dryRun":true}}', - headers: { 'Content-Type': 'application/json;charset=UTF-8' }, - method: 'POST', + expect(mockFetch).toHaveBeenCalledWith('test/test?dryRun=true', { + body: undefined, + method: 'GET', }); }); @@ -52,10 +50,9 @@ describe('proxyGET', () => { await expect(proxyGET(host, path)).rejects.toThrow('error'); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"GET","host":"test","queryParams":{}}', - headers: { 'Content-Type': 'application/json;charset=UTF-8' }, - method: 'POST', + expect(mockFetch).toHaveBeenCalledWith('test/test', { + body: undefined, + method: 'GET', }); }); }); @@ -70,19 +67,20 @@ describe('proxyCREATE', () => { const result = await proxyCREATE(host, path, data); expect(result).toStrictEqual(JSON.parse(textValue)); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"POST","host":"test","queryParams":{},"data":{"key":"value"}}', + expect(mockFetch).toHaveBeenCalledWith('test/test', { + body: '{"key":"value"}', headers: { 'Content-Type': 'application/json;charset=UTF-8' }, method: 'POST', }); }); + it('should handle errors and rethrows', async () => { mockFetch.mockRejectedValue(new Error('error')); await expect(proxyCREATE(host, path, data)).rejects.toThrow('error'); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"POST","host":"test","queryParams":{},"data":{"key":"value"}}', + expect(mockFetch).toHaveBeenCalledWith('test/test', { + body: '{"key":"value"}', headers: { 'Content-Type': 'application/json;charset=UTF-8' }, method: 'POST', }); @@ -91,6 +89,13 @@ describe('proxyCREATE', () => { describe('proxyFILE', () => { const fileContents = 'test'; + const formData = new FormData(); + formData.append( + 'uploadfile', + new Blob(['test'], { type: 'application/x-yaml' }), + 'uploadedFile.yml', + ); + it('should call callProxyJSON with file contents', async () => { mockFetch.mockResolvedValue({ status: 200, @@ -100,9 +105,8 @@ describe('proxyFILE', () => { const result = await proxyFILE(host, path, fileContents); expect(result).toStrictEqual(JSON.parse(textValue)); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"POST","host":"test","queryParams":{},"fileContents":"test"}', - headers: { 'Content-Type': 'application/json;charset=UTF-8' }, + expect(mockFetch).toHaveBeenCalledWith('test/test', { + body: formData, method: 'POST', }); }); @@ -112,9 +116,8 @@ describe('proxyFILE', () => { await expect(proxyFILE(host, path, fileContents)).rejects.toThrow('error'); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"POST","host":"test","queryParams":{},"fileContents":"test"}', - headers: { 'Content-Type': 'application/json;charset=UTF-8' }, + expect(mockFetch).toHaveBeenCalledWith('test/test', { + body: formData, method: 'POST', }); }); @@ -130,9 +133,8 @@ describe('proxyENDPOINT', () => { const result = await proxyENDPOINT(host, path); expect(result).toStrictEqual(JSON.parse(textValue)); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"POST","host":"test","queryParams":{}}', - headers: { 'Content-Type': 'application/json;charset=UTF-8' }, + expect(mockFetch).toHaveBeenCalledWith('test/test', { + body: undefined, method: 'POST', }); }); @@ -142,9 +144,8 @@ describe('proxyENDPOINT', () => { await expect(proxyENDPOINT(host, path)).rejects.toThrow('error'); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"POST","host":"test","queryParams":{}}', - headers: { 'Content-Type': 'application/json;charset=UTF-8' }, + expect(mockFetch).toHaveBeenCalledWith('test/test', { + body: undefined, method: 'POST', }); }); @@ -160,10 +161,10 @@ describe('proxyUPDATE', () => { const result = await proxyUPDATE(host, path, data); expect(result).toStrictEqual(JSON.parse(textValue)); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"PUT","host":"test","queryParams":{},"data":{"key":"value"}}', + expect(mockFetch).toHaveBeenCalledWith('test/test', { + body: '{"key":"value"}', headers: { 'Content-Type': 'application/json;charset=UTF-8' }, - method: 'POST', + method: 'PUT', }); }); @@ -172,10 +173,10 @@ describe('proxyUPDATE', () => { await expect(proxyUPDATE(host, path, data)).rejects.toThrow('error'); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"PUT","host":"test","queryParams":{},"data":{"key":"value"}}', + expect(mockFetch).toHaveBeenCalledWith('test/test', { + body: '{"key":"value"}', headers: { 'Content-Type': 'application/json;charset=UTF-8' }, - method: 'POST', + method: 'PUT', }); }); }); @@ -190,10 +191,10 @@ describe('proxyDELETE', () => { const result = await proxyDELETE(host, path, data); expect(result).toStrictEqual(JSON.parse(textValue)); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"DELETE","host":"test","queryParams":{},"data":{"key":"value"}}', + expect(mockFetch).toHaveBeenCalledWith('test/test', { + body: '{"key":"value"}', headers: { 'Content-Type': 'application/json;charset=UTF-8' }, - method: 'POST', + method: 'DELETE', }); }); @@ -202,10 +203,10 @@ describe('proxyDELETE', () => { await expect(proxyDELETE(host, path, data)).rejects.toThrow('error'); expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith('/api/proxy/test', { - body: '{"path":"/test","method":"DELETE","host":"test","queryParams":{},"data":{"key":"value"}}', + expect(mockFetch).toHaveBeenCalledWith('test/test', { + body: '{"key":"value"}', headers: { 'Content-Type': 'application/json;charset=UTF-8' }, - method: 'POST', + method: 'DELETE', }); }); }); diff --git a/frontend/src/api/proxyUtils.ts b/frontend/src/api/proxyUtils.ts index e9d4166af5..01267a9fae 100644 --- a/frontend/src/api/proxyUtils.ts +++ b/frontend/src/api/proxyUtils.ts @@ -22,21 +22,41 @@ const callProxyJSON = ( ): Promise => { const { method, ...otherOptions } = requestInit; - // Add the path to the end of the proxy call, so it's easier to notice different proxy requests from each other - return fetch(`/api/proxy${path}`, { + const sanitizedQueryParams = queryParams + ? Object.entries(queryParams).reduce((acc, [key, value]) => { + if (value) { + return { ...acc, [key]: value }; + } + + return acc; + }, {}) + : null; + + const searchParams = sanitizedQueryParams + ? new URLSearchParams(sanitizedQueryParams).toString() + : null; + + let requestData: string | undefined; + let contentType: string | undefined; + let formData: FormData | undefined; + if (fileContents) { + formData = new FormData(); + formData.append( + 'uploadfile', + new Blob([fileContents], { type: 'application/x-yaml' }), + 'uploadedFile.yml', + ); + } else if (data) { + // It's OK for contentType and requestData to BOTH be undefined for e.g. a GET request or POST with no body. + contentType = 'application/json;charset=UTF-8'; + requestData = JSON.stringify(data); + } + + return fetch(`${host}${path}${searchParams ? `?${searchParams}` : ''}`, { ...otherOptions, - headers: { - 'Content-Type': `application/json;charset=UTF-8`, - }, - method: 'POST', // we always post so we can send data - body: JSON.stringify({ - path, // Not part of the request -- but easier to read from the network tab - method, - host, - queryParams, - data, - fileContents, - }), + ...(contentType && { headers: { 'Content-Type': contentType } }), + method, + body: formData ?? requestData, }).then((response) => response.text().then((fetchedData) => { if (parseJSON) { @@ -111,6 +131,17 @@ export const proxyUPDATE = ( parseJSON: options?.parseJSON, }); +export const proxyPATCH = ( + host: string, + path: string, + data: Record, + options?: K8sAPIOptions, +): Promise => + callProxyJSON(host, path, mergeRequestInit(options, { method: 'PATCH' }), { + data, + parseJSON: options?.parseJSON, + }); + export const proxyDELETE = ( host: string, path: string, diff --git a/frontend/src/concepts/pipelines/context/PipelinesContext.tsx b/frontend/src/concepts/pipelines/context/PipelinesContext.tsx index 0c0cf0649c..43d2314245 100644 --- a/frontend/src/concepts/pipelines/context/PipelinesContext.tsx +++ b/frontend/src/concepts/pipelines/context/PipelinesContext.tsx @@ -81,8 +81,11 @@ export const PipelineContextProvider = conditionalArea Promise.all([refreshCR(), refreshRoute()]).then(() => undefined), diff --git a/frontend/src/concepts/trustyai/context/TrustyAIContext.tsx b/frontend/src/concepts/trustyai/context/TrustyAIContext.tsx index 37f984406f..8ff1847a89 100644 --- a/frontend/src/concepts/trustyai/context/TrustyAIContext.tsx +++ b/frontend/src/concepts/trustyai/context/TrustyAIContext.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import useTrustyAIAPIRoute from '~/concepts/trustyai/useTrustyAIAPIRoute'; import useTrustyAINamespaceCR, { isTrustyAIAvailable, taiHasServerTimedOut, @@ -14,7 +13,7 @@ type TrustyAIContextProps = { hasCR: boolean; crInitializing: boolean; serverTimedOut: boolean; - serviceLoadError?: Error; + crLoadError?: Error; ignoreTimedOut: () => void; refreshState: () => Promise; refreshAPIState: () => void; @@ -43,7 +42,7 @@ export const TrustyAIContextProvider: React.FC = ( namespace, }) => { const crState = useTrustyAINamespaceCR(namespace); - const [explainabilityNamespaceCR, crLoaded, crLoadError, refreshCR] = crState; + const [trustyNamespaceCR, crLoaded, crLoadError, refreshCR] = crState; const isCRReady = isTrustyAIAvailable(crState); const [disableTimeout, setDisableTimeout] = React.useState(false); const serverTimedOut = !disableTimeout && taiHasServerTimedOut(crState, isCRReady); @@ -51,19 +50,11 @@ export const TrustyAIContextProvider: React.FC = ( setDisableTimeout(true); }, []); - const [routeHost, routeLoaded, routeLoadError, refreshRoute] = useTrustyAIAPIRoute( - isCRReady, - namespace, - ); - - const hostPath = routeLoaded && routeHost ? routeHost : null; + const taisName = trustyNamespaceCR?.metadata.name; - const refreshState = React.useCallback( - () => Promise.all([refreshCR(), refreshRoute()]).then(() => undefined), - [refreshRoute, refreshCR], - ); + const hostPath = namespace && taisName ? `/api/service/trustyai/${namespace}/${taisName}` : null; - const serviceLoadError = crLoadError || routeLoadError; + const refreshState = React.useCallback(() => refreshCR().then(() => undefined), [refreshCR]); const [apiState, refreshAPIState] = useTrustyAIAPIState(hostPath); @@ -72,11 +63,11 @@ export const TrustyAIContextProvider: React.FC = ( const contextValue = React.useMemo( () => ({ namespace, - hasCR: !!explainabilityNamespaceCR, + hasCR: !!trustyNamespaceCR, crInitializing: !crLoaded, serverTimedOut, ignoreTimedOut, - serviceLoadError, + crLoadError, refreshState, refreshAPIState, apiState, @@ -84,11 +75,11 @@ export const TrustyAIContextProvider: React.FC = ( }), [ namespace, - explainabilityNamespaceCR, + trustyNamespaceCR, crLoaded, serverTimedOut, ignoreTimedOut, - serviceLoadError, + crLoadError, refreshState, refreshAPIState, apiState, diff --git a/frontend/src/concepts/trustyai/useTrustyAIAPIRoute.ts b/frontend/src/concepts/trustyai/useTrustyAIAPIRoute.ts deleted file mode 100644 index 698a7593f5..0000000000 --- a/frontend/src/concepts/trustyai/useTrustyAIAPIRoute.ts +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import useFetchState, { - FetchState, - FetchStateCallbackPromise, - NotReadyError, -} from '~/utilities/useFetchState'; -import { getTrustyAIAPIRoute } from '~/api/'; -import { RouteKind } from '~/k8sTypes'; -import { FAST_POLL_INTERVAL } from '~/utilities/const'; -import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; - -type State = string | null; -const useTrustyAIAPIRoute = (hasCR: boolean, namespace: string): FetchState => { - const trustyAIAreaAvailable = useIsAreaAvailable(SupportedArea.TRUSTY_AI).status; - const callback = React.useCallback>( - (opts) => { - if (!trustyAIAreaAvailable) { - return Promise.reject(new NotReadyError('Bias metrics is not enabled')); - } - - if (!hasCR) { - return Promise.reject(new NotReadyError('CR not created')); - } - - return getTrustyAIAPIRoute(namespace, opts) - .then((result: RouteKind) => `https://${result.spec.host}`) - .catch((e) => { - if (e.statusObject?.code === 404) { - // Not finding is okay, not an error - return null; - } - throw e; - }); - }, - [hasCR, namespace, trustyAIAreaAvailable], - ); - - const state = useFetchState(callback, null, { - initialPromisePurity: true, - }); - - const [data, , , refresh] = state; - - const hasData = !!data; - React.useEffect(() => { - let interval: ReturnType; - if (!hasData) { - interval = setInterval(refresh, FAST_POLL_INTERVAL); - } - return () => { - clearInterval(interval); - }; - }, [hasData, refresh]); - return state; -}; - -export default useTrustyAIAPIRoute;