mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
This commit is contained in:
parent
761c8053d4
commit
5d59eb453c
8 changed files with 23 additions and 296 deletions
|
@ -42,9 +42,6 @@ export function createJestConfig({
|
|||
transformIgnorePatterns: [
|
||||
'[/\\\\]node_modules[/\\\\].+\\.js$'
|
||||
],
|
||||
testPathIgnorePatterns: [
|
||||
`.*[/\\\\]plugins[/\\\\]code[/\\\\].*`
|
||||
],
|
||||
snapshotSerializers: [
|
||||
`${kibanaDirectory}/node_modules/enzyme-to-json/serializer`
|
||||
],
|
||||
|
|
|
@ -6,21 +6,5 @@
|
|||
|
||||
export enum InstallationType {
|
||||
Embed,
|
||||
Download,
|
||||
Plugin,
|
||||
}
|
||||
|
||||
export enum InstallEventType {
|
||||
DOWNLOADING,
|
||||
UNPACKING,
|
||||
DONE,
|
||||
FAIL,
|
||||
}
|
||||
|
||||
export interface InstallEvent {
|
||||
langServerName: string;
|
||||
eventType: InstallEventType;
|
||||
progress?: number;
|
||||
message?: string;
|
||||
params?: any;
|
||||
}
|
||||
|
|
|
@ -249,7 +249,7 @@ async function initCodeNode(server: Server, serverOptions: ServerOptions, log: L
|
|||
fileRoute(codeServerRouter, serverOptions);
|
||||
workspaceRoute(codeServerRouter, serverOptions);
|
||||
symbolByQnameRoute(codeServerRouter, log);
|
||||
installRoute(codeServerRouter, lspService, installManager);
|
||||
installRoute(codeServerRouter, lspService);
|
||||
lspRoute(codeServerRouter, lspService, serverOptions);
|
||||
setupRoute(codeServerRouter);
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@ beforeEach(async () => {
|
|||
// @ts-ignore
|
||||
installManager,
|
||||
new ConsoleLoggerFactory(),
|
||||
// @ts-ignore
|
||||
repoConfigController
|
||||
);
|
||||
});
|
||||
|
|
|
@ -6,12 +6,8 @@
|
|||
/* eslint-disable no-console */
|
||||
|
||||
import fs from 'fs';
|
||||
import nock from 'nock';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import tar from 'tar-fs';
|
||||
import URL from 'url';
|
||||
import zlib from 'zlib';
|
||||
import { LanguageServers } from './language_servers';
|
||||
import { InstallManager } from './install_manager';
|
||||
import { ServerOptions } from '../server_options';
|
||||
|
@ -23,14 +19,9 @@ import { InstallationType } from '../../common/installation';
|
|||
const LANG_SERVER_NAME = 'Java';
|
||||
const langSrvDef = LanguageServers.find(l => l.name === LANG_SERVER_NAME)!;
|
||||
|
||||
const fakeTestDir = fs.mkdtempSync(path.join(os.tmpdir(), 'foo-'));
|
||||
const fakePackageFile: string = path.join(fakeTestDir, 'fakePackage.tar.gz');
|
||||
const fakeTarDir = path.join(fakeTestDir, 'fakePackage');
|
||||
const fakeFile = 'fake.file';
|
||||
const fakeContent = 'fake content';
|
||||
const options: ServerOptions = {
|
||||
langServerPath: fakeTestDir,
|
||||
} as ServerOptions;
|
||||
const fakeTestDir = path.join(os.tmpdir(), 'foo-');
|
||||
|
||||
const options: ServerOptions = {} as ServerOptions;
|
||||
|
||||
const server = new Server();
|
||||
server.config = () => {
|
||||
|
@ -48,68 +39,14 @@ server.config = () => {
|
|||
|
||||
const manager = new InstallManager(server, options);
|
||||
|
||||
beforeAll(async () => {
|
||||
console.log('test folder is: ' + fakeTestDir);
|
||||
beforeAll(() => {
|
||||
fs.mkdirSync(fakeTestDir);
|
||||
});
|
||||
|
||||
fs.mkdirSync(fakeTarDir);
|
||||
// create a fake tar.gz package for testing
|
||||
fs.writeFileSync(path.join(fakeTarDir, fakeFile), fakeContent);
|
||||
return await new Promise(resolve => {
|
||||
tar
|
||||
.pack(fakeTarDir)
|
||||
.pipe(zlib.createGzip())
|
||||
.pipe(fs.createWriteStream(fakePackageFile))
|
||||
.on('finish', resolve);
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
const downloadUrl = URL.parse(
|
||||
langSrvDef.downloadUrl!(langSrvDef, server.config().get('pkg.version'))
|
||||
);
|
||||
nock.cleanAll();
|
||||
// mimic github's behavior, redirect to a s3 address
|
||||
nock(`${downloadUrl.protocol}//${downloadUrl.host!}`)
|
||||
.get(downloadUrl.path!)
|
||||
.reply(302, '', {
|
||||
Location: 'https://s3.amazonaws.com/file.tar.gz',
|
||||
});
|
||||
nock('https://s3.amazonaws.com')
|
||||
.get('/file.tar.gz')
|
||||
.replyWithFile(200, fakePackageFile, {
|
||||
'Content-Length': fs.statSync(fakePackageFile).size.toString(),
|
||||
'Content-Disposition': `attachment ;filename=${path.basename(fakePackageFile)}`,
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
const p = manager.installationPath(langSrvDef);
|
||||
if (p) rimraf.sync(p);
|
||||
});
|
||||
afterAll(() => {
|
||||
nock.cleanAll();
|
||||
rimraf.sync(fakeTestDir);
|
||||
});
|
||||
|
||||
test('it can download a package', async () => {
|
||||
langSrvDef.installationType = InstallationType.Download;
|
||||
const p = await manager.downloadFile(langSrvDef);
|
||||
console.log('package downloaded at ' + p);
|
||||
expect(fs.existsSync(p)).toBeTruthy();
|
||||
expect(fs.statSync(p).size).toBe(fs.statSync(fakePackageFile).size);
|
||||
});
|
||||
|
||||
test('it can install language server', async () => {
|
||||
expect(manager.status(langSrvDef)).toBe(LanguageServerStatus.NOT_INSTALLED);
|
||||
langSrvDef.installationType = InstallationType.Download;
|
||||
const installPromise = manager.install(langSrvDef);
|
||||
expect(manager.status(langSrvDef)).toBe(LanguageServerStatus.INSTALLING);
|
||||
await installPromise;
|
||||
expect(manager.status(langSrvDef)).toBe(LanguageServerStatus.READY);
|
||||
const installPath = manager.installationPath(langSrvDef)!;
|
||||
const fakeFilePath = path.join(installPath, fakeFile);
|
||||
expect(fs.existsSync(fakeFilePath)).toBeTruthy();
|
||||
expect(fs.readFileSync(fakeFilePath, 'utf8')).toBe(fakeContent);
|
||||
});
|
||||
|
||||
test('install language server by plugin', async () => {
|
||||
langSrvDef.installationType = InstallationType.Plugin;
|
||||
expect(manager.status(langSrvDef)).toBe(LanguageServerStatus.NOT_INSTALLED);
|
||||
|
|
|
@ -4,204 +4,40 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import fs from 'fs';
|
||||
import { Server } from 'hapi';
|
||||
import mkdirp from 'mkdirp';
|
||||
import fetch from 'node-fetch';
|
||||
import path from 'path';
|
||||
import tar from 'tar-fs';
|
||||
import zlib from 'zlib';
|
||||
import { InstallationType, InstallEvent, InstallEventType } from '../../common/installation';
|
||||
import { InstallationType } from '../../common/installation';
|
||||
import { LanguageServerStatus } from '../../common/language_server';
|
||||
import { ServerOptions } from '../server_options';
|
||||
import { LanguageServerDefinition } from './language_servers';
|
||||
|
||||
const DOWNLOAD_PROGRESS_WEIGHT = 0.9;
|
||||
|
||||
export class InstallManager {
|
||||
private eventEmitter = new EventEmitter();
|
||||
private readonly basePath: string;
|
||||
private installing: Set<LanguageServerDefinition> = new Set();
|
||||
|
||||
constructor(readonly server: Server, readonly serverOptions: ServerOptions) {
|
||||
this.basePath = serverOptions.langServerPath;
|
||||
}
|
||||
constructor(readonly server: Server, readonly serverOptions: ServerOptions) {}
|
||||
|
||||
public status(def: LanguageServerDefinition): LanguageServerStatus {
|
||||
if (def.installationType === InstallationType.Embed) {
|
||||
return LanguageServerStatus.READY;
|
||||
}
|
||||
if (def.installationType === InstallationType.Plugin) {
|
||||
// @ts-ignore
|
||||
const plugin = this.server.plugins[def.installationPluginName!];
|
||||
if (plugin) {
|
||||
const pluginPath = plugin.install.path;
|
||||
if (fs.existsSync(pluginPath)) {
|
||||
return LanguageServerStatus.READY;
|
||||
}
|
||||
// @ts-ignore
|
||||
const plugin = this.server.plugins[def.installationPluginName!];
|
||||
if (plugin) {
|
||||
const pluginPath = plugin.install.path;
|
||||
if (fs.existsSync(pluginPath)) {
|
||||
return LanguageServerStatus.READY;
|
||||
}
|
||||
return LanguageServerStatus.NOT_INSTALLED;
|
||||
}
|
||||
if (this.installing.has(def)) {
|
||||
return LanguageServerStatus.INSTALLING;
|
||||
}
|
||||
const installationPath = this.installationPath(def);
|
||||
return fs.existsSync(installationPath!)
|
||||
? LanguageServerStatus.READY
|
||||
: LanguageServerStatus.NOT_INSTALLED;
|
||||
}
|
||||
|
||||
public on(fn: (event: InstallEvent) => void) {
|
||||
this.eventEmitter.on('event', fn);
|
||||
}
|
||||
|
||||
public async install(def: LanguageServerDefinition) {
|
||||
if (def.installationType === InstallationType.Download) {
|
||||
if (!this.installing.has(def)) {
|
||||
try {
|
||||
this.installing.add(def);
|
||||
const packageFile = await this.downloadFile(def);
|
||||
await this.unPack(packageFile, def);
|
||||
this.sendEvent({
|
||||
langServerName: def.name,
|
||||
eventType: InstallEventType.DONE,
|
||||
progress: 1,
|
||||
message: `install ${def.name} done.`,
|
||||
});
|
||||
} catch (e) {
|
||||
this.sendEvent({
|
||||
langServerName: def.name,
|
||||
eventType: InstallEventType.FAIL,
|
||||
message: `install ${def.name} failed. error: ${e.message}`,
|
||||
});
|
||||
throw e;
|
||||
} finally {
|
||||
this.installing.delete(def);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error("can't install this language server by downloading");
|
||||
}
|
||||
}
|
||||
|
||||
public async downloadFile(def: LanguageServerDefinition): Promise<string> {
|
||||
const url =
|
||||
typeof def.downloadUrl === 'function'
|
||||
? def.downloadUrl(def, this.getKibanaVersion())
|
||||
: def.downloadUrl;
|
||||
|
||||
const res = await fetch(url!);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unable to download language server ${def.name} from url ${url}`);
|
||||
}
|
||||
|
||||
const installationPath = this.installationPath(def)!;
|
||||
let filename: string;
|
||||
const header = res.headers.get('Content-Disposition');
|
||||
const FILE_NAME_HEAD_PREFIX = 'filename=';
|
||||
if (header && header.includes(FILE_NAME_HEAD_PREFIX)) {
|
||||
filename = header.substring(
|
||||
header.indexOf(FILE_NAME_HEAD_PREFIX) + FILE_NAME_HEAD_PREFIX.length
|
||||
);
|
||||
} else {
|
||||
filename = `${def.name}-${def.version}.tar.gz`;
|
||||
}
|
||||
const downloadPath = path.resolve(installationPath, '..', filename);
|
||||
mkdirp.sync(installationPath);
|
||||
const stream = fs.createWriteStream(downloadPath);
|
||||
const total = parseInt(res.headers.get('Content-Length') || '0', 10);
|
||||
let downloaded = 0;
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
res
|
||||
// @ts-ignore
|
||||
.body!.pipe(stream)
|
||||
.on('error', (error: any) => {
|
||||
reject(error);
|
||||
})
|
||||
.on('data', (data: Buffer) => {
|
||||
downloaded += data.length;
|
||||
this.sendEvent({
|
||||
langServerName: def.name,
|
||||
eventType: InstallEventType.DOWNLOADING,
|
||||
progress: DOWNLOAD_PROGRESS_WEIGHT * (downloaded / total),
|
||||
message: `downloading ${filename}(${downloaded}/${total})`,
|
||||
params: {
|
||||
downloaded,
|
||||
total,
|
||||
},
|
||||
});
|
||||
})
|
||||
.on('finish', () => {
|
||||
if (res.ok) {
|
||||
resolve(downloadPath);
|
||||
} else {
|
||||
reject(new Error(res.statusText));
|
||||
}
|
||||
});
|
||||
});
|
||||
return LanguageServerStatus.NOT_INSTALLED;
|
||||
}
|
||||
|
||||
public installationPath(def: LanguageServerDefinition): string | undefined {
|
||||
if (def.installationType === InstallationType.Embed) {
|
||||
return def.embedPath!;
|
||||
} else if (def.installationType === InstallationType.Plugin) {
|
||||
// @ts-ignore
|
||||
const plugin: any = this.server.plugins[def.installationPluginName];
|
||||
if (plugin) {
|
||||
return plugin.install.path;
|
||||
}
|
||||
return undefined;
|
||||
} else {
|
||||
let version = def.version;
|
||||
if (version) {
|
||||
if (def.build) {
|
||||
version += '-' + def.build;
|
||||
}
|
||||
} else {
|
||||
version = this.getKibanaVersion();
|
||||
}
|
||||
return path.join(this.basePath, def.installationFolderName || def.name, version);
|
||||
}
|
||||
}
|
||||
|
||||
private getKibanaVersion(): string {
|
||||
return this.server.config().get('pkg.version');
|
||||
}
|
||||
|
||||
private async unPack(packageFile: string, def: LanguageServerDefinition) {
|
||||
const dest = this.installationPath(def)!;
|
||||
this.sendEvent({
|
||||
langServerName: def.name,
|
||||
eventType: InstallEventType.UNPACKING,
|
||||
progress: DOWNLOAD_PROGRESS_WEIGHT,
|
||||
message: `unpacking ${path.basename(packageFile)}`,
|
||||
});
|
||||
const ext = path.extname(packageFile);
|
||||
switch (ext) {
|
||||
case '.gz':
|
||||
await this.unPackTarball(packageFile, dest);
|
||||
break;
|
||||
default:
|
||||
// todo support .zip
|
||||
throw new Error(`unknown extension "${ext}"`);
|
||||
// @ts-ignore
|
||||
const plugin: any = this.server.plugins[def.installationPluginName];
|
||||
if (plugin) {
|
||||
return plugin.install.path;
|
||||
}
|
||||
// await decompress(packageFile, '/tmp/1/1');
|
||||
}
|
||||
|
||||
private sendEvent(event: InstallEvent) {
|
||||
this.eventEmitter.emit('event', event);
|
||||
}
|
||||
|
||||
private unPackTarball(packageFile: string, dest: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.createReadStream(packageFile)
|
||||
.on('error', reject)
|
||||
.pipe(zlib.createGunzip())
|
||||
.on('error', reject)
|
||||
.pipe(tar.extract(dest))
|
||||
.on('error', reject)
|
||||
.on('finish', resolve);
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,17 +6,11 @@
|
|||
|
||||
import * as Boom from 'boom';
|
||||
import { Request } from 'hapi';
|
||||
import { InstallationType } from '../../common/installation';
|
||||
import { InstallManager } from '../lsp/install_manager';
|
||||
import { LanguageServerDefinition, LanguageServers } from '../lsp/language_servers';
|
||||
import { LspService } from '../lsp/lsp_service';
|
||||
import { CodeServerRouter } from '../security';
|
||||
|
||||
export function installRoute(
|
||||
server: CodeServerRouter,
|
||||
lspService: LspService,
|
||||
installManager: InstallManager
|
||||
) {
|
||||
export function installRoute(server: CodeServerRouter, lspService: LspService) {
|
||||
const kibanaVersion = server.server.config().get('pkg.version') as string;
|
||||
const status = (def: LanguageServerDefinition) => ({
|
||||
name: def.name,
|
||||
|
@ -51,24 +45,4 @@ export function installRoute(
|
|||
},
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
server.route({
|
||||
path: '/api/code/install/{name}',
|
||||
requireAdmin: true,
|
||||
async handler(req: Request) {
|
||||
const name = req.params.name;
|
||||
const def = LanguageServers.find(d => d.name === name);
|
||||
if (def) {
|
||||
if (def.installationType === InstallationType.Plugin) {
|
||||
return Boom.methodNotAllowed(
|
||||
`${name} language server can only be installed by plugin ${def.installationPluginName}`
|
||||
);
|
||||
}
|
||||
await installManager.install(def);
|
||||
} else {
|
||||
return Boom.notFound(`language server ${name} not found.`);
|
||||
}
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -29,8 +29,6 @@ export class ServerOptions {
|
|||
|
||||
public readonly credsPath = resolve(this.config.get('path.data'), 'code/credentials');
|
||||
|
||||
public readonly langServerPath = resolve(this.config.get('path.data'), 'code/langserver');
|
||||
|
||||
public readonly jdtWorkspacePath = resolve(this.config.get('path.data'), 'code/jdt_ws');
|
||||
|
||||
public readonly jdtConfigPath = resolve(this.config.get('path.data'), 'code/jdt_config');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue