diff --git a/backend/src/routes/api/images/__tests__/imageUtils.spec.ts b/backend/src/routes/api/images/__tests__/imageUtils.spec.ts new file mode 100644 index 0000000000..39ec6fc8cd --- /dev/null +++ b/backend/src/routes/api/images/__tests__/imageUtils.spec.ts @@ -0,0 +1,51 @@ +// https://cloud.google.com/artifact-registry/docs/docker/names +// The full name for a container image is one of the following formats: +// LOCATION-docker.pkg.dev/PROJECT-ID/REPOSITORY/IMAGE +// LOCATION-docker.pkg.dev/PROJECT-ID/REPOSITORY/IMAGE:TAG +// LOCATION-docker.pkg.dev/PROJECT-ID/REPOSITORY/IMAGE@IMAGE-DIGEST + +import { IMAGE_URL_REGEXP } from '../imageUtils'; + +describe('IMAGE_URL_REGEXP', () => { + test('Invalid URL', () => { + const url = 'docker.io'; + const match = url.match(IMAGE_URL_REGEXP); + expect(match?.[1]).toBe(''); + }); + + test('Docker container URL without tag', () => { + const url = 'docker.io/library/mysql'; + const match = url.match(IMAGE_URL_REGEXP); + expect(match?.[1]).toBe('docker.io'); + expect(match?.[3]).toBe(undefined); + }); + + test('Docker container URL with tag', () => { + const url = 'docker.io/library/mysql:test-tag'; + const match = url.match(IMAGE_URL_REGEXP); + expect(match?.[1]).toBe('docker.io'); + expect(match?.[3]).toBe('test-tag'); + }); + + test('OpenShift internal registry URL without tag', () => { + const url = 'image-registry.openshift-image-registry.svc:5000/opendatahub/s2i-minimal-notebook'; + const match = url.match(IMAGE_URL_REGEXP); + expect(match?.[1]).toBe('image-registry.openshift-image-registry.svc:5000'); + expect(match?.[3]).toBe(undefined); + }); + + test('OpenShift internal registry URL with tag', () => { + const url = + 'image-registry.openshift-image-registry.svc:5000/opendatahub/s2i-minimal-notebook:v0.3.0-py36'; + const match = url.match(IMAGE_URL_REGEXP); + expect(match?.[1]).toBe('image-registry.openshift-image-registry.svc:5000'); + expect(match?.[3]).toBe('v0.3.0-py36'); + }); + + test('Quay URL with port and tag', () => { + const url = 'quay.io:443/opendatahub/odh-dashboard:main-55e19fa'; + const match = url.match(IMAGE_URL_REGEXP); + expect(match?.[1]).toBe('quay.io:443'); + expect(match?.[3]).toBe('main-55e19fa'); + }); +}); diff --git a/backend/src/routes/api/images/imageUtils.ts b/backend/src/routes/api/images/imageUtils.ts index f5c0492920..10fca0f949 100644 --- a/backend/src/routes/api/images/imageUtils.ts +++ b/backend/src/routes/api/images/imageUtils.ts @@ -14,6 +14,9 @@ import { import { FastifyRequest } from 'fastify'; import createError from 'http-errors'; +export const IMAGE_URL_REGEXP = + /^([\w.\-_]+(?::\d+|)(?=\/[a-z0-9._-]+\/[a-z0-9._-]+)|)(?:\/|)([a-z0-9.\-_]+(?:\/[a-z0-9.\-_]+|))(?::([\w.\-_]{1,127})|)/; + /** * This function uses a regex to match the image location string * The match result will return an array of 4 elements: @@ -23,10 +26,8 @@ import createError from 'http-errors'; export const parseImageURL = ( imageString: string, ): { fullURL: string; host: string; image: string; tag: string } => { - const imageUrlRegex = - /^([\w.\-_]+(?::\d+|)(?=\/[a-z0-9._-]+\/[a-z0-9._-]+)|)(?:\/|)([a-z0-9.\-_]+(?:\/[a-z0-9.\-_]+|))(?::([\w.\-_]{1,127})|)/; const trimmedString = imageString.trim(); - const result = trimmedString.match(imageUrlRegex); + const result = trimmedString.match(IMAGE_URL_REGEXP); if (!result) { return { fullURL: trimmedString, diff --git a/backend/src/utils/constants.ts b/backend/src/utils/constants.ts index 8975d86e72..cb8905e8c9 100644 --- a/backend/src/utils/constants.ts +++ b/backend/src/utils/constants.ts @@ -137,9 +137,6 @@ export const DEFAULT_NOTEBOOK_SIZES: NotebookSize[] = [ }, ]; -export const imageUrlRegex = - /^([\w.\-_]+((?::\d+|)(?=\/[a-z0-9._-]+\/[a-z0-9._-]+))|)(?:\/|)([a-z0-9.\-_]+(?:\/[a-z0-9.\-_]+|))(?::([\w.\-_]{1,127})|)/; - export const THANOS_RBAC_PORT = '9092'; export const THANOS_INSTANCE_NAME = 'thanos-querier'; export const THANOS_NAMESPACE = 'openshift-monitoring'; diff --git a/frontend/src/concepts/areas/__tests__/utils.spec.ts b/frontend/src/concepts/areas/__tests__/utils.spec.ts new file mode 100644 index 0000000000..548c7451cf --- /dev/null +++ b/frontend/src/concepts/areas/__tests__/utils.spec.ts @@ -0,0 +1,259 @@ +import { mockDscStatus } from '~/__mocks__/mockDscStatus'; +import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; +import { StackCapability, StackComponent, SupportedArea } from '~/concepts/areas/types'; +import { SupportedAreasStateMap } from '~/concepts/areas/const'; +import { mockDsciStatus } from '~/__mocks__/mockDsciStatus'; +import { isAreaAvailable } from '~/concepts/areas/utils'; + +describe('isAreaAvailable', () => { + describe('v1 Operator (deprecated)', () => { + it('should enable component (flag true)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.DS_PIPELINES, + mockDashboardConfig({ disablePipelines: false }).spec, + null, + null, + ); + + expect(isAvailable.status).toBe(true); + expect(isAvailable.featureFlags).toEqual({ disablePipelines: 'on' }); + expect(isAvailable.reliantAreas).toBe(null); + expect(isAvailable.requiredComponents).toBe(null); + }); + + it('should disable component (flag false)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.DS_PIPELINES, + mockDashboardConfig({ disablePipelines: true }).spec, + null, + null, + ); + + expect(isAvailable.status).not.toBe(true); + expect(isAvailable.featureFlags).toEqual({ disablePipelines: 'off' }); + expect(isAvailable.reliantAreas).toBe(null); + expect(isAvailable.requiredComponents).toBe(null); + }); + + it('should enable area when not a feature flag component', () => { + const isAvailable = isAreaAvailable( + SupportedArea.WORKBENCHES, + mockDashboardConfig({}).spec, + null, + null, + ); + + expect(isAvailable.status).toBe(true); + expect(isAvailable.featureFlags).toBe(null); + expect(isAvailable.reliantAreas).toEqual({ [SupportedArea.DS_PROJECTS_VIEW]: true }); + expect(isAvailable.requiredComponents).toBe(null); + }); + }); + + describe('v2 Operator', () => { + describe('flags and cluster states', () => { + it('should enable area (flag true, cluster true)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.DS_PIPELINES, + mockDashboardConfig({ disablePipelines: false }).spec, + mockDscStatus({ installedComponents: { [StackComponent.DS_PIPELINES]: true } }), + mockDsciStatus({}), + ); + + expect(isAvailable.status).toBe(true); + expect(isAvailable.featureFlags).toEqual({ disablePipelines: 'on' }); + expect(isAvailable.reliantAreas).toBe(null); + expect(isAvailable.requiredComponents).toEqual({ [StackComponent.DS_PIPELINES]: true }); + }); + + it('should disable area (flag true, cluster false)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.DS_PIPELINES, + mockDashboardConfig({ disablePipelines: false }).spec, + mockDscStatus({ installedComponents: { [StackComponent.DS_PIPELINES]: false } }), + mockDsciStatus({}), + ); + + expect(isAvailable.status).not.toBe(true); + expect(isAvailable.featureFlags).toEqual({ disablePipelines: 'on' }); + expect(isAvailable.reliantAreas).toBe(null); + expect(isAvailable.requiredComponents).toEqual({ [StackComponent.DS_PIPELINES]: false }); + }); + + it('should disable area (flag false, cluster true)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.DS_PIPELINES, + mockDashboardConfig({ disablePipelines: true }).spec, + mockDscStatus({ installedComponents: { [StackComponent.DS_PIPELINES]: true } }), + mockDsciStatus({}), + ); + + expect(isAvailable.status).not.toBe(true); + expect(isAvailable.featureFlags).toEqual({ disablePipelines: 'off' }); + expect(isAvailable.reliantAreas).toBe(null); + expect(isAvailable.requiredComponents).toEqual({ [StackComponent.DS_PIPELINES]: true }); + }); + + it('should disable area (flag false, cluster false)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.DS_PIPELINES, + mockDashboardConfig({ disablePipelines: true }).spec, + mockDscStatus({ installedComponents: { [StackComponent.DS_PIPELINES]: false } }), + mockDsciStatus({}), + ); + + expect(isAvailable.status).not.toBe(true); + expect(isAvailable.featureFlags).toEqual({ disablePipelines: 'off' }); + expect(isAvailable.reliantAreas).toBe(null); + expect(isAvailable.requiredComponents).toEqual({ [StackComponent.DS_PIPELINES]: false }); + }); + + it('should enable area (no flag, cluster true)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.WORKBENCHES, + mockDashboardConfig({}).spec, + mockDscStatus({ installedComponents: { [StackComponent.WORKBENCHES]: true } }), + mockDsciStatus({}), + ); + + expect(isAvailable.status).toBe(true); + expect(isAvailable.featureFlags).toBe(null); + expect(isAvailable.reliantAreas).toEqual({ [SupportedArea.DS_PROJECTS_VIEW]: true }); + expect(isAvailable.requiredComponents).toEqual({ [StackComponent.WORKBENCHES]: true }); + }); + + it('should disable area (no flag, cluster false)', () => { + const isAvailable = isAreaAvailable( + SupportedArea.WORKBENCHES, + mockDashboardConfig({}).spec, + mockDscStatus({ installedComponents: { [StackComponent.WORKBENCHES]: false } }), + mockDsciStatus({}), + ); + + expect(isAvailable.status).not.toBe(true); + expect(isAvailable.featureFlags).toBe(null); + expect(isAvailable.reliantAreas).toEqual({ [SupportedArea.DS_PROJECTS_VIEW]: true }); + expect(isAvailable.requiredComponents).toEqual({ [StackComponent.WORKBENCHES]: false }); + }); + }); + + /** + * These tests rely on Custom Serving Runtime being in a specific configuration, we may need to replace + * these tests if these become obsolete. + */ + describe('reliantAreas', () => { + it('should enable area if at least one reliant area is enabled', () => { + // Make sure this test is valid + expect(SupportedAreasStateMap[SupportedArea.CUSTOM_RUNTIMES].reliantAreas).toEqual([ + SupportedArea.MODEL_SERVING, + ]); + + // Test both reliant areas + const isAvailableReliantCustomRuntimes = isAreaAvailable( + SupportedArea.CUSTOM_RUNTIMES, + mockDashboardConfig({ disableModelServing: false }).spec, + mockDscStatus({}), + mockDsciStatus({}), + ); + + expect(isAvailableReliantCustomRuntimes.status).toBe(true); + expect(isAvailableReliantCustomRuntimes.featureFlags).toEqual({ + disableCustomServingRuntimes: 'on', + }); + expect(isAvailableReliantCustomRuntimes.reliantAreas).toEqual({ + [SupportedArea.MODEL_SERVING]: true, + }); + expect(isAvailableReliantCustomRuntimes.requiredComponents).toBe(null); + }); + + it('should disable area if reliant areas are all disabled', () => { + // Make sure this test is valid + expect(SupportedAreasStateMap[SupportedArea.CUSTOM_RUNTIMES].reliantAreas).toEqual([ + SupportedArea.MODEL_SERVING, + ]); + + // Test areas disabled + const isAvailable = isAreaAvailable( + SupportedArea.CUSTOM_RUNTIMES, + mockDashboardConfig({ disableModelServing: true }).spec, + mockDscStatus({}), + mockDsciStatus({}), + ); + + expect(isAvailable.status).not.toBe(true); + expect(isAvailable.featureFlags).toEqual({ disableCustomServingRuntimes: 'on' }); + expect(isAvailable.reliantAreas).toEqual({ + [SupportedArea.MODEL_SERVING]: false, + }); + expect(isAvailable.requiredComponents).toBe(null); + }); + }); + + describe('requiredCapabilities', () => { + it('should enable area if both capabilities are enabled', () => { + // Make sure this test is valid + expect(SupportedAreasStateMap[SupportedArea.K_SERVE_AUTH].requiredCapabilities).toEqual([ + StackCapability.SERVICE_MESH, + StackCapability.SERVICE_MESH_AUTHZ, + ]); + + // Test both reliant areas + const isAvailableKserveAuth = isAreaAvailable( + SupportedArea.K_SERVE_AUTH, + mockDashboardConfig({ disableKServeAuth: false }).spec, + mockDscStatus({ + installedComponents: { + [StackComponent.K_SERVE]: true, + }, + }), + mockDsciStatus({ + requiredCapabilities: [ + StackCapability.SERVICE_MESH, + StackCapability.SERVICE_MESH_AUTHZ, + ], + }), + ); + + expect(isAvailableKserveAuth.status).toBe(true); + expect(isAvailableKserveAuth.featureFlags).toEqual({ + disableKServeAuth: 'on', + }); + expect(isAvailableKserveAuth.requiredCapabilities).toEqual({ + [StackCapability.SERVICE_MESH]: true, + [StackCapability.SERVICE_MESH_AUTHZ]: true, + }); + }); + + it('should enable area if one capability is missing', () => { + // Make sure this test is valid + expect(SupportedAreasStateMap[SupportedArea.K_SERVE_AUTH].requiredCapabilities).toEqual([ + StackCapability.SERVICE_MESH, + StackCapability.SERVICE_MESH_AUTHZ, + ]); + + // Test both reliant areas + const isAvailableKserveAuth = isAreaAvailable( + SupportedArea.K_SERVE_AUTH, + mockDashboardConfig({ disableKServeAuth: false }).spec, + mockDscStatus({ + installedComponents: { + [StackComponent.K_SERVE]: true, + }, + }), + mockDsciStatus({ + requiredCapabilities: [StackCapability.SERVICE_MESH], + }), + ); + + expect(isAvailableKserveAuth.status).toBe(false); + expect(isAvailableKserveAuth.featureFlags).toEqual({ + disableKServeAuth: 'on', + }); + expect(isAvailableKserveAuth.requiredCapabilities).toEqual({ + [StackCapability.SERVICE_MESH]: true, + [StackCapability.SERVICE_MESH_AUTHZ]: false, + }); + }); + }); + }); +});