-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: manage default uri plugins for DevWorkspaces
Signed-off-by: David Kwon <[email protected]>
- Loading branch information
Showing
6 changed files
with
340 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 34 additions & 0 deletions
34
packages/dashboard-frontend/src/services/dashboard-backend-client/serverConfigApi.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* | ||
* Copyright (c) 2018-2021 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 axios from 'axios'; | ||
import common from '@eclipse-che/common'; | ||
import { prefix } from './const'; | ||
|
||
/** | ||
* Returns an array of default plug-ins for the specified editor | ||
* | ||
* @param editorId The editor id to get default plug-ins for | ||
* @returns Promise resolving with the array of default plug-ins for the specified editor | ||
*/ | ||
export async function getDefaultPlugins(editorId: string): Promise<string[]> { | ||
const url = `${prefix}/server-config/default-plugins`; | ||
try { | ||
const response = await axios.get(url); | ||
const editorPlugins = response.data.find((element: { editor: string; plugins: string[] }) => | ||
element.editor.toLowerCase() === editorId, | ||
); | ||
return editorPlugins ? editorPlugins.plugins : []; | ||
} catch (e) { | ||
throw `Failed to fetch default plugins. ${common.helpers.errors.getMessage(e)}`; | ||
} | ||
} |
135 changes: 135 additions & 0 deletions
135
...-frontend/src/services/workspace-client/devworkspace/DevWorkspaceDefaultPluginsHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/* | ||
* Copyright (c) 2018-2021 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 { injectable } from 'inversify'; | ||
import { api } from '@eclipse-che/common'; | ||
import * as ServerConfigApi from '../../dashboard-backend-client/serverConfigApi'; | ||
import { createHash } from 'crypto'; | ||
import devfileApi from '../../devfileApi'; | ||
import { V1alpha2DevWorkspaceSpecTemplateComponents } from '@devfile/api'; | ||
import * as DwApi from '../../dashboard-backend-client/devWorkspaceApi'; | ||
|
||
const DEFAULT_PLUGIN_ATTRIBUTE = 'che.eclipse.org/default-plugin'; | ||
|
||
/** | ||
* This class manages the default plugins defined in | ||
* the DevWorkspace's spec.template.components array. | ||
*/ | ||
@injectable() | ||
export class DevWorkspaceDefaultPluginsHandler { | ||
public async handle(workspace: devfileApi.DevWorkspace, editor: string): Promise<void> { | ||
const defaultPlugins = await ServerConfigApi.getDefaultPlugins(editor); | ||
this.handleUriPlugins(workspace, defaultPlugins); | ||
this.patchWorkspaceComponents(workspace); | ||
} | ||
|
||
private async handleUriPlugins(workspace: devfileApi.DevWorkspace, defaultPlugins: string[]) { | ||
const defaultUriPlugins = new Set( | ||
defaultPlugins.filter(plugin => { | ||
if (this.isUri(plugin)) { | ||
return true; | ||
} | ||
console.log(`Default plugin ${plugin} is not a uri. Ignoring.`); | ||
return false; | ||
}), | ||
); | ||
|
||
this.removeOldDefaultUriPlugins(workspace, defaultUriPlugins); | ||
|
||
defaultUriPlugins.forEach(plugin => { | ||
const hash = createHash('MD5').update(plugin).digest('hex').substring(0, 20).toLowerCase(); | ||
this.addDefaultPluginByUri(workspace, 'default-' + hash, plugin); | ||
}); | ||
} | ||
|
||
private isUri(str: string): boolean { | ||
try { | ||
new URL(str); | ||
return true; | ||
} catch (err) { | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Checks if there are default plugins in the workspace that are not | ||
* specified in defaultUriPlugins. If such plugins are found, this function | ||
* removes them. | ||
* @param workspace A devworkspace to remove old default plugins for | ||
* @param defaultUriPlugins The set of current default plugins | ||
*/ | ||
private removeOldDefaultUriPlugins( | ||
workspace: devfileApi.DevWorkspace, | ||
defaultUriPlugins: Set<string>, | ||
) { | ||
if (!workspace.spec.template.components) { | ||
return; | ||
} | ||
const components = workspace.spec.template.components.filter(component => { | ||
if (!this.isDefaultPluginComponent(component) || !component.plugin?.uri) { | ||
// component is not a default uri plugin, keep component. | ||
return true; | ||
} | ||
return defaultUriPlugins.has(component.plugin.uri); | ||
}); | ||
workspace.spec.template.components = components; | ||
} | ||
|
||
/** | ||
* Returns true if component is a default plugin managed by this class | ||
* @param component The component to check | ||
* @returns true if component is a default plugin managed by this class | ||
*/ | ||
private isDefaultPluginComponent(component: V1alpha2DevWorkspaceSpecTemplateComponents): boolean { | ||
return component.attributes && component.attributes[DEFAULT_PLUGIN_ATTRIBUTE] | ||
? component.attributes[DEFAULT_PLUGIN_ATTRIBUTE].toString() === 'true' | ||
: false; | ||
} | ||
|
||
/** | ||
* Add a default plugin to the workspace by uri | ||
* @param workspace A devworkspace | ||
* @param pluginName The name of the plugin | ||
* @param pluginUri The uri of the plugin | ||
*/ | ||
private addDefaultPluginByUri( | ||
workspace: devfileApi.DevWorkspace, | ||
pluginName: string, | ||
pluginUri: string, | ||
) { | ||
if (!workspace.spec.template.components) { | ||
workspace.spec.template.components = []; | ||
} | ||
|
||
if (workspace.spec.template.components.find(component => component.name === pluginName)) { | ||
// plugin already exists | ||
return; | ||
} | ||
|
||
workspace.spec.template.components.push({ | ||
name: pluginName, | ||
attributes: { [DEFAULT_PLUGIN_ATTRIBUTE]: true }, | ||
plugin: { uri: pluginUri }, | ||
}); | ||
} | ||
|
||
private async patchWorkspaceComponents(workspace: devfileApi.DevWorkspace) { | ||
const patch: api.IPatch[] = [ | ||
{ | ||
op: 'replace', | ||
path: '/spec/template/components', | ||
value: workspace.spec.template.components, | ||
}, | ||
]; | ||
return DwApi.patchWorkspace(workspace.metadata.namespace, workspace.metadata.name, patch); | ||
} | ||
} |
148 changes: 148 additions & 0 deletions
148
...d/src/services/workspace-client/devworkspace/__tests__/devWorkspaceClient.onStart.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
/* | ||
* Copyright (c) 2018-2021 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 { container } from '../../../../inversify.config'; | ||
import { DevWorkspaceBuilder } from '../../../../store/__mocks__/devWorkspaceBuilder'; | ||
import { DevWorkspaceClient } from '../devWorkspaceClient'; | ||
import * as DwApi from '../../../dashboard-backend-client/devWorkspaceApi'; | ||
import * as ServerConfigApi from '../../../dashboard-backend-client/serverConfigApi'; | ||
|
||
describe('DevWorkspace client, start', () => { | ||
let client: DevWorkspaceClient; | ||
|
||
beforeEach(() => { | ||
client = container.get(DevWorkspaceClient); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
it('should add default plugin uri', async () => { | ||
const namespace = 'che'; | ||
const name = 'wksp-test'; | ||
const testWorkspace = new DevWorkspaceBuilder() | ||
.withMetadata({ | ||
name, | ||
namespace, | ||
}) | ||
.build(); | ||
|
||
const defaultPluginUri = 'https://test.com/devfile.yaml'; | ||
const getDefaultPlugins = jest | ||
.spyOn(ServerConfigApi, 'getDefaultPlugins') | ||
.mockResolvedValueOnce([defaultPluginUri]); | ||
const patchWorkspace = jest.spyOn(DwApi, 'patchWorkspace'); | ||
await client.onStart(testWorkspace, ''); | ||
expect(testWorkspace.spec.template.components!.length).toBe(1); | ||
expect(testWorkspace.spec.template.components![0].plugin!.uri!).toBe(defaultPluginUri); | ||
expect( | ||
testWorkspace.spec.template.components![0].attributes!['che.eclipse.org/default-plugin'], | ||
).toBe(true); | ||
expect(getDefaultPlugins).toHaveBeenCalled(); | ||
expect(patchWorkspace).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should remove default plugin uri when no default plugins exist', async () => { | ||
const namespace = 'che'; | ||
const name = 'wksp-test'; | ||
const testWorkspace = new DevWorkspaceBuilder() | ||
.withMetadata({ | ||
name, | ||
namespace, | ||
}) | ||
.withTemplate({ | ||
components: [ | ||
{ | ||
name: 'default', | ||
attributes: { 'che.eclipse.org/default-plugin': true }, | ||
plugin: { uri: 'https://test.com/devfile.yaml' }, | ||
}, | ||
], | ||
}) | ||
.build(); | ||
|
||
// No default plugins | ||
const getDefaultPlugins = jest | ||
.spyOn(ServerConfigApi, 'getDefaultPlugins') | ||
.mockResolvedValueOnce([]); | ||
const patchWorkspace = jest.spyOn(DwApi, 'patchWorkspace'); | ||
await client.onStart(testWorkspace, ''); | ||
expect(testWorkspace.spec.template.components!.length).toBe(0); | ||
expect(getDefaultPlugins).toHaveBeenCalled(); | ||
expect(patchWorkspace).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should not remove non default plugin uri when no default plugins exist', async () => { | ||
const namespace = 'che'; | ||
const name = 'wksp-test'; | ||
const uri = 'https://test.com/devfile.yaml'; | ||
const testWorkspace = new DevWorkspaceBuilder() | ||
.withMetadata({ | ||
name, | ||
namespace, | ||
}) | ||
.withTemplate({ | ||
components: [ | ||
{ | ||
name: 'my-plugin', | ||
plugin: { uri }, | ||
}, | ||
], | ||
}) | ||
.build(); | ||
|
||
// No default plugins | ||
const getDefaultPlugins = jest | ||
.spyOn(ServerConfigApi, 'getDefaultPlugins') | ||
.mockResolvedValueOnce([]); | ||
const patchWorkspace = jest.spyOn(DwApi, 'patchWorkspace'); | ||
await client.onStart(testWorkspace, ''); | ||
expect(testWorkspace.spec.template.components!.length).toBe(1); | ||
expect(testWorkspace.spec.template.components![0].plugin!.uri!).toBe(uri); | ||
expect(testWorkspace.spec.template.components![0].attributes).toBeUndefined(); | ||
expect(getDefaultPlugins).toHaveBeenCalled(); | ||
expect(patchWorkspace).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should not remove non default plugin uri if attribute is false', async () => { | ||
const namespace = 'che'; | ||
const name = 'wksp-test'; | ||
const uri = 'https://test.com/devfile.yaml'; | ||
const testWorkspace = new DevWorkspaceBuilder() | ||
.withMetadata({ | ||
name, | ||
namespace, | ||
}) | ||
.withTemplate({ | ||
components: [ | ||
{ | ||
name: 'default', | ||
attributes: { 'che.eclipse.org/default-plugin': false }, | ||
plugin: { uri: 'https://test.com/devfile.yaml' }, | ||
}, | ||
], | ||
}) | ||
.build(); | ||
|
||
// No default plugins | ||
const getDefaultPlugins = jest | ||
.spyOn(ServerConfigApi, 'getDefaultPlugins') | ||
.mockResolvedValueOnce([]); | ||
const patchWorkspace = jest.spyOn(DwApi, 'patchWorkspace'); | ||
await client.onStart(testWorkspace, ''); | ||
expect(testWorkspace.spec.template.components!.length).toBe(1); | ||
expect(testWorkspace.spec.template.components![0].plugin!.uri!).toBe(uri); | ||
expect(getDefaultPlugins).toHaveBeenCalled(); | ||
expect(patchWorkspace).toHaveBeenCalled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.