From fae537a30ab90593f56598528c440ae48c014b04 Mon Sep 17 00:00:00 2001 From: Florent Benoit Date: Tue, 23 Feb 2021 13:27:31 +0100 Subject: [PATCH] feat(devfile): Generates devfile.yaml in addition to meta.yaml for che-editors and che-plugins Change-Id: Ibefbb93bc1eab00cdbfc1fe1e2ed2caac87166ce Signed-off-by: Florent Benoit --- tools/build/src/devfile/devfile-module.ts | 18 ++ .../src/devfile/meta-yaml-to-devfile-yaml.ts | 167 ++++++++++++++++ tools/build/src/inversify-binding.ts | 2 + tools/build/src/meta-yaml/index-writer.ts | 14 +- tools/build/src/meta-yaml/meta-yaml-writer.ts | 11 +- .../tests/_data/meta/che-theia-meta.yaml | 109 ++++++++++ .../meta/container-minimal-endpoint.yaml | 9 + .../_data/meta/container-no-endpoints.yaml | 6 + .../_data/meta/machine-exec-plugin-meta.yaml | 33 +++ .../build/tests/_data/meta/no-container.yaml | 17 ++ .../tests/_data/meta/vscode-extension.yaml | 25 +++ .../devfile/meta-yaml-to-devfile-yaml.spec.ts | 189 ++++++++++++++++++ tools/build/tests/inversify-binding.spec.ts | 4 + .../tests/meta-yaml/index-writer.spec.ts | 4 + .../tests/meta-yaml/meta-yaml-writer.spec.ts | 63 ++++++ 15 files changed, 667 insertions(+), 4 deletions(-) create mode 100644 tools/build/src/devfile/devfile-module.ts create mode 100644 tools/build/src/devfile/meta-yaml-to-devfile-yaml.ts create mode 100644 tools/build/tests/_data/meta/che-theia-meta.yaml create mode 100644 tools/build/tests/_data/meta/container-minimal-endpoint.yaml create mode 100644 tools/build/tests/_data/meta/container-no-endpoints.yaml create mode 100644 tools/build/tests/_data/meta/machine-exec-plugin-meta.yaml create mode 100644 tools/build/tests/_data/meta/no-container.yaml create mode 100644 tools/build/tests/_data/meta/vscode-extension.yaml create mode 100644 tools/build/tests/devfile/meta-yaml-to-devfile-yaml.spec.ts diff --git a/tools/build/src/devfile/devfile-module.ts b/tools/build/src/devfile/devfile-module.ts new file mode 100644 index 0000000000..970622a773 --- /dev/null +++ b/tools/build/src/devfile/devfile-module.ts @@ -0,0 +1,18 @@ +/********************************************************************** + * Copyright (c) 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 + ***********************************************************************/ +import { ContainerModule, interfaces } from 'inversify'; + +import { MetaYamlToDevfileYaml } from './meta-yaml-to-devfile-yaml'; + +const devfileModule = new ContainerModule((bind: interfaces.Bind) => { + bind(MetaYamlToDevfileYaml).toSelf().inSingletonScope(); +}); + +export { devfileModule }; diff --git a/tools/build/src/devfile/meta-yaml-to-devfile-yaml.ts b/tools/build/src/devfile/meta-yaml-to-devfile-yaml.ts new file mode 100644 index 0000000000..25c7d62f34 --- /dev/null +++ b/tools/build/src/devfile/meta-yaml-to-devfile-yaml.ts @@ -0,0 +1,167 @@ +/********************************************************************** + * Copyright (c) 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 + ***********************************************************************/ + +import { injectable } from 'inversify'; + +/** + * Convert meta.yaml into a devfile 2.0 syntax + */ +@injectable() +export class MetaYamlToDevfileYaml { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + componentsFromContainer(container: any): any[] { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const components: any[] = []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const component: any = { + name: container.name, + container: { + image: container.image, + }, + }; + if (container.command) { + component.container.args = container.command; + } + if (container.env) { + component.container.env = container.env; + } + if (container.volumes) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + component.container.volumeMounts = container.volumes.map((volume: any) => ({ + name: volume.name, + path: volume.mountPath, + })); + + // add volume components + // eslint-disable-next-line @typescript-eslint/no-explicit-any + container.volumes.map((volume: any) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const volumeComponent: any = { + name: volume.name, + volume: {}, + }; + if (volume.ephemeral === true) { + volumeComponent.volume.ephemeral = true; + } + components.push(volumeComponent); + }); + } + if (container.mountSources) { + component.container.mountSources = container.mountSources; + } + if (container.memoryLimit) { + component.container.memoryLimit = container.memoryLimit; + } + if (container.memoryRequest) { + component.container.memoryRequest = container.memoryRequest; + } + if (container.cpuLimit) { + component.container.cpuLimit = container.cpuLimit; + } + if (container.cpuRequest) { + component.container.cpuRequest = container.cpuRequest; + } + + components.push(component); + + // replace 127.0.0.1 by 0.0.0.0 + return components.map(iteratingComponent => + JSON.parse(JSON.stringify(iteratingComponent).replace(/127\.0\.0\.1/g, '0.0.0.0')) + ); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types + convert(metaYaml: any): any | undefined { + // do not handle VS Code extensions as they can't be converted into devfile 2.0 + if (!metaYaml || metaYaml.type === 'VS Code extension') { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const devfileYaml: any = { + schemaVersion: '2.1.0', + metadata: { + name: metaYaml.displayName, + }, + }; + + // for each container, add a component + const metaYamlSpec = metaYaml.spec; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let components: any[] = []; + if (metaYamlSpec.containers && metaYamlSpec.containers.length === 1) { + // handle only one container from meta.yaml + const container = metaYamlSpec.containers[0]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const componentsFromContainer: any[] = this.componentsFromContainer(container); + // add all endpoints + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const endpoints: any[] = []; + if (metaYamlSpec.endpoints && metaYamlSpec.endpoints.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metaYamlSpec.endpoints.forEach((endpoint: any) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const devfileEndpoint: any = { + name: endpoint.name, + attributes: endpoint.attributes, + }; + devfileEndpoint.targetPort = endpoint.targetPort; + if (endpoint.public === true) { + devfileEndpoint.exposure = 'public'; + } + + // if it's secured, remove secure option for now + if (devfileEndpoint.attributes && devfileEndpoint.attributes.secure === true) { + devfileEndpoint.secure = false; + delete devfileEndpoint.attributes['secure']; + } + + // move protocol upper than inside attributes + if (devfileEndpoint.attributes && devfileEndpoint.attributes.protocol) { + devfileEndpoint.protocol = devfileEndpoint.attributes.protocol; + delete devfileEndpoint.attributes['protocol']; + } + + endpoints.push(devfileEndpoint); + }); + } + // last component is the container component + componentsFromContainer[componentsFromContainer.length - 1].container.endpoints = endpoints; + components = components.concat(componentsFromContainer); + } + if (metaYamlSpec.initContainers && metaYamlSpec.initContainers.length === 1) { + // handle only one container from meta.yaml + const initContainer = metaYamlSpec.initContainers[0]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const componentsFromContainer: any[] = this.componentsFromContainer(initContainer); + + // add a command + const commands = devfileYaml.commands || []; + commands.push({ + id: 'init-container-command', + apply: { + component: componentsFromContainer[componentsFromContainer.length - 1].name, + }, + }); + devfileYaml.commands = commands; + + // add event + const events = devfileYaml.events || {}; + const preStartEvents = events.preStart || []; + preStartEvents.push('init-container-command'); + events.preStart = preStartEvents; + devfileYaml.events = events; + components = components.concat(componentsFromContainer); + } + devfileYaml.components = components; + return devfileYaml; + } +} diff --git a/tools/build/src/inversify-binding.ts b/tools/build/src/inversify-binding.ts index 155e159f72..3e8982ea1a 100644 --- a/tools/build/src/inversify-binding.ts +++ b/tools/build/src/inversify-binding.ts @@ -18,6 +18,7 @@ import { Container } from 'inversify'; import { chePluginsModule } from './che-plugin/che-plugins-module'; import { cheTheiaPluginModule } from './che-theia-plugin/che-theia-plugin-module'; import { commonModule } from './common/common-module'; +import { devfileModule } from './devfile/devfile-module'; import { editorModule } from './editor/editor-module'; import { extensionsModule } from './extensions/extension-module'; import { featuredModule } from './featured/featured-module'; @@ -51,6 +52,7 @@ export class InversifyBinding { this.container.load(commonModule); this.container.load(chePluginsModule); this.container.load(cheTheiaPluginModule); + this.container.load(devfileModule); this.container.load(editorModule); this.container.load(extensionsModule); this.container.load(featuredModule); diff --git a/tools/build/src/meta-yaml/index-writer.ts b/tools/build/src/meta-yaml/index-writer.ts index 31fef12d88..13337fac5f 100644 --- a/tools/build/src/meta-yaml/index-writer.ts +++ b/tools/build/src/meta-yaml/index-writer.ts @@ -24,6 +24,16 @@ export class IndexWriter { @named('OUTPUT_ROOT_DIRECTORY') private outputRootDirectory: string; + getLinks(plugin: MetaYamlPluginInfo): { self: string; devfile?: string } { + const links: { self: string; devfile?: string } = { + self: `/v3/plugins/${plugin.id}`, + }; + if (plugin.type === 'Che Editor' || plugin.type === 'Che Plugin') { + links.devfile = `/v3/plugins/${plugin.id}/devfile.yaml`; + } + return links; + } + async write(generatedMetaYamlPluginInfos: MetaYamlPluginInfo[]): Promise { const v3PluginsFolder = path.resolve(this.outputRootDirectory, 'v3', 'plugins'); await fs.ensureDir(v3PluginsFolder); @@ -33,9 +43,7 @@ export class IndexWriter { id: plugin.id, description: plugin.description, displayName: plugin.displayName, - links: { - self: `/v3/plugins/${plugin.id}`, - }, + links: this.getLinks(plugin), name: plugin.name, publisher: plugin.publisher, type: plugin.type, diff --git a/tools/build/src/meta-yaml/meta-yaml-writer.ts b/tools/build/src/meta-yaml/meta-yaml-writer.ts index 3fb95e8f62..68beb4e553 100644 --- a/tools/build/src/meta-yaml/meta-yaml-writer.ts +++ b/tools/build/src/meta-yaml/meta-yaml-writer.ts @@ -16,6 +16,7 @@ import * as path from 'path'; import { inject, injectable, named } from 'inversify'; import { MetaYamlPluginInfo } from './meta-yaml-plugin-info'; +import { MetaYamlToDevfileYaml } from '../devfile/meta-yaml-to-devfile-yaml'; @injectable() export class MetaYamlWriter { @@ -27,6 +28,9 @@ export class MetaYamlWriter { @named('EMBED_VSIX') private embedVsix: boolean; + @inject(MetaYamlToDevfileYaml) + private metaYamlToDevfileYaml: MetaYamlToDevfileYaml; + public static readonly DEFAULT_ICON = '/v3/images/eclipse-che-logo.png'; convertIdToPublisherAndName(id: string): [string, string] { @@ -155,9 +159,14 @@ export class MetaYamlWriter { metaYamlPluginGenerated.push(generated); const pluginPath = path.resolve(pluginsFolder, computedId, version, 'meta.yaml'); - await fs.ensureDir(path.dirname(pluginPath)); promises.push(fs.writeFile(pluginPath, yamlString)); + const devfileYaml = this.metaYamlToDevfileYaml.convert(metaYaml); + if (devfileYaml) { + const devfilePath = path.resolve(pluginsFolder, computedId, version, 'devfile.yaml'); + const devfileYamlString = jsyaml.safeDump(devfileYaml, { lineWidth: 120 }); + promises.push(fs.writeFile(devfilePath, devfileYamlString)); + } }) ); return Promise.all(promises); diff --git a/tools/build/tests/_data/meta/che-theia-meta.yaml b/tools/build/tests/_data/meta/che-theia-meta.yaml new file mode 100644 index 0000000000..00f5b8cc94 --- /dev/null +++ b/tools/build/tests/_data/meta/che-theia-meta.yaml @@ -0,0 +1,109 @@ +apiVersion: v2 +publisher: eclipse +name: che-theia +version: next +type: Che Editor +displayName: theia-ide +title: Eclipse Theia development version. +description: 'Eclipse Theia, get the latest release each day.' +icon: /v3/images/eclipse-che-logo.png +category: Editor +repository: 'https://github.com/eclipse/che-theia' +firstPublicationDate: '2019-03-07' +latestUpdateDate: '2021-02-19' +spec: + endpoints: + - name: theia + public: true + targetPort: 3100 + attributes: + protocol: http + type: ide + secure: true + cookiesAuthEnabled: true + discoverable: false + - name: webviews + public: true + targetPort: 3100 + attributes: + protocol: http + type: webview + secure: true + cookiesAuthEnabled: true + discoverable: false + unique: true + - name: mini-browser + public: true + targetPort: 3100 + attributes: + protocol: http + type: mini-browser + secure: true + cookiesAuthEnabled: true + discoverable: false + unique: true + - name: theia-dev + public: true + targetPort: 3130 + attributes: + protocol: http + type: ide-dev + discoverable: false + - name: theia-redirect-1 + public: true + targetPort: 13131 + attributes: + protocol: http + discoverable: false + - name: theia-redirect-2 + public: true + targetPort: 13132 + attributes: + protocol: http + discoverable: false + - name: theia-redirect-3 + public: true + targetPort: 13133 + attributes: + protocol: http + discoverable: false + containers: + - name: theia-ide + image: 'quay.io/eclipse/che-theia:next' + env: + - name: THEIA_PLUGINS + value: 'local-dir:///plugins' + - name: HOSTED_PLUGIN_HOSTNAME + value: 0.0.0.0 + - name: HOSTED_PLUGIN_PORT + value: '3130' + - name: THEIA_HOST + value: 127.0.0.1 + mountSources: true + ports: + - exposedPort: 3100 + - exposedPort: 3130 + - exposedPort: 13131 + - exposedPort: 13132 + - exposedPort: 13133 + memoryLimit: 512M + memoryRequest: 50M + cpuLimit: 1500m + cpuRequest: 100m + volumes: + - name: plugins + mountPath: /plugins + - name: theia-local + mountPath: /home/theia/.theia + initContainers: + - name: remote-runtime-injector + image: 'quay.io/eclipse/che-theia-endpoint-runtime-binary:next' + env: + - name: PLUGIN_REMOTE_ENDPOINT_EXECUTABLE + value: /remote-endpoint/plugin-remote-endpoint + - name: REMOTE_ENDPOINT_VOLUME_NAME + value: remote-endpoint + volumes: + - name: remote-endpoint + mountPath: /remote-endpoint + ephemeral: true diff --git a/tools/build/tests/_data/meta/container-minimal-endpoint.yaml b/tools/build/tests/_data/meta/container-minimal-endpoint.yaml new file mode 100644 index 0000000000..e08ebbd6a1 --- /dev/null +++ b/tools/build/tests/_data/meta/container-minimal-endpoint.yaml @@ -0,0 +1,9 @@ +displayName: minimal-endpoint +type: Che Plugin +spec: + endpoints: + - name: www + targetPort: 3100 + containers: + - name: minimal-endpoint + image: 'quay.io/minimal-endpoint' diff --git a/tools/build/tests/_data/meta/container-no-endpoints.yaml b/tools/build/tests/_data/meta/container-no-endpoints.yaml new file mode 100644 index 0000000000..226ce06745 --- /dev/null +++ b/tools/build/tests/_data/meta/container-no-endpoints.yaml @@ -0,0 +1,6 @@ +displayName: no-endpoint +type: Che Plugin +spec: + containers: + - name: no-endpoint + image: 'quay.io/no-endpoint' diff --git a/tools/build/tests/_data/meta/machine-exec-plugin-meta.yaml b/tools/build/tests/_data/meta/machine-exec-plugin-meta.yaml new file mode 100644 index 0000000000..785ee01231 --- /dev/null +++ b/tools/build/tests/_data/meta/machine-exec-plugin-meta.yaml @@ -0,0 +1,33 @@ +apiVersion: v2 +publisher: eclipse +name: che-machine-exec-plugin +version: nightly +type: Che Plugin +displayName: Che machine-exec Service +title: Che machine-exec Service +description: Che Plug-in with che-machine-exec service to provide creation terminal or tasks for Eclipse Che workspace containers. +icon: /v3/images/eclipse-che-logo.png +category: Other +repository: 'https://github.com/eclipse/che-machine-exec/' +firstPublicationDate: '2019-11-07' +latestUpdateDate: '2021-02-19' +spec: + endpoints: + - name: che-machine-exec + public: true + targetPort: 4444 + attributes: + protocol: ws + type: terminal + discoverable: false + secure: true + cookiesAuthEnabled: true + containers: + - name: che-machine-exec + image: 'quay.io/eclipse/che-machine-exec:nightly' + ports: + - exposedPort: 4444 + command: + - /go/bin/che-machine-exec + - '--url' + - '127.0.0.1:4444' diff --git a/tools/build/tests/_data/meta/no-container.yaml b/tools/build/tests/_data/meta/no-container.yaml new file mode 100644 index 0000000000..7d10be360e --- /dev/null +++ b/tools/build/tests/_data/meta/no-container.yaml @@ -0,0 +1,17 @@ +apiVersion: v2 +publisher: eclipse +name: che-theia +version: next +type: Che Editor +displayName: no-container +title: Eclipse Theia development version. +description: 'Eclipse Theia, get the latest release each day.' +icon: /v3/images/eclipse-che-logo.png +category: Editor +repository: 'https://github.com/eclipse/che-theia' +firstPublicationDate: '2019-03-07' +latestUpdateDate: '2021-02-19' +spec: + initContainers: + - name: foo + image: 'quay.io/foobar:next' diff --git a/tools/build/tests/_data/meta/vscode-extension.yaml b/tools/build/tests/_data/meta/vscode-extension.yaml new file mode 100644 index 0000000000..8d7f322cb1 --- /dev/null +++ b/tools/build/tests/_data/meta/vscode-extension.yaml @@ -0,0 +1,25 @@ +apiVersion: v2 +publisher: redhat-developer +name: che-omnisharp-plugin +version: latest +type: VS Code extension +displayName: omnisharp-theia-plugin +title: omnisharp-theia-plugin +description: omnisharp-theia-plugin +icon: /v3/images/eclipse-che-logo.png +category: Other +repository: 'https://github.com/redhat-developer/omnisharp-theia-plugin' +firstPublicationDate: '2019-12-03' +latestUpdateDate: '2021-02-19' +spec: + containers: + - image: 'quay.io/eclipse/che-plugin-sidecar@sha256:f398e3ffd5200c56bf56a6f7f9e8db4aa3f639a6125850f169414528260dce8a' + name: theia-omnisharp + volumes: + - name: nuget + mountPath: /home/theia/.nuget + memoryLimit: 1Gi + cpuRequest: 30m + cpuLimit: 500m + extensions: + - 'https://github.com/redhat-developer/omnisharp-theia-plugin/releases/download/v0.0.6/omnisharp_theia_plugin.theia' diff --git a/tools/build/tests/devfile/meta-yaml-to-devfile-yaml.spec.ts b/tools/build/tests/devfile/meta-yaml-to-devfile-yaml.spec.ts new file mode 100644 index 0000000000..71a1739b92 --- /dev/null +++ b/tools/build/tests/devfile/meta-yaml-to-devfile-yaml.spec.ts @@ -0,0 +1,189 @@ +/********************************************************************** + * Copyright (c) 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 + ***********************************************************************/ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata'; + +import * as fs from 'fs-extra'; +import * as jsYaml from 'js-yaml'; +import * as path from 'path'; + +import { Container } from 'inversify'; +import { MetaYamlToDevfileYaml } from '../../src/devfile/meta-yaml-to-devfile-yaml'; + +describe('Test MetaYamlToDevfileYaml', () => { + let container: Container; + + let metaYamlToDevfileYaml: MetaYamlToDevfileYaml; + + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + + container = new Container(); + container.bind(MetaYamlToDevfileYaml).toSelf().inSingletonScope(); + metaYamlToDevfileYaml = container.get(MetaYamlToDevfileYaml); + }); + + test('machine-exec', async () => { + const metaYamlPath = path.resolve(__dirname, '..', '_data', 'meta', 'machine-exec-plugin-meta.yaml'); + const metaYamlContent = await fs.readFile(metaYamlPath, 'utf-8'); + const metaYaml = jsYaml.safeLoad(metaYamlContent); + const devfileYaml = metaYamlToDevfileYaml.convert(metaYaml); + expect(devfileYaml.schemaVersion).toBe('2.1.0'); + expect(devfileYaml.metadata?.name).toBe('Che machine-exec Service'); + expect(devfileYaml.components).toBeDefined(); + expect(devfileYaml.components?.length).toBe(1); + const component = devfileYaml.components[0]; + expect(component.name).toBe('che-machine-exec'); + const componentContainer = component.container; + expect(componentContainer.image).toBe('quay.io/eclipse/che-machine-exec:nightly'); + expect(componentContainer.args).toStrictEqual(['/go/bin/che-machine-exec', '--url', '0.0.0.0:4444']); + + expect(componentContainer.endpoints).toBeDefined(); + expect(componentContainer.endpoints?.length).toBe(1); + const endpoint = componentContainer.endpoints[0]; + expect(endpoint.name).toBe('che-machine-exec'); + expect(endpoint.exposure).toBe('public'); + const endpointAttributes = endpoint.attributes; + expect(endpointAttributes.type).toBe('terminal'); + }); + + test('che-theia', async () => { + const metaYamlPath = path.resolve(__dirname, '..', '_data', 'meta', 'che-theia-meta.yaml'); + const metaYamlContent = await fs.readFile(metaYamlPath, 'utf-8'); + const metaYaml = jsYaml.safeLoad(metaYamlContent); + const devfileYaml = metaYamlToDevfileYaml.convert(metaYaml); + expect(devfileYaml.schemaVersion).toBe('2.1.0'); + expect(devfileYaml.metadata?.name).toBe('theia-ide'); + expect(devfileYaml.components).toBeDefined(); + expect(devfileYaml.components?.length).toBe(5); + const theiaIdeComponent = devfileYaml.components[2]; + expect(theiaIdeComponent.name).toBe('theia-ide'); + const theiaIdeComponentContainer = theiaIdeComponent.container; + expect(theiaIdeComponentContainer.image).toBe('quay.io/eclipse/che-theia:next'); + + expect(theiaIdeComponentContainer.endpoints).toBeDefined(); + expect(theiaIdeComponentContainer.endpoints?.length).toBe(7); + const theiaIdeFirstEndpoint = theiaIdeComponentContainer.endpoints[0]; + expect(theiaIdeFirstEndpoint.name).toBe('theia'); + expect(theiaIdeFirstEndpoint.exposure).toBe('public'); + const theiaIdeFirstEndpointAttributes = theiaIdeFirstEndpoint.attributes; + expect(theiaIdeFirstEndpointAttributes.type).toBe('ide'); + + expect(theiaIdeComponentContainer.env).toBeDefined(); + expect(theiaIdeComponentContainer.env?.length).toBe(4); + const theiaIdeFirstEnv = theiaIdeComponentContainer.env[0]; + expect(theiaIdeFirstEnv.name).toBe('THEIA_PLUGINS'); + expect(theiaIdeFirstEnv.value).toBe('local-dir:///plugins'); + + const theiaHostEnv = theiaIdeComponentContainer.env.find((env: any) => env.name === 'THEIA_HOST'); + expect(theiaHostEnv.name).toBe('THEIA_HOST'); + // 127.0.0.1 should have been replaced by 0.0.0.0 + expect(theiaHostEnv.value).toBe('0.0.0.0'); + + expect(theiaIdeComponentContainer.volumeMounts).toBeDefined(); + expect(theiaIdeComponentContainer.volumeMounts?.length).toBe(2); + const theiaIdeFirstVolumeMount = theiaIdeComponentContainer.volumeMounts[0]; + expect(theiaIdeFirstVolumeMount.name).toBe('plugins'); + expect(theiaIdeFirstVolumeMount.path).toBe('/plugins'); + + const remoteRuntimeInjectorComponent = devfileYaml.components[4]; + expect(remoteRuntimeInjectorComponent.name).toBe('remote-runtime-injector'); + const remoteRuntimeInjectorComponentContainer = remoteRuntimeInjectorComponent.container; + expect(remoteRuntimeInjectorComponentContainer.image).toBe( + 'quay.io/eclipse/che-theia-endpoint-runtime-binary:next' + ); + + const pluginsVolumeComponent = devfileYaml.components[0]; + expect(pluginsVolumeComponent.name).toBe('plugins'); + expect(pluginsVolumeComponent.volume).toStrictEqual({}); + + const theiaLocalVolumeComponent = devfileYaml.components[1]; + expect(theiaLocalVolumeComponent.name).toBe('theia-local'); + expect(theiaLocalVolumeComponent.volume).toStrictEqual({}); + + const remoteEndpointVolumeComponent = devfileYaml.components[3]; + expect(remoteEndpointVolumeComponent.name).toBe('remote-endpoint'); + expect(remoteEndpointVolumeComponent.volume).toBeDefined(); + expect(remoteEndpointVolumeComponent.volume.ephemeral).toBeTruthy(); + + expect(devfileYaml.commands).toBeDefined(); + expect(devfileYaml.commands?.length).toBe(1); + const devfileFirstCommand = devfileYaml.commands[0]; + expect(devfileFirstCommand.id).toBe('init-container-command'); + expect(devfileFirstCommand.apply).toStrictEqual({ component: 'remote-runtime-injector' }); + + expect(devfileYaml.events).toBeDefined(); + expect(devfileYaml.events.preStart).toBeDefined(); + expect(devfileYaml.events?.preStart?.length).toBe(1); + const preStartFirstEvent = devfileYaml.events.preStart[0]; + expect(preStartFirstEvent).toBe('init-container-command'); + }); + + test('no container', async () => { + const metaYamlPath = path.resolve(__dirname, '..', '_data', 'meta', 'no-container.yaml'); + const metaYamlContent = await fs.readFile(metaYamlPath, 'utf-8'); + const metaYaml = jsYaml.safeLoad(metaYamlContent); + const devfileYaml = metaYamlToDevfileYaml.convert(metaYaml); + expect(devfileYaml.schemaVersion).toBe('2.1.0'); + expect(devfileYaml.metadata?.name).toBe('no-container'); + expect(devfileYaml.components).toBeDefined(); + expect(devfileYaml.components?.length).toBe(1); + const component = devfileYaml.components[0]; + expect(component.name).toBe('foo'); + const componentContainer = component.container; + expect(componentContainer.image).toBe('quay.io/foobar:next'); + }); + + test('vscode extension', async () => { + const metaYamlPath = path.resolve(__dirname, '..', '_data', 'meta', 'vscode-extension.yaml'); + const metaYamlContent = await fs.readFile(metaYamlPath, 'utf-8'); + const metaYaml = jsYaml.safeLoad(metaYamlContent); + const devfileYaml = metaYamlToDevfileYaml.convert(metaYaml); + expect(devfileYaml).toBeUndefined(); + }); + + test('container without endpoints', async () => { + const metaYamlPath = path.resolve(__dirname, '..', '_data', 'meta', 'container-no-endpoints.yaml'); + const metaYamlContent = await fs.readFile(metaYamlPath, 'utf-8'); + const metaYaml = jsYaml.safeLoad(metaYamlContent); + const devfileYaml = metaYamlToDevfileYaml.convert(metaYaml); + expect(devfileYaml.schemaVersion).toBe('2.1.0'); + expect(devfileYaml.metadata?.name).toBe('no-endpoint'); + expect(devfileYaml.components).toBeDefined(); + expect(devfileYaml.components?.length).toBe(1); + const component = devfileYaml.components[0]; + expect(component.name).toBe('no-endpoint'); + const componentContainer = component.container; + expect(componentContainer.image).toBe('quay.io/no-endpoint'); + }); + + test('container with minimal endpoint', async () => { + const metaYamlPath = path.resolve(__dirname, '..', '_data', 'meta', 'container-minimal-endpoint.yaml'); + const metaYamlContent = await fs.readFile(metaYamlPath, 'utf-8'); + const metaYaml = jsYaml.safeLoad(metaYamlContent); + const devfileYaml = metaYamlToDevfileYaml.convert(metaYaml); + expect(devfileYaml.schemaVersion).toBe('2.1.0'); + expect(devfileYaml.metadata?.name).toBe('minimal-endpoint'); + expect(devfileYaml.components).toBeDefined(); + expect(devfileYaml.components?.length).toBe(1); + const component = devfileYaml.components[0]; + expect(component.name).toBe('minimal-endpoint'); + const componentContainer = component.container; + expect(componentContainer.image).toBe('quay.io/minimal-endpoint'); + + expect(componentContainer.endpoints).toBeDefined(); + expect(componentContainer.endpoints?.length).toBe(1); + const wwwEndpoint = componentContainer.endpoints[0]; + expect(wwwEndpoint.name).toBe('www'); + expect(wwwEndpoint.exposure).toBeUndefined(); + expect(wwwEndpoint.attributes).toBeUndefined(); + }); +}); diff --git a/tools/build/tests/inversify-binding.spec.ts b/tools/build/tests/inversify-binding.spec.ts index 610bf7fbd5..24da32ac46 100644 --- a/tools/build/tests/inversify-binding.spec.ts +++ b/tools/build/tests/inversify-binding.spec.ts @@ -25,6 +25,7 @@ import { FeaturedAnalyzer } from '../src/featured/featured-analyzer'; import { FeaturedWriter } from '../src/featured/featured-writer'; import { IndexWriter } from '../src/meta-yaml/index-writer'; import { InversifyBinding } from '../src/inversify-binding'; +import { MetaYamlToDevfileYaml } from '../src/devfile/meta-yaml-to-devfile-yaml'; import { MetaYamlWriter } from '../src/meta-yaml/meta-yaml-writer'; import { RecommendationsAnalyzer } from '../src/recommendations/recommendations-analyzer'; import { RecommendationsWriter } from '../src/recommendations/recommendations-writer'; @@ -56,6 +57,9 @@ describe('Test InversifyBinding', () => { expect(container.get(CheTheiaPluginsAnalyzer)).toBeDefined(); expect(container.get(CheTheiaPluginsMetaYamlGenerator)).toBeDefined(); + // check devfile module + expect(container.get(MetaYamlToDevfileYaml)).toBeDefined(); + // check editor module expect(container.get(CheEditorsAnalyzer)).toBeDefined(); expect(container.get(CheEditorsMetaYamlGenerator)).toBeDefined(); diff --git a/tools/build/tests/meta-yaml/index-writer.spec.ts b/tools/build/tests/meta-yaml/index-writer.spec.ts index 8ff5350294..04af7ede83 100644 --- a/tools/build/tests/meta-yaml/index-writer.spec.ts +++ b/tools/build/tests/meta-yaml/index-writer.spec.ts @@ -79,6 +79,7 @@ describe('Test IndexWriter', () => { expect(jsonOutput[0].id).toBe('my-publisher/my-che-editor-name/latest'); expect(jsonOutput[0].description).toBe('my-che-plugin'); expect(jsonOutput[0].links.self).toBe('/v3/plugins/my-publisher/my-che-editor-name/latest'); + expect(jsonOutput[0].links.devfile).toBe('/v3/plugins/my-publisher/my-che-editor-name/latest/devfile.yaml'); expect(jsonOutput[0].name).toBe('my-che-editor-name'); expect(jsonOutput[0].publisher).toBe('my-publisher'); expect(jsonOutput[0].type).toBe('Che Editor'); @@ -87,6 +88,7 @@ describe('Test IndexWriter', () => { expect(jsonOutput[1].id).toBe('my-publisher/my-che-plugin-name/latest'); expect(jsonOutput[1].description).toBe('my-che-plugin'); expect(jsonOutput[1].links.self).toBe('/v3/plugins/my-publisher/my-che-plugin-name/latest'); + expect(jsonOutput[1].links.devfile).toBe('/v3/plugins/my-publisher/my-che-plugin-name/latest/devfile.yaml'); expect(jsonOutput[1].name).toBe('my-che-plugin-name'); expect(jsonOutput[1].publisher).toBe('my-publisher'); expect(jsonOutput[1].type).toBe('Che Plugin'); @@ -95,6 +97,8 @@ describe('Test IndexWriter', () => { expect(jsonOutput[2].id).toBe('my-publisher/my-name/latest'); expect(jsonOutput[2].description).toBe('my-description'); expect(jsonOutput[2].links.self).toBe('/v3/plugins/my-publisher/my-name/latest'); + // no devfile generation for VS Code extensions + expect(jsonOutput[2].links.devfile).toBeUndefined(); expect(jsonOutput[2].name).toBe('my-name'); expect(jsonOutput[2].publisher).toBe('my-publisher'); expect(jsonOutput[2].type).toBe('VS Code extension'); diff --git a/tools/build/tests/meta-yaml/meta-yaml-writer.spec.ts b/tools/build/tests/meta-yaml/meta-yaml-writer.spec.ts index 56641dc72d..5721933556 100644 --- a/tools/build/tests/meta-yaml/meta-yaml-writer.spec.ts +++ b/tools/build/tests/meta-yaml/meta-yaml-writer.spec.ts @@ -15,6 +15,7 @@ import * as moment from 'moment'; import { Container } from 'inversify'; import { MetaYamlPluginInfo } from '../../src/meta-yaml/meta-yaml-plugin-info'; +import { MetaYamlToDevfileYaml } from '../../src/devfile/meta-yaml-to-devfile-yaml'; import { MetaYamlWriter } from '../../src/meta-yaml/meta-yaml-writer'; import { VsixInfo } from '../../src/extensions/vsix-info'; @@ -27,10 +28,16 @@ describe('Test MetaYamlWriter', () => { let embedVsix = false; const vsixInfos = new Map(); + const metaYamlToDevfileYamlConvertMethod = jest.fn(); + const metaYamlToDevfileYaml = { + convert: metaYamlToDevfileYamlConvertMethod, + } as any; + function initContainer() { container = new Container(); container.bind('string').toConstantValue('/fake-output').whenTargetNamed('OUTPUT_ROOT_DIRECTORY'); container.bind('boolean').toConstantValue(embedVsix).whenTargetNamed('EMBED_VSIX'); + container.bind(MetaYamlToDevfileYaml).toConstantValue(metaYamlToDevfileYaml); container.bind(MetaYamlWriter).toSelf().inSingletonScope(); } @@ -387,4 +394,60 @@ spec: {} // no version written with disable Latest expect(fsWriteFileSpy).toHaveBeenCalledTimes(1); }); + + test('meta yaml --> devfile yaml', async () => { + initContainer(); + metaYamlWriter = container.get(MetaYamlWriter); + + const fsCopyFileSpy = jest.spyOn(fs, 'copyFile'); + const fsEnsureDirSpy = jest.spyOn(fs, 'ensureDir'); + const fsWriteFileSpy = jest.spyOn(fs, 'writeFile'); + + fsEnsureDirSpy.mockReturnValue(); + fsCopyFileSpy.mockReturnValue(); + fsWriteFileSpy.mockReturnValue(); + + metaPluginYaml = { + apiVersion: 'v2', + id: 'foo/bar', + publisher: 'foo', + name: 'bar', + version: '0.0.1', + displayName: 'minimal-endpoint', + title: 'minimal-endpoint', + description: 'minimal-endpoint', + icon: '/v3/images/eclipse-che-logo.png', + category: 'Other', + repository: 'http://fake-repository', + firstPublicationDate: '2019-01-01', + latestUpdateDate, + type: 'Che Plugin', + spec: { + endpoints: [ + { + name: 'www', + targetPort: 3100, + }, + ], + containers: [ + { + name: 'minimal-endpoint', + image: 'quay.io/minimal-endpoint', + }, + ], + }, + } as any; + + const metaYamlPlugins: MetaYamlPluginInfo[] = [metaPluginYaml]; + metaYamlToDevfileYamlConvertMethod.mockReturnValue({ devfileFakeResult: 'dummy' }); + await metaYamlWriter.write(metaYamlPlugins); + + expect(fsWriteFileSpy).toHaveBeenCalledTimes(2); + + expect(fsWriteFileSpy).toHaveBeenNthCalledWith( + 2, + '/fake-output/v3/plugins/foo/bar/latest/devfile.yaml', + 'devfileFakeResult: dummy\n' + ); + }); });