From 2372f836c6c718cc055a15ae3c1ef58cda80fda2 Mon Sep 17 00:00:00 2001 From: Valeriy Svydenko Date: Fri, 31 May 2024 11:20:09 +0300 Subject: [PATCH] fix update DevWorkspaceTemplate functionality; use SA token to read editors from CM Signed-off-by: Valeriy Svydenko --- .../src/routes/api/__tests__/editors.spec.ts | 2 +- .../src/routes/api/editors.ts | 17 +- .../__mocks__/devWorkspaceSpecTemplates.ts | 163 +++++++----- .../__tests__/__mocks__/editorDefinitions.ts | 147 +++++++++++ .../devWorkspaceClient.editorUpdate.spec.ts | 247 ++++++++++++++---- .../devworkspace/devWorkspaceClient.ts | 62 +++-- .../src/store/DevfileRegistries/getEditor.ts | 2 +- .../devWorkspaces/__tests__/actions.spec.ts | 9 +- .../__tests__/updateEditor.spec.ts | 28 +- .../Workspaces/devWorkspaces/updateEditor.ts | 3 +- 10 files changed, 513 insertions(+), 167 deletions(-) create mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/__mocks__/editorDefinitions.ts diff --git a/packages/dashboard-backend/src/routes/api/__tests__/editors.spec.ts b/packages/dashboard-backend/src/routes/api/__tests__/editors.spec.ts index 0d5bf6967..6a27489f0 100644 --- a/packages/dashboard-backend/src/routes/api/__tests__/editors.spec.ts +++ b/packages/dashboard-backend/src/routes/api/__tests__/editors.spec.ts @@ -16,7 +16,7 @@ import { baseApiPath } from '@/constants/config'; import { editorsArray } from '@/routes/api/helpers/__mocks__/getDevWorkspaceClient'; import { setup, teardown } from '@/utils/appBuilder'; -jest.mock('@/routes/api/helpers/getToken'); +jest.mock('@/routes/api/helpers/getServiceAccountToken'); jest.mock('@/routes/api/helpers/getDevWorkspaceClient'); describe('Editors Route', () => { diff --git a/packages/dashboard-backend/src/routes/api/editors.ts b/packages/dashboard-backend/src/routes/api/editors.ts index 49835c495..c8ba31864 100644 --- a/packages/dashboard-backend/src/routes/api/editors.ts +++ b/packages/dashboard-backend/src/routes/api/editors.ts @@ -11,25 +11,20 @@ */ import { FastifyInstance } from 'fastify'; -import { FastifyRequest } from 'fastify/types/request'; import { baseApiPath } from '@/constants/config'; import { getDevWorkspaceClient } from '@/routes/api/helpers/getDevWorkspaceClient'; -import { getToken } from '@/routes/api/helpers/getToken'; +import { getServiceAccountToken } from '@/routes/api/helpers/getServiceAccountToken'; import { getSchema } from '@/services/helpers'; const tags = ['Editor Definitions']; export function registerEditorsRoutes(instance: FastifyInstance) { instance.register(async server => { - server.get( - `${baseApiPath}/editors`, - getSchema({ tags }), - async function (request: FastifyRequest) { - const token = getToken(request); - const { editorsApi } = getDevWorkspaceClient(token); - return editorsApi.list(); - }, - ); + server.get(`${baseApiPath}/editors`, getSchema({ tags }), async () => { + const token = getServiceAccountToken(); + const { editorsApi } = getDevWorkspaceClient(token); + return editorsApi.list(); + }); }); } diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/__mocks__/devWorkspaceSpecTemplates.ts b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/__mocks__/devWorkspaceSpecTemplates.ts index 2d0520193..3d2b91c05 100644 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/__mocks__/devWorkspaceSpecTemplates.ts +++ b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/__mocks__/devWorkspaceSpecTemplates.ts @@ -12,35 +12,56 @@ import devfileApi from '@/services/devfileApi'; -const getDevWorkspaceTemplate = (cpuLimit = '1500m'): devfileApi.DevWorkspaceTemplate => { +const getVSCodeDevWorkspaceTemplate = (cpuLimit = '1500m'): devfileApi.DevWorkspaceTemplate => { return { apiVersion: 'workspace.devfile.io/v1alpha2', kind: 'DevWorkspaceTemplate', metadata: { annotations: { 'che.eclipse.org/components-update-policy': 'managed', - 'che.eclipse.org/plugin-registry-url': - 'https://192.168.64.24.nip.io/plugin-registry/v3/plugins/eclipse/che-theia/next/devfile.yaml', + 'che.eclipse.org/plugin-registry-url': 'che-incubator/che-code/latest', }, - creationTimestamp: new Date('2021-11-24T17:11:37Z'), + creationTimestamp: new Date('2024-05-30T12:51:45Z'), generation: 1, - name: 'theia-ide-workspacee2ade80d625b4f3e', + name: 'che-code-empty-mm9t', namespace: 'admin-che', - resourceVersion: '3766', - uid: '106c3fa1-32c6-47ef-87e5-333de6914837', + ownerReferences: [ + { + apiVersion: 'workspace.devfile.io/v1alpha2', + kind: 'devworkspace', + name: 'empty-mm9t', + uid: 'ca85c4f7-d36d-4f4a-8ce9-b8cf72aa8a37', + }, + ], + resourceVersion: '7429', + uid: 'e4781884-a294-4719-966a-4a4dfce3b7ff', }, spec: { - commands: [], - components: [ + attributes: { + version: null, + }, + commands: [ { - attributes: { - 'app.kubernetes.io/component': 'che-theia', - 'app.kubernetes.io/part-of': 'che-theia.eclipse.org', + apply: { + component: 'che-code-injector', }, + id: 'init-container-command', + }, + { + exec: { + commandLine: + 'nohup /checode/entrypoint-volume.sh > /checode/entrypoint-logs.txt 2>&1 &', + component: 'che-code-runtime-description', + }, + id: 'init-che-code-command', + }, + ], + components: [ + { container: { + command: ['/entrypoint-init-container.sh'], cpuLimit, - cpuRequest: '100m', - endpoints: [], + cpuRequest: '30m', env: [ { name: 'CHE_DASHBOARD_URL', @@ -55,60 +76,73 @@ const getDevWorkspaceTemplate = (cpuLimit = '1500m'): devfileApi.DevWorkspaceTem value: 'plugin-registry-internal-url', }, ], - image: 'quay.io/eclipse/che-theia:next', - memoryLimit: '512M', - mountSources: true, + image: 'quay.io/che-incubator/che-code:latest', + memoryLimit: '256Mi', + memoryRequest: '32Mi', sourceMapping: '/projects', - volumeMounts: [], + volumeMounts: [ + { + name: 'checode', + path: '/checode', + }, + ], }, - name: 'theia-ide', - }, - { - name: 'plugins', - volume: {}, - }, - { - name: 'theia-local', - volume: {}, + name: 'che-code-injector', }, { attributes: { - 'app.kubernetes.io/component': 'machine-exec', - 'app.kubernetes.io/part-of': 'che-theia.eclipse.org', + 'app.kubernetes.io/component': 'che-code-runtime', + 'app.kubernetes.io/part-of': 'che-code.eclipse.org', + 'controller.devfile.io/container-contribution': true, }, container: { - command: ['/go/bin/che-machine-exec', '--url', '127.0.0.1:3333'], cpuLimit: '500m', cpuRequest: '30m', - env: [ + endpoints: [ { - name: 'CHE_DASHBOARD_URL', - value: 'http://localhost', + attributes: { + cookiesAuthEnabled: true, + discoverable: false, + type: 'main', + urlRewriteSupported: true, + }, + exposure: 'public', + name: 'che-code', + protocol: 'https', + secure: true, + targetPort: 3100, }, { - name: 'CHE_PLUGIN_REGISTRY_URL', - value: 'plugin-registry-url', + attributes: { + discoverable: false, + urlRewriteSupported: false, + }, + exposure: 'public', + name: 'code-redirect-1', + protocol: 'https', + targetPort: 13131, }, { - name: 'CHE_PLUGIN_REGISTRY_INTERNAL_URL', - value: 'plugin-registry-internal-url', + attributes: { + discoverable: false, + urlRewriteSupported: false, + }, + exposure: 'public', + name: 'code-redirect-2', + protocol: 'https', + targetPort: 13132, + }, + { + attributes: { + discoverable: false, + urlRewriteSupported: false, + }, + exposure: 'public', + name: 'code-redirect-3', + protocol: 'https', + targetPort: 13133, }, ], - image: 'quay.io/eclipse/che-machine-exec:next', - memoryLimit: '128Mi', - memoryRequest: '32Mi', - sourceMapping: '/projects', - }, - name: 'che-machine-exec', - }, - { - attributes: { - 'app.kubernetes.io/component': 'remote-runtime-injector', - 'app.kubernetes.io/part-of': 'che-theia.eclipse.org', - }, - container: { - cpuLimit: '500m', - cpuRequest: '30m', env: [ { name: 'CHE_DASHBOARD_URL', @@ -123,35 +157,30 @@ const getDevWorkspaceTemplate = (cpuLimit = '1500m'): devfileApi.DevWorkspaceTem value: 'plugin-registry-internal-url', }, ], - image: 'quay.io/eclipse/che-theia-endpoint-runtime-binary:next', - memoryLimit: '128Mi', - memoryRequest: '32Mi', + image: 'quay.io/devfile/universal-developer-image:latest', + memoryLimit: '1024Mi', + memoryRequest: '256Mi', sourceMapping: '/projects', volumeMounts: [ { - name: 'plugins', - path: '/plugins', - }, - { - name: 'remote-endpoint', - path: '/remote-endpoint', + name: 'checode', + path: '/checode', }, ], }, - name: 'remote-runtime-injector', + name: 'che-code-runtime-description', }, { - name: 'remote-endpoint', - volume: { - ephemeral: true, - }, + name: 'checode', + volume: {}, }, ], events: { + postStart: ['init-che-code-command'], preStart: ['init-container-command'], }, }, }; }; -export default getDevWorkspaceTemplate; +export default getVSCodeDevWorkspaceTemplate; diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/__mocks__/editorDefinitions.ts b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/__mocks__/editorDefinitions.ts new file mode 100644 index 000000000..074d8799b --- /dev/null +++ b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/__mocks__/editorDefinitions.ts @@ -0,0 +1,147 @@ +/* + * 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 { V222Devfile } from '@devfile/api'; + +const getVSCodeEditorDefinition = (): V222Devfile => { + return { + attributes: { + version: null, + }, + commands: [ + { + apply: { + component: 'che-code-injector', + }, + id: 'init-container-command', + }, + { + exec: { + commandLine: 'nohup /checode/entrypoint-volume.sh > /checode/entrypoint-logs.txt 2>&1 &', + component: 'che-code-runtime-description', + }, + id: 'init-che-code-command', + }, + ], + components: [ + { + container: { + command: ['/entrypoint-init-container.sh'], + cpuLimit: '500m', + cpuRequest: '30m', + image: 'quay.io/che-incubator/che-code:latest', + memoryLimit: '256Mi', + memoryRequest: '32Mi', + volumeMounts: [ + { + name: 'checode', + path: '/checode', + }, + ], + }, + name: 'che-code-injector', + }, + { + attributes: { + 'app.kubernetes.io/component': 'che-code-runtime', + 'app.kubernetes.io/part-of': 'che-code.eclipse.org', + 'controller.devfile.io/container-contribution': true, + }, + container: { + cpuLimit: '500m', + cpuRequest: '30m', + endpoints: [ + { + attributes: { + cookiesAuthEnabled: true, + discoverable: false, + type: 'main', + urlRewriteSupported: true, + }, + exposure: 'public', + name: 'che-code', + protocol: 'https', + secure: true, + targetPort: 3100, + }, + { + attributes: { + discoverable: false, + urlRewriteSupported: false, + }, + exposure: 'public', + name: 'code-redirect-1', + protocol: 'https', + targetPort: 13131, + }, + { + attributes: { + discoverable: false, + urlRewriteSupported: false, + }, + exposure: 'public', + name: 'code-redirect-2', + protocol: 'https', + targetPort: 13132, + }, + { + attributes: { + discoverable: false, + urlRewriteSupported: false, + }, + exposure: 'public', + name: 'code-redirect-3', + protocol: 'https', + targetPort: 13133, + }, + ], + image: 'quay.io/devfile/universal-developer-image:latest', + memoryLimit: '1024Mi', + memoryRequest: '256Mi', + volumeMounts: [ + { + name: 'checode', + path: '/checode', + }, + ], + }, + name: 'che-code-runtime-description', + }, + { + name: 'checode', + volume: {}, + }, + ], + events: { + postStart: ['init-che-code-command'], + preStart: ['init-container-command'], + }, + metadata: { + attributes: { + firstPublicationDate: '2021-10-31', + iconData: + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n', + iconMediatype: 'image/svg+xml', + publisher: 'che-incubator', + repository: 'https://github.com/che-incubator/che-code', + title: 'Microsoft Visual Studio Code - Open Source IDE for Eclipse Che', + version: 'latest', + }, + description: 'Microsoft Visual Studio Code - Open Source IDE for Eclipse Che', + displayName: 'VS Code - Open Source', + name: 'che-code', + }, + schemaVersion: '2.2.2', + }; +}; + +export default getVSCodeEditorDefinition; diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/devWorkspaceClient.editorUpdate.spec.ts b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/devWorkspaceClient.editorUpdate.spec.ts index f885b47f1..f9e0efe88 100644 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/devWorkspaceClient.editorUpdate.spec.ts +++ b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/__tests__/devWorkspaceClient.editorUpdate.spec.ts @@ -16,8 +16,13 @@ import { container } from '@/inversify.config'; import { dashboardBackendPrefix } from '@/services/backend-client/const'; import * as DwtApi from '@/services/backend-client/devWorkspaceTemplateApi'; import devfileApi from '@/services/devfileApi'; -import getDevWorkspaceTemplate from '@/services/workspace-client/devworkspace/__tests__/__mocks__/devWorkspaceSpecTemplates'; -import { DevWorkspaceClient } from '@/services/workspace-client/devworkspace/devWorkspaceClient'; +import getVSCodeDevWorkspaceTemplate from '@/services/workspace-client/devworkspace/__tests__/__mocks__/devWorkspaceSpecTemplates'; +import getVSCodeEditorDefinition from '@/services/workspace-client/devworkspace/__tests__/__mocks__/editorDefinitions'; +import { + COMPONENT_UPDATE_POLICY, + DevWorkspaceClient, + REGISTRY_URL, +} from '@/services/workspace-client/devworkspace/devWorkspaceClient'; const mockFetchData = jest.fn(); jest.mock('@/services/registry/fetchData', () => ({ @@ -34,25 +39,22 @@ describe('DevWorkspace client editor update', () => { jest.clearAllMocks(); }); - describe('has target plugin in store', () => { + describe('has target editor in store', () => { it('should return patch for an editor if it has been updated', async () => { - const template = getDevWorkspaceTemplate('1000m'); + const template = getVSCodeDevWorkspaceTemplate('1000m'); const mockPatch = mockAxios.get as jest.Mock; mockPatch.mockResolvedValueOnce(new Promise(resolve => resolve({ data: template }))); - // if cpuLimit changed from '1000m' to '2000m' - const newTemplate = getDevWorkspaceTemplate('2000m'); - - const url = newTemplate.metadata.annotations?.['che.eclipse.org/plugin-registry-url']; + // if cpuLimit changed from '1000m' to '500m' + const newTemplate = getVSCodeDevWorkspaceTemplate('500m'); + const editors: devfileApi.Devfile[] = [getVSCodeEditorDefinition() as devfileApi.Devfile]; const editorName = newTemplate.metadata.name; const patch = await client.checkForTemplatesUpdate( editorName, namespace, - { - [url]: newTemplate.spec as devfileApi.Devfile, - }, + editors, pluginRegistryUrl, pluginRegistryInternalUrl, undefined, @@ -72,23 +74,20 @@ describe('DevWorkspace client editor update', () => { }); it(`should return an empty object if it hasn't been updated`, async () => { - const template = getDevWorkspaceTemplate('1000m'); + const template = getVSCodeDevWorkspaceTemplate('500m'); const mockPatch = mockAxios.get as jest.Mock; mockPatch.mockResolvedValueOnce(new Promise(resolve => resolve({ data: template }))); // if nothing changed - const newTemplate = getDevWorkspaceTemplate('1000m'); - - const url = newTemplate.metadata.annotations?.['che.eclipse.org/plugin-registry-url']; + const newTemplate = getVSCodeDevWorkspaceTemplate('500m'); + const editors: devfileApi.Devfile[] = [getVSCodeEditorDefinition() as devfileApi.Devfile]; const editorName = newTemplate.metadata.name; const patch = await client.checkForTemplatesUpdate( editorName, namespace, - { - [url]: newTemplate.spec as devfileApi.Devfile, - }, + editors, pluginRegistryUrl, pluginRegistryInternalUrl, undefined, @@ -102,41 +101,82 @@ describe('DevWorkspace client editor update', () => { }); }); - describe('don`t have target plugin in store', () => { - it('should return patch for an editor if it has been updated', async () => { - const template = getDevWorkspaceTemplate('1000m'); - const mockGet = mockAxios.get as jest.Mock; - mockGet.mockResolvedValueOnce(new Promise(resolve => resolve({ data: template }))); - - // if cpuLimit changed from '1000m' to '2000m' - const newTemplate = getDevWorkspaceTemplate('2000m'); - const mockPost = mockAxios.post as jest.Mock; - mockPost.mockResolvedValueOnce( - new Promise(resolve => resolve({ data: JSON.stringify(newTemplate.spec) })), - ); + describe('has no target editor in store', () => { + it(`should return an empty patch`, async () => { + const template = getVSCodeDevWorkspaceTemplate('1000m'); + const mockPatch = mockAxios.get as jest.Mock; + mockPatch.mockResolvedValueOnce(new Promise(resolve => resolve({ data: template }))); - const url = newTemplate.metadata.annotations?.['che.eclipse.org/plugin-registry-url']; + // if cpuLimit changed from '1000m' to '500m' + const newTemplate = getVSCodeDevWorkspaceTemplate('500m'); + const editor = getVSCodeEditorDefinition() as devfileApi.Devfile; + editor.metadata.name = 'non-existing-editor'; + const editors: devfileApi.Devfile[] = [editor]; const editorName = newTemplate.metadata.name; const patch = await client.checkForTemplatesUpdate( editorName, namespace, - {}, + editors, pluginRegistryUrl, pluginRegistryInternalUrl, undefined, ); - expect(mockGet).toHaveBeenCalledWith( - `${dashboardBackendPrefix}/namespace/${namespace}/devworkspacetemplates/${editorName}`, + expect(mockPatch.mock.calls).toEqual([ + [`${dashboardBackendPrefix}/namespace/${namespace}/devworkspacetemplates/${editorName}`], + ]); + + expect(patch).toEqual([]); + }); + }); + + describe('DevWorkspaceTemplate with plugin registry URL', () => { + it('should return patch for an editor if it has been updated', async () => { + const template = getVSCodeDevWorkspaceTemplate('1000m'); + template.metadata.annotations = { + 'che.eclipse.org/components-update-policy': 'managed', + 'che.eclipse.org/plugin-registry-url': + 'https://192.168.64.24.nip.io/plugin-registry/v3/plugins/che-incubator/che-code/latest/devfile.yaml', + }; + + const mockPatch = mockAxios.get as jest.Mock; + mockPatch.mockResolvedValueOnce(new Promise(resolve => resolve({ data: template }))); + + // if cpuLimit changed from '1000m' to '500m' + const newTemplate = getVSCodeDevWorkspaceTemplate('500m'); + newTemplate.metadata.annotations = { + 'che.eclipse.org/components-update-policy': 'managed', + 'che.eclipse.org/plugin-registry-url': + 'https://192.168.64.24.nip.io/plugin-registry/v3/plugins/che-incubator/che-code/latest/devfile.yaml', + }; + + const editors: devfileApi.Devfile[] = [getVSCodeEditorDefinition() as devfileApi.Devfile]; + const editorName = newTemplate.metadata.name; + + const patch = await client.checkForTemplatesUpdate( + editorName, + namespace, + editors, + pluginRegistryUrl, + pluginRegistryInternalUrl, undefined, ); - expect(mockPost).toHaveBeenCalledWith(expect.stringContaining('/data/resolver'), { - url: url, - }); + + expect(mockPatch.mock.calls).toEqual([ + [`${dashboardBackendPrefix}/namespace/${namespace}/devworkspacetemplates/${editorName}`], + ]); expect(patch).toEqual([ + { + op: 'replace', + path: '/metadata/annotations', + value: { + [COMPONENT_UPDATE_POLICY]: 'managed', + [REGISTRY_URL]: 'che-incubator/che-code/latest', + }, + }, { op: 'replace', path: '/spec', @@ -145,44 +185,141 @@ describe('DevWorkspace client editor update', () => { ]); }); - it(`should return an empty object if it hasn't been updated`, async () => { - const template = getDevWorkspaceTemplate('1000m'); - const mockGet = mockAxios.get as jest.Mock; - mockGet.mockResolvedValueOnce(new Promise(resolve => resolve({ data: template }))); + it('should return an empty patch if registry URL does not match a default plugin registry URL', async () => { + const template = getVSCodeDevWorkspaceTemplate('1000m'); + template.metadata.annotations = { + 'che.eclipse.org/components-update-policy': 'managed', + 'che.eclipse.org/plugin-registry-url': + 'https://192.168.64.24.nip.io/custom-registry/v3/plugins/che-incubator/che-code/latest/devfile.yaml', + }; - // if nothing changed - const newTemplate = getDevWorkspaceTemplate('1000m'); - const mockPost = mockAxios.post as jest.Mock; - mockPost.mockResolvedValueOnce( - new Promise(resolve => resolve({ data: JSON.stringify(newTemplate.spec) })), + const mockPatch = mockAxios.get as jest.Mock; + mockPatch.mockResolvedValueOnce(new Promise(resolve => resolve({ data: template }))); + + // if cpuLimit changed from '1000m' to '500m' + const newTemplate = getVSCodeDevWorkspaceTemplate('500m'); + newTemplate.metadata.annotations = { + 'che.eclipse.org/components-update-policy': 'managed', + 'che.eclipse.org/plugin-registry-url': + 'https://192.168.64.24.nip.io/custom-registry/v3/plugins/che-incubator/che-code/latest/devfile.yaml', + }; + + const editors: devfileApi.Devfile[] = [getVSCodeEditorDefinition() as devfileApi.Devfile]; + const editorName = newTemplate.metadata.name; + + const patch = await client.checkForTemplatesUpdate( + editorName, + namespace, + editors, + pluginRegistryUrl, + pluginRegistryInternalUrl, + undefined, ); - const url = newTemplate.metadata.annotations?.['che.eclipse.org/plugin-registry-url']; + expect(mockPatch.mock.calls).toEqual([ + [`${dashboardBackendPrefix}/namespace/${namespace}/devworkspacetemplates/${editorName}`], + ]); + + expect(patch).toEqual([]); + }); + + it('should update annotations if registry URL matches a default plugin registry URL but no changes in a template', async () => { + const template = getVSCodeDevWorkspaceTemplate('500m'); + template.metadata.annotations = { + 'che.eclipse.org/components-update-policy': 'managed', + 'che.eclipse.org/plugin-registry-url': + 'https://192.168.64.24.nip.io/plugin-registry/v3/plugins/che-incubator/che-code/latest/devfile.yaml', + }; + + const mockPatch = mockAxios.get as jest.Mock; + mockPatch.mockResolvedValueOnce(new Promise(resolve => resolve({ data: template }))); + + // if cpuLimit changed from '1000m' to '500m' + const newTemplate = getVSCodeDevWorkspaceTemplate('500m'); + newTemplate.metadata.annotations = { + 'che.eclipse.org/components-update-policy': 'managed', + 'che.eclipse.org/plugin-registry-url': + 'https://192.168.64.24.nip.io/plugin-registry/v3/plugins/che-incubator/che-code/latest/devfile.yaml', + }; + + const editors: devfileApi.Devfile[] = [getVSCodeEditorDefinition() as devfileApi.Devfile]; const editorName = newTemplate.metadata.name; const patch = await client.checkForTemplatesUpdate( editorName, namespace, - {}, + editors, pluginRegistryUrl, pluginRegistryInternalUrl, undefined, ); - expect(mockGet).toHaveBeenCalledWith( - `${dashboardBackendPrefix}/namespace/${namespace}/devworkspacetemplates/${editorName}`, + expect(mockPatch.mock.calls).toEqual([ + [`${dashboardBackendPrefix}/namespace/${namespace}/devworkspacetemplates/${editorName}`], + ]); + + expect(patch).toEqual([ + { + op: 'replace', + path: '/metadata/annotations', + value: { + [COMPONENT_UPDATE_POLICY]: 'managed', + [REGISTRY_URL]: 'che-incubator/che-code/latest', + }, + }, + ]); + }); + + it('should update annotations if registry URL matches a default plugin registry but editor id is not in local storage', async () => { + const template = getVSCodeDevWorkspaceTemplate('500m'); + template.metadata.annotations = { + 'che.eclipse.org/components-update-policy': 'managed', + 'che.eclipse.org/plugin-registry-url': + 'https://192.168.64.24.nip.io/plugin-registry/v3/plugins/che-incubator/custom-editor/latest/devfile.yaml', + }; + + const mockPatch = mockAxios.get as jest.Mock; + mockPatch.mockResolvedValueOnce(new Promise(resolve => resolve({ data: template }))); + + // if cpuLimit changed from '1000m' to '500m' + const newTemplate = getVSCodeDevWorkspaceTemplate('1000m'); + newTemplate.metadata.annotations = { + 'che.eclipse.org/components-update-policy': 'managed', + 'che.eclipse.org/plugin-registry-url': + 'https://192.168.64.24.nip.io/plugin-registry/v3/plugins/che-incubator/custom-editor/latest/devfile.yaml', + }; + + const editors: devfileApi.Devfile[] = [getVSCodeEditorDefinition() as devfileApi.Devfile]; + const editorName = newTemplate.metadata.name; + + const patch = await client.checkForTemplatesUpdate( + editorName, + namespace, + editors, + pluginRegistryUrl, + pluginRegistryInternalUrl, undefined, ); - expect(mockPost).toHaveBeenCalledWith(expect.stringContaining('/data/resolver'), { - url: url, - }); - expect(patch).toEqual([]); + expect(mockPatch.mock.calls).toEqual([ + [`${dashboardBackendPrefix}/namespace/${namespace}/devworkspacetemplates/${editorName}`], + ]); + + expect(patch).toEqual([ + { + op: 'replace', + path: '/metadata/annotations', + value: { + [COMPONENT_UPDATE_POLICY]: 'managed', + [REGISTRY_URL]: 'che-incubator/custom-editor/latest', + }, + }, + ]); }); }); it('should patch target template', async () => { - const template = getDevWorkspaceTemplate('1000m'); + const template = getVSCodeDevWorkspaceTemplate('1000m'); const editorName = template.metadata.name; 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 59d40ec78..809e8aeee 100644 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts +++ b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts @@ -18,7 +18,6 @@ import { } from '@devfile/api'; import { api } from '@eclipse-che/common'; import { inject, injectable } from 'inversify'; -import { load } from 'js-yaml'; import { cloneDeep, isEqual } from 'lodash'; import * as DwApi from '@/services/backend-client/devWorkspaceApi'; @@ -37,7 +36,6 @@ import { import { delay } from '@/services/helpers/delay'; import { isWebTerminal } from '@/services/helpers/devworkspace'; import { DevWorkspaceStatus } from '@/services/helpers/types'; -import { fetchData } from '@/services/registry/fetchData'; import { WorkspaceAdapter } from '@/services/workspace-adapter'; import { devWorkspaceApiGroup, @@ -679,7 +677,7 @@ export class DevWorkspaceClient { async checkForTemplatesUpdate( editorName: string, namespace: string, - pluginsByUrl: { [url: string]: devfileApi.Devfile } = {}, + editors: devfileApi.Devfile[], pluginRegistryUrl: string | undefined, pluginRegistryInternalUrl: string | undefined, openVSXUrl: string | undefined, @@ -692,32 +690,64 @@ export class DevWorkspaceClient { const patch: api.IPatch[] = []; - const url = managedTemplate.metadata?.annotations?.[REGISTRY_URL]; + let editorReference = managedTemplate.metadata?.annotations?.[REGISTRY_URL]; - if (!url || managedTemplate.metadata?.annotations?.[COMPONENT_UPDATE_POLICY] !== 'managed') { + if ( + !editorReference || + managedTemplate.metadata?.annotations?.[COMPONENT_UPDATE_POLICY] !== 'managed' + ) { console.log('Template is not managed'); return patch; } - let plugin: devfileApi.Devfile | undefined; - if (pluginsByUrl[url]) { - plugin = pluginsByUrl[url]; - } else { - const pluginContent = await fetchData(url); - plugin = load(pluginContent) as devfileApi.Devfile; - pluginsByUrl[url] = plugin; + if (/^(https?:\/\/)/.test(editorReference)) { + // Define a regular expression pattern to match URLs containing 'plugin-registry/v3/plugins' + // and ending with 'devfile.yaml'. The part between 'v3/plugins/' and '/devfile.yaml' is captured. + const pluginRegistryURLPattern = /plugin-registry\/v3\/plugins\/(.+?)\/devfile\.yaml$/; + const match = editorReference.match(pluginRegistryURLPattern); + + if (match) { + editorReference = match[1]; + const annotations = { + [COMPONENT_UPDATE_POLICY]: 'managed', + [REGISTRY_URL]: editorReference, + }; + // Create a patch to update the annotations by replacing plugin registry URL with the editor reference + patch.push({ + op: 'replace', + path: '/metadata/annotations', + value: annotations, + }); + } else { + console.log('Template is not managed'); + return patch; + } + } + + const originalEditor: devfileApi.Devfile | undefined = editors.find(editor => { + return ( + editor.metadata?.attributes?.publisher + + '/' + + editor.metadata?.name + + '/' + + editor.metadata?.attributes?.version === + editorReference + ); + }); + if (!originalEditor) { + return patch; } const spec: Partial = {}; - for (const key in plugin) { + for (const key in originalEditor) { if (key !== 'schemaVersion' && key !== 'metadata') { if (key === 'components') { - plugin.components?.forEach(component => { + originalEditor.components?.forEach(component => { if (component.container && !component.container.sourceMapping) { component.container.sourceMapping = '/projects'; } }); - spec.components = plugin.components; + spec.components = originalEditor.components; this.addEnvVarsToContainers( spec.components, pluginRegistryUrl, @@ -726,7 +756,7 @@ export class DevWorkspaceClient { clusterConsole, ); } else { - spec[key] = plugin[key]; + spec[key] = originalEditor[key]; } } } diff --git a/packages/dashboard-frontend/src/store/DevfileRegistries/getEditor.ts b/packages/dashboard-frontend/src/store/DevfileRegistries/getEditor.ts index 395e384a2..e11901ac9 100644 --- a/packages/dashboard-frontend/src/store/DevfileRegistries/getEditor.ts +++ b/packages/dashboard-frontend/src/store/DevfileRegistries/getEditor.ts @@ -69,7 +69,7 @@ export async function getEditor( editorIdOrPath ) { editor = e; - return Object.assign({ content: dump(editor), editorIdOrPath }); + return Object.assign({ content: dump(editor), editorYamlUrl: editorIdOrPath }); } } } diff --git a/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/__tests__/actions.spec.ts b/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/__tests__/actions.spec.ts index 88c71169d..d1844c3d0 100644 --- a/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/__tests__/actions.spec.ts +++ b/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/__tests__/actions.spec.ts @@ -557,7 +557,10 @@ describe('DevWorkspace store, actions', () => { { op: 'add', path: '/metadata/annotations', - value: {}, + value: { + 'che.eclipse.org/components-update-policy': 'managed', + 'che.eclipse.org/plugin-registry-url': 'che-incubator/che-code/latest', + }, }, { op: 'replace', @@ -1075,6 +1078,10 @@ describe('DevWorkspace store, actions', () => { apiVersion: 'workspace.devfile.io/v1alpha2', kind: 'DevWorkspaceTemplate', metadata: { + annotations: { + 'che.eclipse.org/components-update-policy': 'managed', + 'che.eclipse.org/plugin-registry-url': 'che-incubator/che-code/latest', + }, name: 'che-code', }, }), diff --git a/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/__tests__/updateEditor.spec.ts b/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/__tests__/updateEditor.spec.ts index 9090f94da..922155ea2 100644 --- a/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/__tests__/updateEditor.spec.ts +++ b/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/__tests__/updateEditor.spec.ts @@ -115,19 +115,19 @@ describe('updateEditor, functions', () => { icon: 'https://console-icon-url', title: 'Cluster console', }; - const editorResourceUrl = 'http://editor-url'; const editorId = 'che-incubator/che-code/latest'; - const editors = { - [editorId]: { - url: editorResourceUrl, - plugin: { - schemaVersion: '2.2.0', - metadata: { - name: 'che-code', + const editors = [ + { + schemaVersion: '2.2.0', + metadata: { + name: 'che-code', + attributes: { + publisher: 'che-incubator', + version: 'latest', }, - } as devfileApi.Devfile, - }, - }; + }, + } as devfileApi.Devfile, + ]; let store: MockStoreEnhanced>; beforeEach(() => { store = new FakeStoreBuilder() @@ -140,7 +140,7 @@ describe('updateEditor, functions', () => { pluginRegistryInternalURL: 'https://internal.registry', pluginRegistryURL, }) - .withDwPlugins({}, editors, false, [], undefined, editorId) + .withDwPlugins({}, {}, false, editors, undefined, editorId) .build(); }); @@ -152,7 +152,7 @@ describe('updateEditor, functions', () => { expect(mockCheckForEditorUpdate).toHaveBeenCalledWith( 'che-code', namespace, - { [editorResourceUrl]: editors[editorId].plugin }, + editors, pluginRegistryURL, 'https://internal.registry', 'https://openvsx.org', @@ -171,7 +171,7 @@ describe('updateEditor, functions', () => { expect(mockCheckForEditorUpdate).toHaveBeenCalledWith( 'che-code', namespace, - { [editorResourceUrl]: editors[editorId].plugin }, + editors, pluginRegistryURL, 'https://internal.registry', 'https://openvsx.org', diff --git a/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/updateEditor.ts b/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/updateEditor.ts index 35816e1dd..ce7a4b1c9 100644 --- a/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/updateEditor.ts +++ b/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/updateEditor.ts @@ -35,6 +35,7 @@ export async function updateEditor(editorName: string, getState: () => AppState) selectDwEditorsPluginsList(state.dwPlugins.defaultEditorName)(state).forEach(dwEditor => { pluginsByUrl[dwEditor.url] = dwEditor.devfile; }); + const editors = state.dwPlugins.cmEditors || []; const openVSXUrl = selectOpenVSXUrl(state); const pluginRegistryUrl = selectPluginRegistryUrl(state); const pluginRegistryInternalUrl = selectPluginRegistryInternalUrl(state); @@ -46,7 +47,7 @@ export async function updateEditor(editorName: string, getState: () => AppState) const updates = await devWorkspaceClient.checkForTemplatesUpdate( editorName, namespace, - pluginsByUrl, + editors, pluginRegistryUrl, pluginRegistryInternalUrl, openVSXUrl,