diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/__tests__/index.spec.tsx index 00c2fcc9c..8ed8ada76 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/__tests__/index.spec.tsx @@ -78,6 +78,9 @@ const devfile = { }, } as devfileApi.Devfile; +// mute console.error +console.error = jest.fn(); + describe('Creating steps, applying a devfile', () => { let searchParams: URLSearchParams; let factoryId: string; @@ -245,6 +248,7 @@ describe('Creating steps, applying a devfile', () => { factoryId, undefined, false, + undefined, ), ); await waitFor(() => expect(mockCreateWorkspaceFromDevfile).toHaveBeenCalled()); @@ -334,6 +338,7 @@ describe('Creating steps, applying a devfile', () => { factoryId, undefined, false, + undefined, ), ); await waitFor(() => expect(mockCreateWorkspaceFromDevfile).toHaveBeenCalled()); @@ -410,6 +415,7 @@ describe('Creating steps, applying a devfile', () => { factoryId, undefined, false, + undefined, ), ); await waitFor(() => expect(mockCreateWorkspaceFromDevfile).toHaveBeenCalled()); @@ -488,6 +494,7 @@ describe('Creating steps, applying a devfile', () => { factoryId, undefined, false, + undefined, ), ); await waitFor(() => expect(mockCreateWorkspaceFromDevfile).toHaveBeenCalled()); @@ -511,7 +518,7 @@ describe('Creating steps, applying a devfile', () => { await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS); await waitFor(() => - expect(prepareDevfile).toHaveBeenCalledWith(devfile, factoryId, undefined, true), + expect(prepareDevfile).toHaveBeenCalledWith(devfile, factoryId, undefined, true, undefined), ); }); @@ -534,7 +541,7 @@ describe('Creating steps, applying a devfile', () => { await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS); await waitFor(() => - expect(prepareDevfile).toHaveBeenCalledWith(devfile, factoryId, undefined, true), + expect(prepareDevfile).toHaveBeenCalledWith(devfile, factoryId, undefined, true, undefined), ); }); @@ -554,7 +561,13 @@ describe('Creating steps, applying a devfile', () => { await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS); await waitFor(() => - expect(prepareDevfile).toHaveBeenCalledWith(devfile, factoryId, undefined, false), + expect(prepareDevfile).toHaveBeenCalledWith( + devfile, + factoryId, + undefined, + false, + undefined, + ), ); }); }); @@ -679,7 +692,7 @@ describe('Creating steps, applying a devfile', () => { expect(mockOnNextStep).not.toHaveBeenCalled(); expect(mockOnError).not.toHaveBeenCalled(); - expect(mockCreateWorkspaceFromDevfile).toHaveBeenCalledTimes(1); + await waitFor(() => expect(mockCreateWorkspaceFromDevfile).toHaveBeenCalledTimes(1)); }); test('action callback to continue with default devfile', async () => { diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/__tests__/prepareDevfile.spec.ts b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/__tests__/prepareDevfile.spec.ts index acdeaec56..f6eb0e195 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/__tests__/prepareDevfile.spec.ts +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/__tests__/prepareDevfile.spec.ts @@ -26,7 +26,7 @@ jest.mock('@/services/helpers/generateName'); describe('FactoryLoaderContainer/prepareDevfile', () => { describe('DEVWORKSPACE_METADATA_ANNOTATION attribute', () => { - test('add the attribute with annotation', () => { + test('add the attribute with annotation', async () => { const devfile = { schemaVersion: '2.2.0', metadata: { @@ -49,7 +49,7 @@ describe('FactoryLoaderContainer/prepareDevfile', () => { }); }); - test('update the attribute with annotation', () => { + test('update the attribute with annotation', async () => { const customAnnotation = { 'custom-annotation': 'value', }; @@ -83,7 +83,7 @@ describe('FactoryLoaderContainer/prepareDevfile', () => { ); }); - test('update the attribute with annotation - bad DEVWORKSPACE_METADATA_ANNOTATION', () => { + test('update the attribute with annotation - bad DEVWORKSPACE_METADATA_ANNOTATION', async () => { const factoryId = 'url=https://devfile-location'; const factorySource = { [DEVWORKSPACE_DEVFILE_SOURCE]: dump({ @@ -113,7 +113,7 @@ describe('FactoryLoaderContainer/prepareDevfile', () => { expect(newDevfile.attributes?.[DEVWORKSPACE_METADATA_ANNOTATION]).toEqual(factorySource); }); - test('update the attribute with annotation - bad DEVWORKSPACE_DEVFILE_SOURCE', () => { + test('update the attribute with annotation - bad DEVWORKSPACE_DEVFILE_SOURCE', async () => { const factoryId = 'url=https://devfile-location'; const factorySource = { [DEVWORKSPACE_DEVFILE_SOURCE]: dump({ @@ -147,7 +147,7 @@ describe('FactoryLoaderContainer/prepareDevfile', () => { }); describe('DevWorkspace name', () => { - it('should not change the name', () => { + it('should not change the name', async () => { const factoryId = 'url=https://devfile-location'; const devfile = { schemaVersion: '2.2.0', @@ -161,7 +161,7 @@ describe('FactoryLoaderContainer/prepareDevfile', () => { expect(newDevfile.metadata.name).toEqual('wksp-test'); }); - it('should append a suffix to the name', () => { + it('should append a suffix to the name', async () => { const factoryId = 'url=https://devfile-location'; const devfile = { schemaVersion: '2.2.0', @@ -175,7 +175,7 @@ describe('FactoryLoaderContainer/prepareDevfile', () => { expect(newDevfile.metadata.name).toEqual('wksp-test1234'); }); - it('should generate a new name #1', () => { + it('should generate a new name #1', async () => { const factoryId = 'url=https://devfile-location'; const devfile = { schemaVersion: '2.2.0', @@ -189,7 +189,7 @@ describe('FactoryLoaderContainer/prepareDevfile', () => { expect(newDevfile.metadata.name).toEqual('wksp-1234'); }); - it('should generate a new name #2', () => { + it('should generate a new name #2', async () => { const factoryId = 'url=https://devfile-location'; const devfile = { schemaVersion: '2.2.0', @@ -213,7 +213,7 @@ describe('FactoryLoaderContainer/prepareDevfile', () => { }, } as devfileApi.Devfile; - test('default storage type', () => { + test('default storage type', async () => { const newDevfile = prepareDevfile(devfile, factoryId, undefined, false); expect(newDevfile.metadata.attributes?.[DEVWORKSPACE_STORAGE_TYPE_ATTR]).toBeUndefined(); @@ -226,5 +226,112 @@ describe('FactoryLoaderContainer/prepareDevfile', () => { expect(newDevfile.metadata.attributes?.[DEVWORKSPACE_STORAGE_TYPE_ATTR]).toBeUndefined(); expect(newDevfile.attributes?.[DEVWORKSPACE_STORAGE_TYPE_ATTR]).toEqual('ephemeral'); }); + + describe('has parent', () => { + describe('with registryUrl', () => { + it('with storage-type attribute', async () => { + // mute console logs + console.warn = jest.fn(); + const devfile = { + schemaVersion: '2.2.0', + metadata: { + name: 'wksp-test', + }, + parent: { + id: 'nodejs', + registryUrl: 'https://registry.devfile.io/', + }, + } as devfileApi.Devfile; + + const newDevfile = prepareDevfile(devfile, factoryId, 'ephemeral', false, { + schemaVersion: '2.2.2', + metadata: { + generateName: 'nodejs', + }, + attributes: { + 'controller.devfile.io/storage-type': 'ephemeral', + }, + } as devfileApi.Devfile); + + expect(console.warn).toHaveBeenCalledWith( + 'Unable to apply controller.devfile.io/storage-type attribute.', + ); + expect(newDevfile.attributes?.[DEVWORKSPACE_STORAGE_TYPE_ATTR]).toBeUndefined(); + }); + + it('without storage-type attribute', async () => { + const devfile = { + schemaVersion: '2.2.0', + metadata: { + name: 'wksp-test', + }, + parent: { + id: 'nodejs', + registryUrl: 'https://registry.devfile.io/', + }, + } as devfileApi.Devfile; + + const newDevfile = prepareDevfile(devfile, factoryId, 'ephemeral', false, { + schemaVersion: '2.2.2', + metadata: { + generateName: 'nodejs', + }, + } as devfileApi.Devfile); + + expect(newDevfile.attributes?.[DEVWORKSPACE_STORAGE_TYPE_ATTR]).toEqual('ephemeral'); + }); + }); + describe('with uri', () => { + it('with storage-type attribute', async () => { + // mute console logs + console.warn = jest.fn(); + const devfile = { + schemaVersion: '2.2.0', + metadata: { + name: 'wksp-test', + }, + parent: { + uri: 'https://raw.githubusercontent.com/test/devfile.yaml', + }, + } as devfileApi.Devfile; + + const newDevfile = prepareDevfile(devfile, factoryId, 'ephemeral', false, { + schemaVersion: '2.2.2', + metadata: { + generateName: 'nodejs', + }, + attributes: { + 'controller.devfile.io/storage-type': 'ephemeral', + }, + } as devfileApi.Devfile); + + expect(console.warn).toHaveBeenCalledWith( + 'Unable to apply controller.devfile.io/storage-type attribute.', + ); + expect(newDevfile.attributes?.[DEVWORKSPACE_STORAGE_TYPE_ATTR]).toBeUndefined(); + }); + + it('without storage-type attribute', async () => { + const devfile = { + schemaVersion: '2.2.0', + metadata: { + name: 'wksp-test', + }, + parent: { + uri: 'https://raw.githubusercontent.com/test/devfile.yaml', + }, + } as devfileApi.Devfile; + + const newDevfile = prepareDevfile(devfile, factoryId, 'ephemeral', false, { + schemaVersion: '2.2.2', + metadata: { + generateName: 'nodejs', + }, + } as devfileApi.Devfile); + + expect(newDevfile.attributes?.[DEVWORKSPACE_STORAGE_TYPE_ATTR]).toEqual('ephemeral'); + }); + }); + }); }); }); diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/index.tsx b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/index.tsx index 01135cf94..52b2105f2 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/index.tsx +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/index.tsx @@ -45,6 +45,7 @@ import { RootState } from '@/store'; import { selectDefaultDevfile } from '@/store/DevfileRegistries/selectors'; import { selectFactoryResolver } from '@/store/FactoryResolver/selectors'; import { selectDefaultNamespace } from '@/store/InfrastructureNamespaces/selectors'; +import { selectPvcStrategy } from '@/store/ServerConfig'; import { workspacesActionCreators } from '@/store/Workspaces'; import { selectDevWorkspaceWarnings } from '@/store/Workspaces/devWorkspaces/selectors'; import { selectAllWorkspaces } from '@/store/Workspaces/selectors'; @@ -173,10 +174,10 @@ class CreatingStepApplyDevfile extends ProgressStep { this.prepareAndRun(); } - private updateCurrentDevfile(devfile: devfileApi.Devfile): void { + private async updateCurrentDevfile(devfile: devfileApi.Devfile): Promise { const { factoryResolver, allWorkspaces, defaultDevfile } = this.props; const { factoryParams } = this.state; - const { factoryId, policiesCreate, sourceUrl, storageType, remotes } = factoryParams; + const { factoryId, policiesCreate, sourceUrl, remotes } = factoryParams; // when using the default devfile instead of a user devfile if (factoryResolver === undefined && isEqual(devfile, defaultDevfile)) { @@ -214,8 +215,16 @@ class CreatingStepApplyDevfile extends ProgressStep { // test the devfile name to decide if we need to append a suffix to is const nameConflict = allWorkspaces.some(w => devfile.metadata.name === w.name); + const storageType = factoryParams.storageType || this.props.preferredStorageType || undefined; const appendSuffix = policiesCreate === 'perclick' || nameConflict; - const updatedDevfile = prepareDevfile(devfile, factoryId, storageType, appendSuffix); + const parentDevfile = factoryResolver?.parentDevfile; + const updatedDevfile = prepareDevfile( + devfile, + factoryId, + storageType, + appendSuffix, + parentDevfile, + ); this.setState({ devfile: updatedDevfile, @@ -265,7 +274,7 @@ class CreatingStepApplyDevfile extends ProgressStep { throw new Error('Failed to resolve the default devfile.'); } const _devfile = cloneDeep(defaultDevfile); - this.updateCurrentDevfile(_devfile); + await this.updateCurrentDevfile(_devfile); } else { try { await this.createWorkspaceFromDevfile(devfile); @@ -296,7 +305,7 @@ class CreatingStepApplyDevfile extends ProgressStep { _devfile.metadata.generateName = metadata.generateName; } - this.updateCurrentDevfile(_devfile); + await this.updateCurrentDevfile(_devfile); return false; } @@ -317,7 +326,7 @@ class CreatingStepApplyDevfile extends ProgressStep { throw new Error('Failed to resolve the devfile.'); } const _devfile = cloneDeep(resolvedDevfile); - this.updateCurrentDevfile(_devfile); + await this.updateCurrentDevfile(_devfile); } else { const { devfile } = this.state; if (devfile) { @@ -465,6 +474,7 @@ const mapStateToProps = (state: RootState) => ({ factoryResolver: selectFactoryResolver(state), defaultDevfile: selectDefaultDevfile(state), devWorkspaceWarnings: selectDevWorkspaceWarnings(state), + preferredStorageType: selectPvcStrategy(state), }); const connector = connect(mapStateToProps, workspacesActionCreators, null, { diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/prepareDevfile.ts b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/prepareDevfile.ts index fcb2791b8..6b607cdcb 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/prepareDevfile.ts +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/prepareDevfile.ts @@ -31,6 +31,7 @@ export function prepareDevfile( factoryId: string, storageType: che.WorkspaceStorageType | undefined, appendSuffix: boolean, + parentDevfile?: devfileApi.Devfile | undefined, ): devfileApi.Devfile { const devfile = cloneDeep(_devfile); const attributes = DevfileAdapter.getAttributes(devfile); @@ -60,8 +61,15 @@ export function prepareDevfile( devfile.metadata.name = sanitizeName(devfile.metadata.name); // propagate storage type - if (storageType === 'ephemeral') { - attributes[DEVWORKSPACE_STORAGE_TYPE_ATTR] = 'ephemeral'; + if (storageType) { + attributes[DEVWORKSPACE_STORAGE_TYPE_ATTR] = storageType; + } + if (parentDevfile && attributes[DEVWORKSPACE_STORAGE_TYPE_ATTR]) { + const parentDevfileAttributes = DevfileAdapter.getAttributes(parentDevfile); + if (parentDevfileAttributes[DEVWORKSPACE_STORAGE_TYPE_ATTR]) { + delete attributes[DEVWORKSPACE_STORAGE_TYPE_ATTR]; + console.warn(`Unable to apply ${DEVWORKSPACE_STORAGE_TYPE_ATTR} attribute.`); + } } return devfile; diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Resources/index.tsx b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Resources/index.tsx index 87ceda7cc..f7ab74f06 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Resources/index.tsx +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Resources/index.tsx @@ -41,6 +41,7 @@ import { selectDevWorkspaceResources } from '@/store/DevfileRegistries/selectors import { factoryResolverActionCreators } from '@/store/FactoryResolver'; import { selectFactoryResolver } from '@/store/FactoryResolver/selectors'; import { selectDefaultNamespace } from '@/store/InfrastructureNamespaces/selectors'; +import { selectPvcStrategy } from '@/store/ServerConfig'; import { workspacesActionCreators } from '@/store/Workspaces'; import { selectDevWorkspaceWarnings } from '@/store/Workspaces/devWorkspaces/selectors'; import { selectAllWorkspaces } from '@/store/Workspaces/selectors'; @@ -174,7 +175,7 @@ class CreatingStepApplyResources extends ProgressStep { protected async runStep(): Promise { const { devWorkspaceResources } = this.props; const { factoryParams, shouldCreate, resources, warning } = this.state; - const { cheEditor, factoryId, sourceUrl, storageType, policiesCreate } = factoryParams; + const { cheEditor, factoryId, sourceUrl, policiesCreate } = factoryParams; if (warning) { const newName = `Warning: ${warning}`; @@ -202,7 +203,7 @@ class CreatingStepApplyResources extends ProgressStep { return true; } - if (shouldCreate === false) { + if (!shouldCreate) { throw new Error('The workspace creation unexpectedly failed.'); } @@ -218,6 +219,7 @@ class CreatingStepApplyResources extends ProgressStep { ); const appendSuffix = policiesCreate === 'perclick' || nameConflict; + const storageType = factoryParams.storageType || this.props.preferredStorageType || undefined; // create a workspace using pre-generated resources const [devWorkspace, devWorkspaceTemplate] = prepareResources( _resources, @@ -297,6 +299,7 @@ const mapStateToProps = (state: RootState) => ({ factoryResolver: selectFactoryResolver(state), devWorkspaceResources: selectDevWorkspaceResources(state), devWorkspaceWarnings: selectDevWorkspaceWarnings(state), + preferredStorageType: selectPvcStrategy(state), }); const connector = connect( diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Resources/prepareResources.ts b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Resources/prepareResources.ts index 8c7b15e98..84ecf6a6d 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Resources/prepareResources.ts +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Resources/prepareResources.ts @@ -45,7 +45,7 @@ export default function prepareResources( } // set storage type attribute - if (storageType === 'ephemeral') { + if (storageType) { if (!devWorkspace.spec.template.attributes) { devWorkspace.spec.template.attributes = {}; } diff --git a/packages/dashboard-frontend/src/services/backend-client/__tests__/factoryApi.spec.ts b/packages/dashboard-frontend/src/services/backend-client/__tests__/factoryApi.spec.ts index 955289661..abcc5a72f 100644 --- a/packages/dashboard-frontend/src/services/backend-client/__tests__/factoryApi.spec.ts +++ b/packages/dashboard-frontend/src/services/backend-client/__tests__/factoryApi.spec.ts @@ -16,6 +16,15 @@ import { getFactoryResolver, refreshFactoryOauthToken } from '@/services/backend import devfileApi from '@/services/devfileApi'; import { FactoryResolver } from '@/services/helpers/types'; +const mockFetchParentDevfile = jest.fn(); +jest.mock('@/services/backend-client/parentDevfileApi', () => { + return { + getParentDevfile: async (href: string) => { + return mockFetchParentDevfile(href); + }, + }; +}); + describe('Factory API', () => { const mockPost = mockAxios.post as jest.Mock; @@ -42,6 +51,9 @@ describe('Factory API', () => { }); describe('resolve factory', () => { + beforeEach(() => { + mockFetchParentDevfile.mockResolvedValueOnce(expect.anything()); + }); it('should call "/factory/resolver"', async () => { mockPost.mockResolvedValueOnce({ data: expect.anything(), @@ -54,6 +66,7 @@ describe('Factory API', () => { expect(mockPost).toHaveBeenCalledWith('/api/factory/resolver', { url: 'https://test.azure.com/_git/public-repo?version=GBtest/branch', }); + expect(mockFetchParentDevfile).toHaveBeenCalled(); }); it('should return a factory resolver', async () => { @@ -63,6 +76,7 @@ describe('Factory API', () => { const res = await getFactoryResolver(location, {}); + expect(mockFetchParentDevfile).toHaveBeenCalled(); expect(res).toEqual(factoryResolver); }); }); diff --git a/packages/dashboard-frontend/src/services/backend-client/__tests__/parentDevfileApi.spec.ts b/packages/dashboard-frontend/src/services/backend-client/__tests__/parentDevfileApi.spec.ts new file mode 100644 index 000000000..c9c01a87f --- /dev/null +++ b/packages/dashboard-frontend/src/services/backend-client/__tests__/parentDevfileApi.spec.ts @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2018-2024 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { dump } from 'js-yaml'; + +import { getParentDevfile } from '@/services/backend-client/parentDevfileApi'; +import devfileApi from '@/services/devfileApi'; + +const mockFetchRemoteData = jest.fn(); +jest.mock('@/services/backend-client/dataResolverApi', () => { + return { + getDataResolver: async (href: string) => { + return mockFetchRemoteData(href); + }, + }; +}); + +const _parentDevfile = { + schemaVersion: '2.2.0', + metadata: { + name: 'nodejs', + }, +}; + +describe('Parent Devfile API', () => { + let devfile: devfileApi.Devfile; + + beforeEach(() => { + devfile = { + schemaVersion: '2.2.0', + metadata: { + name: 'wksp-test', + }, + } as devfileApi.Devfile; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('devfile without a parent', () => { + it('should return undefined', async () => { + const parentDevfile = await getParentDevfile(devfile); + + expect(mockFetchRemoteData).not.toHaveBeenCalled(); + expect(parentDevfile).toBeUndefined(); + }); + }); + describe('devfile with a parent', () => { + describe('with registryUrl', () => { + beforeEach(() => { + devfile.parent = { + id: 'nodejs', + registryUrl: 'https://registry.devfile.io/', + }; + }); + + it('should fetch parent devfile', async () => { + mockFetchRemoteData.mockResolvedValueOnce(dump(_parentDevfile)); + + const parentDevfile = await getParentDevfile(devfile); + + expect(mockFetchRemoteData).toHaveBeenCalledWith( + 'https://registry.devfile.io//devfiles/nodejs', + ); + expect(parentDevfile).toEqual(_parentDevfile); + }); + }); + describe('with uri', () => { + beforeEach(() => { + devfile.parent = { + uri: 'https://raw.githubusercontent.com/test/devfile.yaml', + }; + }); + + it('should fetch parent devfile', async () => { + mockFetchRemoteData.mockResolvedValueOnce(dump(_parentDevfile)); + + const parentDevfile = await getParentDevfile(devfile); + + expect(mockFetchRemoteData).toHaveBeenCalledWith( + 'https://raw.githubusercontent.com/test/devfile.yaml', + ); + expect(parentDevfile).toEqual(_parentDevfile); + }); + + it('should return undefined on error happens', async () => { + const error = new Error('Not Found'); + // mute the outputs + console.error = jest.fn(); + mockFetchRemoteData.mockRejectedValue(error); + + const parentDevfile = await getParentDevfile(devfile); + + expect(mockFetchRemoteData).toHaveBeenCalledWith( + 'https://raw.githubusercontent.com/test/devfile.yaml', + ); + + expect(console.error).toHaveBeenCalledWith('Failed to fetch parent devfile', error); + expect(parentDevfile).toBeUndefined(); + }); + }); + }); +}); diff --git a/packages/dashboard-frontend/src/services/backend-client/factoryApi.ts b/packages/dashboard-frontend/src/services/backend-client/factoryApi.ts index 48883a967..1fc317fb0 100644 --- a/packages/dashboard-frontend/src/services/backend-client/factoryApi.ts +++ b/packages/dashboard-frontend/src/services/backend-client/factoryApi.ts @@ -13,6 +13,7 @@ import axios from 'axios'; import { cheServerPrefix } from '@/services/backend-client/const'; +import { getParentDevfile } from '@/services/backend-client/parentDevfileApi'; import { FactoryResolver } from '@/services/helpers/types'; export async function getFactoryResolver( @@ -32,7 +33,13 @@ export async function getFactoryResolver( Object.assign({}, overrideParams, { url }), ); - return response.data; + const factoryResolver: FactoryResolver = response.data; + + if (factoryResolver) { + factoryResolver.parentDevfile = await getParentDevfile(factoryResolver.devfile); + } + + return factoryResolver; } export async function refreshFactoryOauthToken(url: string): Promise { diff --git a/packages/dashboard-frontend/src/services/backend-client/parentDevfileApi.ts b/packages/dashboard-frontend/src/services/backend-client/parentDevfileApi.ts new file mode 100644 index 000000000..a5642387d --- /dev/null +++ b/packages/dashboard-frontend/src/services/backend-client/parentDevfileApi.ts @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018-2024 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { load } from 'js-yaml'; + +import { getDataResolver } from '@/services/backend-client/dataResolverApi'; +import devfileApi, { isDevfileV2 } from '@/services/devfileApi'; + +export async function getParentDevfile(devfile: unknown): Promise { + if (isDevfileV2(devfile) && devfile.parent) { + let uri: string | undefined; + if (devfile.parent.uri) { + uri = devfile.parent.uri; + } else if (devfile.parent.id && devfile.parent.registryUrl) { + uri = `${devfile.parent.registryUrl}/devfiles/${devfile.parent.id}`; + } + if (uri) { + try { + const data = await getDataResolver(uri); + if (typeof data === 'string') { + const parentDevfile = load(data); + if (isDevfileV2(parentDevfile)) { + return parentDevfile; + } + } + } catch (e) { + console.error('Failed to fetch parent devfile', e); + } + } + } + + return undefined; +} diff --git a/packages/dashboard-frontend/src/services/helpers/types.ts b/packages/dashboard-frontend/src/services/helpers/types.ts index 9fd39affe..667bd463e 100644 --- a/packages/dashboard-frontend/src/services/helpers/types.ts +++ b/packages/dashboard-frontend/src/services/helpers/types.ts @@ -37,6 +37,7 @@ export interface AlertItem { // - any other - devfile is found in repository as filename from the value export interface FactoryResolver extends Omit { devfile?: che.api.workspace.devfile.Devfile | devfileApi.Devfile; + parentDevfile?: devfileApi.Devfile; location?: string; scm_info?: FactoryResolverScmInfo; } diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/DevWorkspaceDefaultPluginsHandler.ts b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/DevWorkspaceDefaultPluginsHandler.ts index c72cdeaf8..3c770e483 100644 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/DevWorkspaceDefaultPluginsHandler.ts +++ b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/DevWorkspaceDefaultPluginsHandler.ts @@ -191,12 +191,14 @@ export class DevWorkspaceDefaultPluginsHandler { return true; } - private async patchWorkspaceComponents(workspace: devfileApi.DevWorkspace) { + private async patchWorkspaceComponents( + workspace: devfileApi.DevWorkspace, + ): Promise<{ headers: DwApi.Headers; devWorkspace: devfileApi.DevWorkspace }> { const patch: api.IPatch[] = [ { op: 'replace', path: '/spec/template/components', - value: workspace.spec.template.components, + value: workspace.spec.template.components || [], }, ]; return DwApi.patchWorkspace(workspace.metadata.namespace, workspace.metadata.name, patch); diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/devWorkspaceClient.managePvcStrategy.spec.ts b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/devWorkspaceClient.managePvcStrategy.spec.ts index 8dcc66372..587b5edaa 100644 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/devWorkspaceClient.managePvcStrategy.spec.ts +++ b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/devWorkspaceClient.managePvcStrategy.spec.ts @@ -89,34 +89,6 @@ describe('DevWorkspace client, managePvcStrategy', () => { ]); }); - it('should update devWorkspace storage type', async () => { - const devWorkspace = devWorkspaceBuilder - .withSpec({ - template: { - attributes: { - [DEVWORKSPACE_CONFIG_ATTR]: 'custom-config', - }, - }, - }) - .build(); - const config = { - defaults: { - pvcStrategy: 'custom-pvc-strategy', - }, - cheNamespace: namespace, - } as api.IServerConfig; - - await client.managePvcStrategy(devWorkspace, config); - - expect(spyPatchWorkspace).toHaveBeenCalledWith(namespace, name, [ - { - op: 'add', - path: '/spec/template/attributes/controller.devfile.io~1storage-type', - value: 'custom-pvc-strategy', - }, - ]); - }); - it('should update devWorkspace template components', async () => { const devWorkspace = devWorkspaceBuilder .withSpec({ diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts index 51b4fce07..ba951d65e 100644 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts +++ b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts @@ -32,7 +32,6 @@ import { import { DEVWORKSPACE_CONFIG_ATTR, DEVWORKSPACE_CONTAINER_BUILD_ATTR, - DEVWORKSPACE_STORAGE_TYPE_ATTR, } from '@/services/devfileApi/devWorkspace/spec/template'; import { delay } from '@/services/helpers/delay'; import { isWebTerminal } from '@/services/helpers/devworkspace'; @@ -212,7 +211,7 @@ export class DevWorkspaceClient { { op: 'replace', path: '/spec/template/components', - value: devWorkspace.spec.template.components, + value: devWorkspace.spec.template.components || [], }, ]); } @@ -460,26 +459,6 @@ export class DevWorkspaceClient { } } - const currentPvcStrategy = config.defaults.pvcStrategy; - if (currentPvcStrategy) { - const devworkspaceStorageTypePath = `/spec/template/attributes/${this.escape( - DEVWORKSPACE_STORAGE_TYPE_ATTR, - )}`; - - if (attributes) { - if (!attributes[DEVWORKSPACE_STORAGE_TYPE_ATTR]) { - patch.push({ op: 'add', path: devworkspaceStorageTypePath, value: currentPvcStrategy }); - } - } else { - patch.push({ - op: 'add', - path: '/spec/template/attributes', - value: { [DEVWORKSPACE_STORAGE_TYPE_ATTR]: currentPvcStrategy }, - }); - attributes = {}; - } - } - const openVSXURL = config.pluginRegistry?.openVSXURL || ''; const components = cloneDeep(workspace.spec.template.components); if (components) {