Upgrade Hapi in legacy platform to v17 (#21707)

* Disable even-better monitoring

* Upgrade to Hapi v15

* Upgrade to Hapi v16

* Handle optional req params correctly

* Update http and kbnServer

* Get mocha tests passing

* Convert `reply` usages [wip]

* Fix Joi and Plugin incompatibilities

* Get server up and running

* Get basic logging working

* Fix optimizer

* Fix recent route handlers

* Various fixes

* Fix recent routes

* Upgrade wreck for async/await

* Fix mocha tests

* Fix joi issues

* Fix xpack jest tests

* Fix recent routes

* Fix tests

* Fix index setup

* Decouple monitoring stats collection from good plugin

* Update reload logging test to work

* Reimplement logging with updated good plugin

* Fix unit tests

* Fix getConnections back

* Make LegacyLoggingServer compatible with Hapi v17

* Update joi types

* Fix x-pack unit tests

* Remove stray debugger

* Remove hapi-compat

* Fix API integrations

* Upgrade boom

* Fix security plugin

* Misc fixes

* bump

* Fix licensePreRoutingFactory

* Fix failing integration tests

* Remove unnecessary test change

* Remove hapi-latest package

* fx

* Various cleanup

* Fix race condition in oppsy events

* Use elastic/good fork

* Fix boom.wrap and hapi-latest changes

* Simplify LegacyLoggingServer updates

* package.json cleanup + test fix

* yarn.lock cleanup

* Change good tag

* Fixes

* Change return err -> throw err in routes

* Fix await returns

* Fix new load_data test

* Make cookie security flags consistent

* tmp doc

* Fix types

* Fix tests

* Upgrade canvas plugin

* Move good package to published @elastic/good one

* Fix SO test

* Fix logging reloading

* Update APM apis

* Fix error logging

* Fix logging test

* Convert spaces plugin

* Add validation error shim

* Remove 7.0 release notes

* Await renderApp

* Fix ccr routes

* Prevent header popovers from scrolling with page content (#23850)

* Fix spaces test

* new yarn.lock-s

* Fix spaces tests

* Remove h2o2-latest

* Fix @types/hapi

* Upgrade InfraOps plugin

* Fix package.json

* Add back isSameSite: false

* Upgrade beats_management plugin

* Update snapshot

* Fix InfraOps

* Upgrade kql_telemetry

* Merge upstream/master

* Upgrade apm and ml

* Put snapshot test back

* Fx beats

* Upgrade rollups

* Update boom usages in new plugins
This commit is contained in:
Josh Dover 2018-10-25 16:01:12 -05:00 committed by GitHub
parent ab776d4577
commit 27e5406d7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
361 changed files with 3023 additions and 3486 deletions

View file

@ -55,8 +55,8 @@ Append `request.getBasePath()` to any absolute URL path.
const basePath = server.config().get('server.basePath');
server.route({
path: '/redirect',
handler(request, reply) {
reply.redirect(`${request.getBasePath()}/otherLocation`);
handler(request, h) {
return h.redirect(`${request.getBasePath()}/otherLocation`);
}
});
-----------
@ -84,4 +84,4 @@ or `yarn start`.
["source","shell"]
-----------
yarn start --no-base-path
-----------
-----------

View file

@ -68,6 +68,7 @@
"dependencies": {
"@elastic/eui": "4.5.1",
"@elastic/filesaver": "1.1.2",
"@elastic/good": "8.1.1-kibana1",
"@elastic/numeral": "2.3.2",
"@elastic/ui-ace": "0.2.3",
"@kbn/babel-preset": "link:packages/kbn-babel-preset",
@ -92,7 +93,7 @@
"babel-polyfill": "6.20.0",
"babel-register": "6.18.0",
"bluebird": "2.9.34",
"boom": "5.2.0",
"boom": "^7.2.0",
"brace": "0.11.1",
"cache-loader": "1.0.3",
"chalk": "^2.4.1",
@ -108,7 +109,6 @@
"elasticsearch": "^15.1.1",
"elasticsearch-browser": "^15.1.1",
"encode-uri-query": "1.0.0",
"even-better": "7.0.2",
"execa": "^0.10.0",
"expiry-js": "0.1.7",
"extract-text-webpack-plugin": "3.0.1",
@ -118,16 +118,15 @@
"glob": "^7.1.2",
"glob-all": "^3.1.0",
"good-squeeze": "2.1.0",
"h2o2": "5.1.1",
"h2o2-latest": "npm:h2o2@8.1.2",
"h2o2": "^8.1.2",
"handlebars": "4.0.5",
"hapi": "14.2.0",
"hapi-latest": "npm:hapi@17.5.0",
"hapi": "^17.5.3",
"hjson": "3.1.0",
"hoek": "^5.0.4",
"http-proxy-agent": "^2.1.0",
"https-proxy-agent": "^2.2.1",
"inert": "4.0.2",
"joi": "10.4.1",
"inert": "^5.1.0",
"joi": "^13.5.2",
"jquery": "^3.3.1",
"js-yaml": "3.4.1",
"json-stringify-pretty-compact": "1.0.4",
@ -151,6 +150,7 @@
"ngreact": "0.5.1",
"no-ui-slider": "1.2.0",
"node-fetch": "1.3.2",
"oppsy": "^2.0.0",
"pegjs": "0.9.0",
"postcss-loader": "2.0.6",
"prop-types": "15.5.8",
@ -203,11 +203,11 @@
"vega-lite": "^2.4.0",
"vega-schema-url-parser": "1.0.0",
"vega-tooltip": "^0.9.14",
"vision": "4.1.0",
"vision": "^5.3.3",
"webpack": "3.6.0",
"webpack-merge": "4.1.0",
"whatwg-fetch": "^2.0.3",
"wreck": "12.4.0",
"wreck": "^14.0.2",
"x-pack": "link:x-pack",
"yauzl": "2.7.0"
},
@ -238,10 +238,11 @@
"@types/fetch-mock": "^5.12.2",
"@types/getopts": "^2.0.0",
"@types/glob": "^5.0.35",
"@types/hapi-latest": "npm:@types/hapi@17.0.12",
"@types/hapi": "^17.0.18",
"@types/has-ansi": "^3.0.0",
"@types/hoek": "^4.1.3",
"@types/jest": "^23.3.1",
"@types/joi": "^10.4.4",
"@types/joi": "^13.4.2",
"@types/jquery": "^3.3.6",
"@types/js-yaml": "^3.11.1",
"@types/listr": "^0.13.0",

View file

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import * as Joi from 'joi';
import Joi from 'joi';
import {
AnySchema,
JoiRoot,

View file

@ -101,12 +101,7 @@ export abstract class Type<V> {
return error;
}
const { context = {}, type, path: rawPath, message } = error;
// Before v11.0.0 Joi reported paths as `.`-delimited strings, but more
// recent version use arrays instead. Once we upgrade Joi, we should just
// remove this split logic and use `path` provided by Joi directly.
const path = rawPath ? rawPath.split('.') : [];
const { context = {}, type, path, message } = error;
const errorHandleResult = this.handleError(type, context, path);
if (errorHandleResult instanceof SchemaTypeError) {

View file

@ -3,8 +3,8 @@ export default function (server) {
server.route({
path: '/api/<%= name %>/example',
method: 'GET',
handler(req, reply) {
reply({ time: (new Date()).toISOString() });
handler() {
return { time: (new Date()).toISOString() };
}
});

View file

@ -1,76 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Server logging configuration should be reloadable via SIGHUP process signaling 1`] = `
Object {
"code": 0,
"lines": Array [
Object {
"@timestamp": "## @timestamp ##",
"message": "Plugin initialization disabled.",
"pid": "## PID ##",
"tags": Array [
"info",
],
"type": "log",
},
Object {
"@timestamp": "## @timestamp ##",
"message": "The elasticsearch plugin is disabled. Skipping migrations.",
"pid": "## PID ##",
"tags": Array [
"warning",
"migration",
],
"type": "log",
},
Object {
"@timestamp": "## @timestamp ##",
"message": "Server running at http://localhost:8274",
"pid": "## PID ##",
"tags": Array [
"info",
"http",
"server",
"listening",
],
"type": "log",
},
Object {
"@timestamp": "## @timestamp ##",
"message": "Reloading logging configuration due to SIGHUP.",
"pid": "## PID ##",
"tags": Array [
"info",
"config",
],
"type": "log",
},
Object {
"@timestamp": "## @timestamp ##",
"message": "New logging configuration:
{
\\"ops\\": {
\\"interval\\": 5000
},
\\"logging\\": {
\\"json\\": false,
\\"silent\\": false,
\\"quiet\\": false,
\\"verbose\\": false,
\\"events\\": {},
\\"dest\\": \\"stdout\\",
\\"filter\\": {},
\\"useUTC\\": true
}
}",
"pid": "## PID ##",
"tags": Array [
"info",
"config",
],
"type": "log",
},
" log [## timestamp ##] [info][config] Reloaded logging configuration due to SIGHUP.",
],
}
`;

View file

@ -22,8 +22,7 @@ import { writeFileSync } from 'fs';
import { relative, resolve } from 'path';
import { safeDump } from 'js-yaml';
import es from 'event-stream';
import stripAnsi from 'strip-ansi';
import { getConfigFromFiles } from '../../../core/server/config';
import { getConfigFromFiles } from '../../../core/server/config/read_config';
const testConfigFile = follow('__fixtures__/reload_logging_config/kibana.test.yml');
const kibanaPath = follow('../../../../scripts/kibana.js');
@ -42,19 +41,7 @@ function setLoggingJson(enabled) {
writeFileSync(testConfigFile, yaml);
}
const prepareJson = obj => ({
...obj,
pid: '## PID ##',
'@timestamp': '## @timestamp ##'
});
const prepareLogLine = str =>
stripAnsi(str.replace(
/\[\d{2}:\d{2}:\d{2}.\d{3}\]/,
'[## timestamp ##]'
));
describe.skip('Server logging configuration', function () {
describe('Server logging configuration', function () {
let child;
let isJson;
@ -80,7 +67,7 @@ describe.skip('Server logging configuration', function () {
});
} else {
it('should be reloadable via SIGHUP process signaling', function (done) {
expect.assertions(1);
expect.assertions(3);
child = spawn('node', [kibanaPath, '--config', testConfigFile]);
@ -88,12 +75,15 @@ describe.skip('Server logging configuration', function () {
done(new Error(`error in child process while attempting to reload config. ${err.stack || err.message || err}`));
});
const lines = [];
let sawJson = false;
let sawNonjson = false;
child.on('exit', _code => {
const code = _code === null ? 0 : _code;
expect({ code, lines }).toMatchSnapshot();
expect(code).toEqual(0);
expect(sawJson).toEqual(true);
expect(sawNonjson).toEqual(true);
done();
});
@ -107,23 +97,18 @@ describe.skip('Server logging configuration', function () {
if (isJson) {
const data = JSON.parse(line);
lines.push(prepareJson(data));
sawJson = true;
if (data.tags.includes('listening')) {
switchToPlainTextLog();
}
} else if (line.startsWith('{')) {
// We have told Kibana to stop logging json, but it hasn't completed
// the switch yet, so we verify the messages that are logged while
// switching over.
const data = JSON.parse(line);
lines.push(prepareJson(data));
// the switch yet, so we ignore before switching over.
} else {
// Kibana has successfully stopped logging json, so we verify the
// log line and kill the server.
// Kibana has successfully stopped logging json, so kill the server.
lines.push(prepareLogLine(line));
sawNonjson = true;
child.kill();
child = undefined;

View file

@ -19,7 +19,6 @@
import Wreck from 'wreck';
import Progress from '../progress';
import { fromNode as fn } from 'bluebird';
import { createWriteStream } from 'fs';
import HttpProxyAgent from 'http-proxy-agent';
import HttpsProxyAgent from 'https-proxy-agent';
@ -41,32 +40,31 @@ function getProxyAgent(sourceUrl, logger) {
}
}
function sendRequest({ sourceUrl, timeout }, logger) {
async function sendRequest({ sourceUrl, timeout }, logger) {
const maxRedirects = 11; //Because this one goes to 11.
return fn(cb => {
const reqOptions = { timeout, redirects: maxRedirects };
const proxyAgent = getProxyAgent(sourceUrl, logger);
const reqOptions = { timeout, redirects: maxRedirects };
const proxyAgent = getProxyAgent(sourceUrl, logger);
if (proxyAgent) {
reqOptions.agent = proxyAgent;
if (proxyAgent) {
reqOptions.agent = proxyAgent;
}
try {
const promise = Wreck.request('GET', sourceUrl, reqOptions);
const req = promise.req;
const resp = await promise;
if (resp.statusCode >= 400) {
throw new Error('ENOTFOUND');
}
const req = Wreck.request('GET', sourceUrl, reqOptions, (err, resp) => {
if (err) {
if (err.code === 'ECONNREFUSED') {
err = new Error('ENOTFOUND');
}
return { req, resp };
} catch (err) {
if (err.code === 'ECONNREFUSED') {
err = new Error('ENOTFOUND');
}
return cb(err);
}
if (resp.statusCode >= 400) {
return cb(new Error('ENOTFOUND'));
}
cb(null, { req, resp });
});
});
throw err;
}
}
function downloadResponse({ resp, targetPath, progress }) {

View file

@ -10,12 +10,15 @@ Object {
"maxBytes": 1024,
},
"validate": Object {
"failAction": [Function],
"options": Object {
"abortEarly": false,
},
},
},
"state": Object {
"isHttpOnly": true,
"isSameSite": false,
"strictHeader": false,
},
}

View file

@ -18,7 +18,7 @@
*/
import { ByteSizeValue } from '@kbn/config-schema';
import { Server } from 'hapi-latest';
import { Server } from 'hapi';
import { Agent as HttpsAgent, ServerOptions as TlsOptions } from 'https';
import { sample } from 'lodash';
import { DevConfig } from '../dev';
@ -66,7 +66,7 @@ export class BasePathProxyServer {
// Register hapi plugin that adds proxying functionality. It can be configured
// through the route configuration object (see { handler: { proxy: ... } }).
await this.server.register({ plugin: require('h2o2-latest') });
await this.server.register({ plugin: require('h2o2') });
if (this.httpConfig.ssl.enabled) {
const tlsOptions = serverOptions.tls as TlsOptions;

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { Server, ServerOptions } from 'hapi-latest';
import { Server, ServerOptions } from 'hapi';
import { modifyUrl } from '../../utils';
import { Logger } from '../logging';

View file

@ -0,0 +1,59 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Request, ResponseToolkit } from 'hapi';
import Joi from 'joi';
import { defaultValidationErrorHandler, HapiValidationError } from './http_tools';
const emptyOutput = {
statusCode: 400,
headers: {},
payload: {
statusCode: 400,
error: '',
validation: {
source: '',
keys: [],
},
},
};
describe('defaultValidationErrorHandler', () => {
it('formats value validation errors correctly', () => {
expect.assertions(1);
const schema = Joi.array().items(
Joi.object({
type: Joi.string().required(),
}).required()
);
const error = schema.validate([{}], { abortEarly: false }).error as HapiValidationError;
// Emulate what Hapi v17 does by default
error.output = { ...emptyOutput };
error.output.payload.validation.keys = ['0.type', ''];
try {
defaultValidationErrorHandler({} as Request, {} as ResponseToolkit, error);
} catch (err) {
// Verify the empty string gets corrected to 'value'
expect(err.output.payload.validation.keys).toEqual(['0.type', 'value']);
}
});
});

View file

@ -18,8 +18,10 @@
*/
import { readFileSync } from 'fs';
import { Server, ServerOptions } from 'hapi-latest';
import { Lifecycle, Request, ResponseToolkit, Server, ServerOptions, Util } from 'hapi';
import Hoek from 'hoek';
import { ServerOptions as TLSOptions } from 'https';
import { ValidationError } from 'joi';
import { HttpConfig } from './http_config';
/**
@ -39,6 +41,7 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = {
maxBytes: config.maxPayload.getValueInBytes(),
},
validate: {
failAction: defaultValidationErrorHandler,
options: {
abortEarly: false,
},
@ -46,6 +49,8 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = {
},
state: {
strictHeader: false,
isHttpOnly: true,
isSameSite: false, // necessary to allow using Kibana inside an iframe
},
};
@ -88,3 +93,55 @@ export function createServer(options: ServerOptions) {
return server;
}
/**
* Hapi extends the ValidationError interface to add this output key with more data.
*/
export interface HapiValidationError extends ValidationError {
output: {
statusCode: number;
headers: Util.Dictionary<string | string[]>;
payload: {
statusCode: number;
error: string;
message?: string;
validation: {
source: string;
keys: string[];
};
};
};
}
/**
* Used to replicate Hapi v16 and below's validation responses. Should be used in the routes.validate.failAction key.
*/
export function defaultValidationErrorHandler(
request: Request,
h: ResponseToolkit,
err?: Error
): Lifecycle.ReturnValue {
// Newer versions of Joi don't format the key for missing params the same way. This shim
// provides backwards compatibility. Unfortunately, Joi doesn't export it's own Error class
// in JS so we have to rely on the `name` key before we can cast it.
//
// The Hapi code we're 'overwriting' can be found here:
// https://github.com/hapijs/hapi/blob/master/lib/validation.js#L102
if (err && err.name === 'ValidationError' && err.hasOwnProperty('output')) {
const validationError: HapiValidationError = err as HapiValidationError;
const validationKeys: string[] = [];
validationError.details.forEach(detail => {
if (detail.path.length > 0) {
validationKeys.push(Hoek.escapeHtml(detail.path.join('.')));
} else {
// If no path, use the value sigil to signal the entire value had an issue.
validationKeys.push('value');
}
});
validationError.output.payload.validation.keys = validationKeys;
}
throw err;
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { Request, ResponseToolkit, Server } from 'hapi-latest';
import { Request, ResponseToolkit, Server } from 'hapi';
import { format as formatUrl } from 'url';
import { Logger } from '../logging';

View file

@ -18,7 +18,7 @@
*/
import { ObjectType, TypeOf } from '@kbn/config-schema';
import { Request } from 'hapi-latest';
import { Request } from 'hapi';
import { filterHeaders, Headers } from './headers';
import { RouteSchemas } from './route';

View file

@ -18,7 +18,7 @@
*/
import { ObjectType, schema, TypeOf } from '@kbn/config-schema';
import { Request, ResponseObject, ResponseToolkit } from 'hapi-latest';
import { Request, ResponseObject, ResponseToolkit } from 'hapi';
import { KibanaRequest } from './request';
import { KibanaResponse, ResponseFactory, responseFactory } from './response';

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { Server as HapiServer } from 'hapi-latest';
import { Server as HapiServer } from 'hapi';
import { combineLatest, ConnectableObservable, EMPTY, Subscription } from 'rxjs';
import { first, map, mergeMap, publishReplay, tap } from 'rxjs/operators';
import { CoreService } from '../../types/core_service';

View file

@ -26,7 +26,7 @@ import { LegacyLoggingServer } from './legacy_logging_server';
test('correctly forwards log records.', () => {
const loggingServer = new LegacyLoggingServer({ events: {} });
const onLogMock = jest.fn();
loggingServer.on('log', onLogMock);
loggingServer.events.on('log', onLogMock);
const timestamp = 1554433221100;
const firstLogRecord = {

View file

@ -17,7 +17,8 @@
* under the License.
*/
import { EventEmitter } from 'events';
import { ServerExtType } from 'hapi';
import Podium from 'podium';
// @ts-ignore: implicit any for JS file
import { Config, transformDeprecations } from '../../../../server/config';
// @ts-ignore: implicit any for JS file
@ -25,12 +26,11 @@ import { setupLogging } from '../../../../server/logging';
import { LogRecord } from '../../logging/log_record';
interface PluginRegisterParams {
register: {
plugin: {
register: (
server: LegacyLoggingServer,
options: PluginRegisterParams['options'],
cb: () => void
) => void;
options: PluginRegisterParams['options']
) => Promise<void>;
};
options: Record<string, any>;
}
@ -41,12 +41,14 @@ interface PluginRegisterParams {
* same as the rest of the records generated by the "legacy" Kibana. But to reduce
* overhead of having full blown Hapi server instance we create our own "light" version.
*/
export class LegacyLoggingServer extends EventEmitter {
export class LegacyLoggingServer {
public connections = [];
// Emulates Hapi's usage of the podium event bus.
public events: Podium = new Podium(['log', 'request', 'response']);
private onPostStopCallback?: () => void;
constructor(legacyLoggingConfig: Readonly<Record<string, any>>) {
super();
// We set `ops.interval` to max allowed number and `ops` filter to value
// that doesn't exist to avoid logging of ops at all, if turned on it will be
// logged by the "legacy" Kibana.
@ -64,12 +66,12 @@ export class LegacyLoggingServer extends EventEmitter {
setupLogging(this, Config.withDefaultSchema(transformDeprecations(config)));
}
public register({ register: plugin, options }: PluginRegisterParams, cb: () => void) {
plugin.register(this, options, cb);
public register({ plugin: { register }, options }: PluginRegisterParams): Promise<void> {
return register(this, options);
}
public log({ level, context, message, error, timestamp, meta = {} }: LogRecord) {
this.emit('log', {
this.events.emit('log', {
data: error || message,
tags: [level.id.toLowerCase(), ...context.split('.'), ...(meta.tags || [])],
timestamp: timestamp.getTime(),
@ -77,7 +79,18 @@ export class LegacyLoggingServer extends EventEmitter {
}
public stop() {
this.emit('stop');
// Tell the plugin we're stopping.
if (this.onPostStopCallback !== undefined) {
this.onPostStopCallback();
}
}
public ext(eventName: ServerExtType, callback: () => void) {
// method is called by plugin that's being registered.
if (eventName === 'onPostStop') {
this.onPostStopCallback = callback;
}
// We don't care about any others the plugin resgisters
}
public expose() {

View file

@ -21,7 +21,7 @@ import _ from 'lodash';
const KNOWN_APIS = ['es_6_0'];
export function resolveApi(senseVersion, apis, reply) {
export function resolveApi(senseVersion, apis, h) {
const result = {};
_.each(apis, function (name) {
{
@ -33,5 +33,5 @@ export function resolveApi(senseVersion, apis, reply) {
}
});
return reply(result).type('application/json');
return h.response(result).type('application/json');
}

View file

@ -21,7 +21,7 @@ import { resolveApi } from './server';
describe('resolveApi', () => {
it('allows known APIs to be resolved', () => {
const mockReply = jest.fn((result) => ({ type: () => result }));
const result = resolveApi('Sense Version', ['es_6_0'], mockReply);
const result = resolveApi('Sense Version', ['es_6_0'], { response: mockReply });
expect(result).toMatchObject({
es_6_0: {
endpoints: expect.any(Object),
@ -33,13 +33,13 @@ describe('resolveApi', () => {
it('does not resolve APIs that are not known', () => {
const mockReply = jest.fn((result) => ({ type: () => result }));
const result = resolveApi('Sense Version', ['unknown'], mockReply);
const result = resolveApi('Sense Version', ['unknown'], { response: mockReply });
expect(result).toEqual({});
});
it('handles request for apis that are known and unknown', () => {
const mockReply = jest.fn((result) => ({ type: () => result }));
const result = resolveApi('Sense Version', ['es_6_0'], mockReply);
const result = resolveApi('Sense Version', ['es_6_0'], { response: mockReply });
expect(result).toMatchObject({
es_6_0: {
endpoints: expect.any(Object),
@ -48,4 +48,4 @@ describe('resolveApi', () => {
}
});
});
});
});

View file

@ -113,14 +113,13 @@ export default function (kibana) {
server.route({
path: '/api/console/api_server',
method: ['GET', 'POST'],
handler: function (req, reply) {
handler: function (req, h) {
const { sense_version: version, apis } = req.query;
if (!apis) {
reply(Boom.badRequest('"apis" is a required param.'));
return;
throw Boom.badRequest('"apis" is a required param.');
}
return resolveApi(version, apis.split(','), reply);
return resolveApi(version, apis.split(','), h);
}
});
},

View file

@ -36,8 +36,6 @@ describe('Console Proxy Route', () => {
sandbox.stub(Wreck, 'request').callsFake(createWreckResponseStub(response));
const server = new Server();
server.connection({ port: 0 });
server.route(createProxyRoute({
baseUrl: 'http://localhost:9200'
}));

View file

@ -38,8 +38,6 @@ describe('Console Proxy Route', () => {
setup = () => {
const server = new Server();
server.connection({ port: 0 });
server.route(createProxyRoute({
baseUrl: 'http://localhost:9200'
}));

View file

@ -38,7 +38,6 @@ describe('Console Proxy Route', () => {
setup = () => {
const server = new Server();
server.connection({ port: 0 });
teardowns.push(() => server.stop());
return { server };
};

View file

@ -36,8 +36,6 @@ describe('Console Proxy Route', () => {
request = async (method, path) => {
const server = new Server();
server.connection({ port: 0 });
server.route(createProxyRoute({
baseUrl: 'http://localhost:9200'
}));

View file

@ -20,7 +20,7 @@
import { Readable } from 'stream';
export function createWreckResponseStub(response) {
return (...args) => {
return async () => {
const resp = new Readable({
read() {
if (response) {
@ -37,6 +37,6 @@ export function createWreckResponseStub(response) {
'content-length': String(response ? response.length : 0)
};
args.pop()(null, resp);
return resp;
};
}

View file

@ -46,7 +46,7 @@ function getProxyHeaders(req) {
// see https://git.io/vytQ7
extendCommaList(headers, 'x-forwarded-for', req.info.remoteAddress);
extendCommaList(headers, 'x-forwarded-port', req.info.remotePort);
extendCommaList(headers, 'x-forwarded-proto', req.connection.info.protocol);
extendCommaList(headers, 'x-forwarded-proto', req.server.info.protocol);
extendCommaList(headers, 'x-forwarded-host', req.info.host);
}
@ -82,21 +82,21 @@ export const createProxyRoute = ({
},
pre: [
function filterPath(req, reply) {
function filterPath(req) {
const { path } = req.query;
if (!pathFilters.some(re => re.test(path))) {
const err = Boom.forbidden();
err.output.payload = `Error connecting to '${path}':\n\nUnable to send requests to that path.`;
err.output.headers['content-type'] = 'text/plain';
reply(err);
} else {
reply();
if (pathFilters.some(re => re.test(path))) {
return null;
}
const err = Boom.forbidden();
err.output.payload = `Error connecting to '${path}':\n\nUnable to send requests to that path.`;
err.output.headers['content-type'] = 'text/plain';
throw err;
},
],
handler(req, reply) {
handler: async (req, h) => {
const { payload, query } = req;
const { path, method } = query;
const uri = resolveUri(baseUrl, path);
@ -119,23 +119,18 @@ export const createProxyRoute = ({
},
};
Wreck.request(method, uri, wreckOptions, (err, esResponse) => {
if (err) {
return reply(err);
}
const esResponse = await Wreck.request(method, uri, wreckOptions);
if (method.toUpperCase() !== 'HEAD') {
reply(esResponse)
.code(esResponse.statusCode)
.header('warning', esResponse.headers.warning);
return;
}
reply(`${esResponse.statusCode} - ${esResponse.statusMessage}`)
if (method.toUpperCase() !== 'HEAD') {
return h.response(esResponse)
.code(esResponse.statusCode)
.type('text/plain')
.header('warning', esResponse.headers.warning);
});
}
return h.response(`${esResponse.statusCode} - ${esResponse.statusMessage}`)
.code(esResponse.statusCode)
.type('text/plain')
.header('warning', esResponse.headers.warning);
}
}
});

View file

@ -35,34 +35,32 @@ export default function (kibana) {
return new kibana.Plugin({
require: ['kibana'],
config(Joi) {
const { array, boolean, number, object, string, ref } = Joi;
const sslSchema = object({
verificationMode: string().valid('none', 'certificate', 'full').default('full'),
certificateAuthorities: array().single().items(string()),
certificate: string(),
key: string(),
keyPassphrase: string(),
alwaysPresentCertificate: boolean().default(false),
const sslSchema = Joi.object({
verificationMode: Joi.string().valid('none', 'certificate', 'full').default('full'),
certificateAuthorities: Joi.array().single().items(Joi.string()),
certificate: Joi.string(),
key: Joi.string(),
keyPassphrase: Joi.string(),
alwaysPresentCertificate: Joi.boolean().default(false),
}).default();
return object({
enabled: boolean().default(true),
url: string().uri({ scheme: ['http', 'https'] }).default('http://localhost:9200'),
preserveHost: boolean().default(true),
username: string(),
password: string(),
shardTimeout: number().default(30000),
requestTimeout: number().default(30000),
requestHeadersWhitelist: array().items().single().default(DEFAULT_REQUEST_HEADERS),
customHeaders: object().default({}),
pingTimeout: number().default(ref('requestTimeout')),
startupTimeout: number().default(5000),
logQueries: boolean().default(false),
return Joi.object({
enabled: Joi.boolean().default(true),
url: Joi.string().uri({ scheme: ['http', 'https'] }).default('http://localhost:9200'),
preserveHost: Joi.boolean().default(true),
username: Joi.string(),
password: Joi.string(),
shardTimeout: Joi.number().default(30000),
requestTimeout: Joi.number().default(30000),
requestHeadersWhitelist: Joi.array().items().single().default(DEFAULT_REQUEST_HEADERS),
customHeaders: Joi.object().default({}),
pingTimeout: Joi.number().default(Joi.ref('requestTimeout')),
startupTimeout: Joi.number().default(5000),
logQueries: Joi.boolean().default(false),
ssl: sslSchema,
apiVersion: Joi.string().default('master'),
healthCheck: object({
delay: number().default(2500)
healthCheck: Joi.object({
delay: Joi.number().default(2500)
}).default(),
}).default();
},

View file

@ -36,7 +36,9 @@ describe('plugins/elasticsearch', function () {
elasticsearch: {}
},
expose: sinon.mock(),
on: sinon.stub(),
events: {
on: sinon.stub(),
}
};
clusters = createClusters(server);
@ -86,7 +88,6 @@ describe('plugins/elasticsearch', function () {
it('closes all clusters', async () => {
const server = new Hapi.Server();
server.connection({ port: 0 });
const clusters = createClusters(server);
const cluster = clusters.create('name', { config: true });
expect(cluster).to.have.property('stub', true);

View file

@ -114,13 +114,10 @@ describe('plugins/elasticsearch', () => {
sinon.assert.calledOnce(server.ext);
sinon.assert.calledWithExactly(server.ext, sinon.match.string, sinon.match.func);
// call the server extension
const reply = sinon.stub();
const [, handler] = server.ext.firstCall.args;
handler({}, reply);
handler(); // this should be health.stop
// ensure that the handler called reply and unregistered the time
sinon.assert.calledOnce(reply);
// ensure that the handler unregistered the timer
expect(getTimerCount()).to.be(0);
});

View file

@ -22,7 +22,7 @@ import { Cluster } from './cluster';
export function createClusters(server) {
const clusters = new Map();
server.on('stop', () => {
server.events.on('stop', () => {
for (const [name, cluster] of clusters) {
cluster.close();
clusters.delete(name);

View file

@ -33,10 +33,9 @@ export function createProxy(server, method, path, config) {
['/elasticsearch', server.plugins.elasticsearch.getCluster('data')],
]);
const responseHandler = function (err, upstreamResponse, request, reply) {
const responseHandler = function (err, upstreamResponse) {
if (err) {
reply(err);
return;
throw err;
}
if (upstreamResponse.headers.location) {
@ -44,7 +43,7 @@ export function createProxy(server, method, path, config) {
upstreamResponse.headers.location = encodeURI(upstreamResponse.headers.location);
}
reply(null, upstreamResponse);
return upstreamResponse;
};
for (const [proxyPrefix, cluster] of proxies) {

View file

@ -95,10 +95,7 @@ export default function (plugin, server) {
return true;
}
server.ext('onPreStop', (request, reply) => {
stopChecking();
reply();
});
server.ext('onPreStop', stopChecking);
return {
waitUntilReady: waitUntilReady,
@ -107,5 +104,4 @@ export default function (plugin, server) {
stop: stopChecking,
isRunning: function () { return !!timeoutId; },
};
}

View file

@ -27,7 +27,7 @@ export default function mapUri(cluster, proxyPrefix) {
return trimRight(pathA, '/') + '/' + trimLeft(pathB, '/');
}
return function (request, done) {
return function (request) {
const {
protocol: esUrlProtocol,
slashes: esUrlHasSlashes,
@ -60,6 +60,6 @@ export default function mapUri(cluster, proxyPrefix) {
const filteredHeaders = filterHeaders(request.headers, cluster.getRequestHeadersWhitelist());
const mappedHeaders = setHeaders(filteredHeaders, cluster.getCustomHeaders());
const mappedUrl = formatUrl(mappedUrlComponents);
done(null, mappedUrl, mappedHeaders);
return { uri: mappedUrl, headers: mappedHeaders };
};
}

View file

@ -36,18 +36,18 @@ export function exportApi(server) {
tags: ['api'],
},
method: ['GET'],
handler: (req, reply) => {
handler: async (req, h) => {
const currentDate = moment.utc();
return exportDashboards(req)
.then(resp => {
const json = JSON.stringify(resp, null, ' ');
const filename = `kibana-dashboards.${currentDate.format('YYYY-MM-DD-HH-mm-ss')}.json`;
reply(json)
return h.response(json)
.header('Content-Disposition', `attachment; filename="${filename}"`)
.header('Content-Type', 'application/json')
.header('Content-Length', Buffer.byteLength(json, 'utf8'));
})
.catch(err => reply(Boom.boomify(err, { statusCode: 400 })));
.catch(err => Boom.boomify(err, { statusCode: 400 }));
}
});
}

View file

@ -22,8 +22,8 @@ export function registerTutorials(server) {
server.route({
path: '/api/kibana/home/tutorials',
method: ['GET'],
handler: async function (req, reply) {
reply(server.getTutorials(req));
handler: function (req) {
return server.getTutorials(req);
}
});
}

View file

@ -39,10 +39,12 @@ export function importApi(server) {
tags: ['api'],
},
handler: (req, reply) => {
return importDashboards(req)
.then((resp) => reply(resp))
.catch(err => reply(Boom.boomify(err, { statusCode: 400 })));
handler: async (req) => {
try {
return await importDashboards(req);
} catch (err) {
throw Boom.boomify(err, { statusCode: 400 });
}
}
});
}

View file

@ -32,7 +32,7 @@ export function registerKqlTelemetryApi(server) {
},
tags: ['api'],
},
handler: async function (request, reply) {
handler: async function (request) {
const { savedObjects: { getSavedObjectsRepository } } = server;
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
@ -51,10 +51,10 @@ export function registerKqlTelemetryApi(server) {
);
}
catch (error) {
reply(new Boom('Something went wrong', { statusCode: error.status, data: { success: false } }));
return new Boom('Something went wrong', { statusCode: error.status, data: { success: false } });
}
reply({ success: true });
return { success: true };
},
});
}

View file

@ -38,20 +38,19 @@ export function registerRelationships(server) {
},
},
handler: async (req, reply) => {
handler: async (req) => {
const type = req.params.type;
const id = req.params.id;
const size = req.query.size || 10;
try {
const response = await findRelationships(type, id, size, req.getSavedObjectsClient());
reply(response);
return await findRelationships(type, id, size, req.getSavedObjectsClient());
} catch (err) {
if (isNotFoundError(err)) {
reply(Boom.boomify(new Error('Resource not found'), { statusCode: 404 }));
return;
throw Boom.boomify(new Error('Resource not found'), { statusCode: 404 });
}
reply(Boom.boomify(err, { statusCode: 500 }));
throw Boom.boomify(err, { statusCode: 500 });
}
},
});

View file

@ -45,13 +45,14 @@ export function registerScrollForExportRoute(server) {
},
},
handler: async (req, reply) => {
handler: async (req) => {
const savedObjectsClient = req.getSavedObjectsClient();
const objects = await findAll(savedObjectsClient, {
perPage: 1000,
type: req.payload.typesToInclude
});
const response = objects.map(hit => {
return objects.map(hit => {
const type = hit.type;
return {
_id: hit.id,
@ -63,8 +64,6 @@ export function registerScrollForExportRoute(server) {
_migrationVersion: hit.migrationVersion,
};
});
reply(response);
}
});
}
@ -82,7 +81,7 @@ export function registerScrollForCountRoute(server) {
},
},
handler: async (req, reply) => {
handler: async (req) => {
const savedObjectsClient = req.getSavedObjectsClient();
const findOptions = {
type: req.payload.typesToInclude,
@ -108,7 +107,7 @@ export function registerScrollForCountRoute(server) {
}
}
reply(counts);
return counts;
}
});
}

View file

@ -20,8 +20,15 @@ import Hapi from 'hapi';
import { registerScrollForExportRoute } from './scroll';
const createMockServer = () => {
const mockServer = new Hapi.Server({ debug: false });
mockServer.connection({ port: 8080 });
const mockServer = new Hapi.Server({
debug: false,
port: 8080,
routes: {
validate: {
failAction: (r, h, err) => { throw err; }
}
}
});
return mockServer;
};

View file

@ -21,8 +21,8 @@ export function registerLanguages(server) {
server.route({
path: '/api/kibana/scripts/languages',
method: 'GET',
handler: function (request, reply) {
reply(['painless', 'expression']);
handler: function () {
return ['painless', 'expression'];
}
});
}

View file

@ -23,7 +23,7 @@ export function scrollSearchApi(server) {
server.route({
path: '/api/kibana/legacy_scroll_start',
method: ['POST'],
handler: (req, reply) => {
handler: async (req) => {
const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin');
const { index, size, body } = req.payload;
const params = {
@ -33,21 +33,26 @@ export function scrollSearchApi(server) {
scroll: '1m',
sort: '_doc',
};
return callWithRequest(req, 'search', params)
.then(reply)
.catch(error => reply(handleESError(error)));
try {
return await callWithRequest(req, 'search', params);
} catch (err) {
throw handleESError(err);
}
}
});
server.route({
path: '/api/kibana/legacy_scroll_continue',
method: ['POST'],
handler: (req, reply) => {
handler: async (req) => {
const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin');
const { scrollId } = req.payload;
return callWithRequest(req, 'scroll', { scrollId, scroll: '1m' })
.then(reply)
.catch(error => reply(handleESError(error)));
try {
return await callWithRequest(req, 'scroll', { scrollId, scroll: '1m' });
} catch (err) {
throw handleESError(err);
}
}
});
}

View file

@ -24,22 +24,20 @@ export default function registerCount(server) {
server.route({
path: '/api/kibana/{id}/_count',
method: ['POST', 'GET'],
handler: function (req, reply) {
handler: async function (req) {
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
const boundCallWithRequest = _.partial(callWithRequest, req);
boundCallWithRequest('count', {
allowNoIndices: false,
index: req.params.id
})
.then(
function (res) {
reply({ count: res.count });
},
function (error) {
reply(handleESError(error));
}
);
try {
const res = await boundCallWithRequest('count', {
allowNoIndices: false,
index: req.params.id
});
return { count: res.count };
} catch (err) {
throw handleESError(err);
}
}
});
}

View file

@ -24,7 +24,7 @@ export function registerValueSuggestions(server) {
server.route({
path: '/api/kibana/suggestions/values/{index}',
method: ['POST'],
handler: async function (req, reply) {
handler: async function (req) {
const { index } = req.params;
const { field, query, boolFilter } = req.payload;
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
@ -33,9 +33,9 @@ export function registerValueSuggestions(server) {
const response = await callWithRequest(req, 'search', { index, body });
const buckets = get(response, 'aggregations.suggestions.buckets') || [];
const suggestions = map(buckets, 'key');
reply(suggestions);
return suggestions;
} catch (error) {
reply(handleESError(error));
throw handleESError(error);
}
}
});

View file

@ -20,11 +20,11 @@
import { IndexPatternsService } from '../../../../server/index_patterns/service';
export const getIndexPatternService = {
assign: 'indexPatternsService',
method(req, reply) {
method(req) {
const dataCluster = req.server.plugins.elasticsearch.getCluster('data');
const callDataCluster = (...args) => {
return dataCluster.callWithRequest(req, ...args);
};
reply(new IndexPatternsService(callDataCluster));
return new IndexPatternsService(callDataCluster);
}
};

View file

@ -27,15 +27,17 @@ export default (server) => {
},
path: '/api/metrics/fields',
method: 'GET',
handler: (req, reply) => {
getFields(req)
.then(reply)
.catch((err) => {
if (err.isBoom && err.status === 401) return reply(err);
reply([]);
});
handler: async (req) => {
try {
return await getFields(req);
} catch (err) {
if (err.isBoom && err.status === 401) {
return err;
}
return [];
}
}
});
};

View file

@ -24,13 +24,16 @@ export default (server) => {
server.route({
path: '/api/metrics/vis/data',
method: 'POST',
handler: (req, reply) => {
getVisData(req)
.then(reply)
.catch(err => {
if (err.isBoom && err.status === 401) return reply(err);
reply(Boom.boomify(err, { statusCode: 500 }));
});
handler: async (req) => {
try {
return await getVisData(req);
} catch(err) {
if (err.isBoom && err.status === 401) {
return err;
}
throw Boom.boomify(err, { statusCode: 500 });
}
}
});

View file

@ -23,12 +23,13 @@ export default function (server) {
server.route({
method: 'GET',
path: '/api/timelion/functions',
handler: function (request, reply) {
handler: () => {
const functionArray = _.map(server.plugins.timelion.functions, function (val, key) {
// TODO: This won't work on frozen objects, it should be removed when everything is converted to datasources and chainables
return _.extend({}, val, { name: key });
});
reply(_.sortBy(functionArray, 'name'));
return _.sortBy(functionArray, 'name');
}
});
}

View file

@ -22,8 +22,8 @@ import _ from 'lodash';
import chainRunnerFn from '../handlers/chain_runner.js';
const timelionDefaults = require('../lib/get_namespaced_settings')();
function replyWithError(e, reply) {
reply({
function formatErrorResponse(e, h) {
return h.response({
title: e.toString(),
message: e.toString()
}).code(500);
@ -34,7 +34,7 @@ export default function (server) {
server.route({
method: ['POST', 'GET'],
path: '/api/timelion/run',
handler: async (request, reply) => {
handler: async (request, h) => {
try {
const uiSettings = await request.getUiSettingsService().getAll();
@ -55,18 +55,18 @@ export default function (server) {
}
}));
reply({
return {
sheet,
stats: chainRunner.getStats()
});
};
} catch (err) {
server.log(['timelion', 'error'], `${err.toString()}: ${err.stack}`);
// TODO Maybe we should just replace everywhere we throw with Boom? Probably.
if (err.isBoom) {
reply(err);
return err;
} else {
replyWithError(err, reply);
return formatErrorResponse(err, h);
}
}
}

View file

@ -23,7 +23,7 @@ export default function (server) {
server.route({
method: 'GET',
path: '/api/timelion/validate/es',
handler: async function (request, reply) {
handler: async function (request) {
const uiSettings = await request.getUiSettingsService().getAll();
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
@ -57,19 +57,18 @@ export default function (server) {
}
if (_.has(resp, 'aggregations.maxAgg.value') && _.has(resp, 'aggregations.minAgg.value')) {
reply({
return {
ok: true,
field: timefield,
min: _.get(resp, 'aggregations.minAgg.value'),
max: _.get(resp, 'aggregations.maxAgg.value')
});
return;
};
}
reply({
return {
ok: false,
resp: resp
});
};
}
});
}

View file

@ -52,11 +52,7 @@ export async function download(options) {
try {
log.debug(`Attempting download of ${url}`, chalk.dim(sha256));
const request = wreck.request('GET', url);
const response = await Promise.race([
new Promise(resolve => request.once('response', resolve)),
new Promise((resolve, reject) => request.once('error', reject)),
]);
const response = await wreck.request('GET', url);
if (response.statusCode !== 200) {
throw new Error(

View file

@ -427,8 +427,7 @@ export default class BaseOptimizer {
Stats.presetToOptions('minimal')
));
return Boom.create(
500,
throw Boom.internal(
`Optimizations failure.\n${details.split('\n').join('\n ')}\n`,
stats.toJson(Stats.presetToOptions('detailed'))
);

View file

@ -47,7 +47,6 @@ describe('optimizer/bundle route', () => {
} = options;
const server = new Hapi.Server();
server.connection({ port: 0 });
server.register([Inert]);
server.route(createBundlesRoute({
@ -217,18 +216,18 @@ describe('optimizer/bundle route', () => {
});
describe('js file outside bundlesPath', () => {
it('responds with a 403', async () => {
it('responds with a 404', async () => {
const server = createServer();
const response = await server.inject({
url: '/bundles/../outside_output.js'
});
expect(response.statusCode).to.be(403);
expect(response.statusCode).to.be(404);
expect(response.result).to.eql({
error: 'Forbidden',
message: 'Forbidden',
statusCode: 403
error: 'Not Found',
message: 'Not Found',
statusCode: 404
});
});
});

View file

@ -58,18 +58,19 @@ export function createBundlesRoute({ bundlesPath, basePublicPath }) {
auth: false,
ext: {
onPreHandler: {
method(request, reply) {
method(request, h) {
const ext = extname(request.params.path);
if (ext !== '.js' && ext !== '.css') {
return reply.continue();
return h.continue;
}
reply(createDynamicAssetResponse({
return createDynamicAssetResponse({
request,
h,
bundlesPath,
fileHashCache,
publicPath: `${basePublicPath}/bundles/`
}));
});
}
}
},

View file

@ -54,6 +54,7 @@ import { replacePlaceholder } from '../public_path_placeholder';
export async function createDynamicAssetResponse(options) {
const {
request,
h,
bundlesPath,
publicPath,
fileHashCache,
@ -65,7 +66,7 @@ export async function createDynamicAssetResponse(options) {
// prevent path traversal, only process paths that resolve within bundlesPath
if (!path.startsWith(bundlesPath)) {
return Boom.forbidden(null, 'EACCES');
throw Boom.forbidden(null, 'EACCES');
}
// we use and manage a file descriptor mostly because
@ -83,13 +84,12 @@ export async function createDynamicAssetResponse(options) {
});
fd = null; // read stream is now responsible for fd
const response = request.generateResponse(replacePlaceholder(read, publicPath));
response.code(200);
response.etag(`${hash}-${publicPath}`);
response.header('cache-control', 'must-revalidate');
response.type(request.server.mime.path(path).type);
return response;
return h.response(replacePlaceholder(read, publicPath))
.takeover()
.code(200)
.etag(`${hash}-${publicPath}`)
.header('cache-control', 'must-revalidate')
.type(request.server.mime.path(path).type);
} catch (error) {
if (fd) {
try {
@ -101,9 +101,9 @@ export async function createDynamicAssetResponse(options) {
}
if (error.code === 'ENOENT') {
return Boom.notFound();
throw Boom.notFound();
}
return Boom.boomify(error);
throw Boom.boomify(error);
}
}

View file

@ -63,10 +63,9 @@ export default class WatchOptimizer extends BaseOptimizer {
// pause all requests received while the compiler is running
// and continue once an outcome is reached (aborting the request
// with an error if it was a failure).
server.ext('onRequest', (request, reply) => {
this.onceBuildOutcome()
.then(() => reply.continue())
.catch(reply);
server.ext('onRequest', async (request, h) => {
await this.onceBuildOutcome();
return h.continue;
});
server.route(createBundlesRoute({

View file

@ -19,26 +19,23 @@
import { Server } from 'hapi';
import { fromNode } from 'bluebird';
import { registerHapiPlugins } from '../../server/http/register_hapi_plugins';
export default class WatchServer {
constructor(host, port, basePath, optimizer) {
this.basePath = basePath;
this.optimizer = optimizer;
this.server = new Server();
registerHapiPlugins(this.server);
this.server.connection({
this.server = new Server({
host: host,
port: port
});
registerHapiPlugins(this.server);
}
async init() {
await this.optimizer.init();
this.optimizer.bindToServer(this.server, this.basePath);
await fromNode(cb => this.server.start(cb));
await this.server.start();
}
}

View file

@ -31,7 +31,7 @@ describe('config/deprecation warnings mixin', function () {
let stdio = '';
let proc = null;
before(() => new Promise((resolve, reject) => {
before(async () => {
proc = spawn(process.execPath, [
'-r', SETUP_NODE_ENV,
RUN_KBN_SERVER_STARTUP
@ -50,23 +50,35 @@ describe('config/deprecation warnings mixin', function () {
}
});
proc.stdout.on('data', (chunk) => {
stdio += chunk.toString('utf8');
});
// Either time out in 10 seconds, or resolve once the line is in our buffer
return Promise.race([
new Promise((resolve) => setTimeout(resolve, 10000)),
new Promise((resolve, reject) => {
proc.stdout.on('data', (chunk) => {
stdio += chunk.toString('utf8');
if (chunk.toString('utf8').includes('deprecation')) {
resolve();
}
});
proc.stderr.on('data', (chunk) => {
stdio += chunk.toString('utf8');
});
proc.stderr.on('data', (chunk) => {
stdio += chunk.toString('utf8');
if (chunk.toString('utf8').includes('deprecation')) {
resolve();
}
});
proc.on('exit', (code) => {
proc = null;
if (code > 0) {
reject(new Error(`Kibana server exited with ${code} -- stdout:\n\n${stdio}\n`));
} else {
resolve();
}
});
}));
proc.on('exit', (code) => {
proc = null;
if (code > 0) {
reject(new Error(`Kibana server exited with ${code} -- stdout:\n\n${stdio}\n`));
} else {
resolve();
}
});
})
]);
});
after(() => {
if (proc) {

View file

@ -51,20 +51,20 @@ describe('Config schema', function () {
it('rejects strings with trailing slashes', function () {
const { error } = validate({ server: { basePath: '/path/' } });
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', 'server.basePath');
expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']);
});
it('rejects strings without leading slashes', function () {
const { error } = validate({ server: { basePath: 'path' } });
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', 'server.basePath');
expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']);
});
it('rejects things that are not strings', function () {
for (const value of [1, true, {}, [], /foo/]) {
const { error } = validate({ server: { basePath: value } });
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', 'server.basePath');
expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']);
}
});
});
@ -91,13 +91,13 @@ describe('Config schema', function () {
it('rejects true if basePath not set', function () {
const { error } = validate({ server: { rewriteBasePath: true } });
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', 'server.rewriteBasePath');
expect(error.details[0]).toHaveProperty('path', ['server', 'rewriteBasePath']);
});
it('rejects strings', function () {
const { error } = validate({ server: { rewriteBasePath: 'foo' } });
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', 'server.rewriteBasePath');
expect(error.details[0]).toHaveProperty('path', ['server', 'rewriteBasePath']);
});
});
@ -110,7 +110,7 @@ describe('Config schema', function () {
const { error } = validate(config);
expect(error).toBeInstanceOf(Object);
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', 'server.ssl.enabled');
expect(error.details[0]).toHaveProperty('path', ['server', 'ssl', 'enabled']);
});
it('can be true', function () {
@ -146,7 +146,7 @@ describe('Config schema', function () {
const { error } = validate(config);
expect(error).toBeInstanceOf(Object);
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', 'server.ssl.certificate');
expect(error.details[0]).toHaveProperty('path', ['server', 'ssl', 'certificate']);
});
});
@ -165,7 +165,7 @@ describe('Config schema', function () {
const { error } = validate(config);
expect(error).toBeInstanceOf(Object);
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', 'server.ssl.key');
expect(error.details[0]).toHaveProperty('path', ['server', 'ssl', 'key']);
});
});
@ -202,7 +202,7 @@ describe('Config schema', function () {
const { error } = validate(config);
expect(error).toBeInstanceOf(Object);
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', 'server.ssl.supportedProtocols.0');
expect(error.details[0]).toHaveProperty('path', ['server', 'ssl', 'supportedProtocols', 0]);
});
it('rejects SSLv3', function () {
@ -211,7 +211,7 @@ describe('Config schema', function () {
const { error } = validate(config);
expect(error).toBeInstanceOf(Object);
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', 'server.ssl.supportedProtocols.0');
expect(error.details[0]).toHaveProperty('path', ['server', 'ssl', 'supportedProtocols', 0]);
});
it('accepts TLSv1, TLSv1.1, TLSv1.2', function () {
@ -243,7 +243,7 @@ describe('Config schema', function () {
const { error } = validate(config);
expect(error).toBeInstanceOf(Object);
expect(error).toHaveProperty('details');
expect(error.details[0]).toHaveProperty('path', 'server.xsrf.whitelist.0');
expect(error.details[0]).toHaveProperty('path', ['server', 'xsrf', 'whitelist', 0]);
});
it('whitelist accepts paths that start with a slash.', () => {

View file

@ -28,14 +28,12 @@ import { setupBasePathProvider } from './setup_base_path_provider';
import { setupXsrf } from './xsrf';
export default async function (kbnServer, server, config) {
kbnServer.server = new Hapi.Server();
kbnServer.server = new Hapi.Server(kbnServer.core.serverOptions);
server = kbnServer.server;
server.connection(kbnServer.core.serverOptions);
setupBasePathProvider(server, config);
registerHapiPlugins(server);
await registerHapiPlugins(server);
// provide a simple way to expose static directories
server.decorate('server', 'exposeStaticDir', function (routePath, dirPath) {
@ -63,7 +61,7 @@ export default async function (kbnServer, server, config) {
});
// attach the app name to the server, so we can be sure we are actually talking to kibana
server.ext('onPreResponse', function (req, reply) {
server.ext('onPreResponse', function onPreResponse(req, h) {
const response = req.response;
const customHeaders = {
@ -82,32 +80,34 @@ export default async function (kbnServer, server, config) {
});
}
return reply.continue();
return h.continue;
});
server.route({
path: '/',
method: 'GET',
handler(req, reply) {
handler(req, h) {
const basePath = req.getBasePath();
const defaultRoute = config.get('server.defaultRoute');
reply.redirect(`${basePath}${defaultRoute}`);
return h.redirect(`${basePath}${defaultRoute}`);
}
});
server.route({
method: 'GET',
path: '/{p*}',
handler: function (req, reply) {
handler: function (req, h) {
const path = req.path;
if (path === '/' || path.charAt(path.length - 1) !== '/') {
return reply(Boom.notFound());
throw Boom.notFound();
}
const pathPrefix = req.getBasePath() ? `${req.getBasePath()}/` : '';
return reply.redirect(format({
search: req.url.search,
pathname: pathPrefix + path.slice(0, -1),
}))
return h
.redirect(format({
search: req.url.search,
pathname: pathPrefix + path.slice(0, -1),
}))
.permanent(true);
}
});

View file

@ -29,7 +29,7 @@ beforeAll(async () => {
path: '/payload_size_check/test/route',
method: 'POST',
config: { payload: { maxBytes: 200 } },
handler: (req, reply) => reply(null, req.payload.data.slice(0, 5)),
handler: (req) => req.payload.data.slice(0, 5),
});
}, 30000);
@ -41,12 +41,12 @@ test('accepts payload with a size larger than default but smaller than route con
.expect(200, '+++++');
});
test('fails with 400 if payload size is larger than default and route config allows', async () => {
test('fails with 413 if payload size is larger than default and route config allows', async () => {
await kbnTestServer.request.post(root, '/payload_size_check/test/route')
.send({ data: Array(250).fill('+').join('') })
.expect(400, {
statusCode: 400,
error: 'Bad Request',
.expect(413, {
statusCode: 413,
error: 'Request Entity Too Large',
message: 'Payload content length greater than maximum allowed: 200'
});
});

View file

@ -35,8 +35,8 @@ describe('version_check request filter', function () {
kbnTestServer.getKbnServer(root).server.route({
path: '/version_check/test/route',
method: 'GET',
handler: function (req, reply) {
reply(null, 'ok');
handler: function () {
return 'ok';
}
});
}, 30000);

View file

@ -44,8 +44,8 @@ describe('xsrf request filter', () => {
kbnServer.server.route({
path: testPath,
method: 'GET',
handler: function (req, reply) {
reply(null, 'ok');
handler: async function () {
return 'ok';
}
});
@ -58,8 +58,8 @@ describe('xsrf request filter', () => {
parse: false
}
},
handler: function (req, reply) {
reply(null, 'ok');
handler: async function () {
return 'ok';
}
});
@ -72,8 +72,8 @@ describe('xsrf request filter', () => {
parse: false
}
},
handler: function (req, reply) {
reply(null, 'ok');
handler: async function () {
return 'ok';
}
});
}, 30000);

View file

@ -20,14 +20,11 @@
import HapiTemplates from 'vision';
import HapiStaticFiles from 'inert';
import HapiProxy from 'h2o2';
import { fromNode } from 'bluebird';
const plugins = [HapiTemplates, HapiStaticFiles, HapiProxy];
async function registerPlugins(server) {
await fromNode(cb => {
server.register(plugins, cb);
});
return await server.register(plugins);
}
export function registerHapiPlugins(server) {

View file

@ -23,16 +23,16 @@ export function setupVersionCheck(server, config) {
const versionHeader = 'kbn-version';
const actualVersion = config.get('pkg.version');
server.ext('onPostAuth', function (req, reply) {
server.ext('onPostAuth', function onPostAuthVersionCheck(req, h) {
const versionRequested = req.headers[versionHeader];
if (versionRequested && versionRequested !== actualVersion) {
return reply(badRequest('Browser client is out of date, please refresh the page', {
throw badRequest('Browser client is out of date, please refresh the page', {
expected: actualVersion,
got: versionRequested
}));
});
}
return reply.continue();
return h.continue;
});
}

View file

@ -25,13 +25,13 @@ export function setupXsrf(server, config) {
const versionHeader = 'kbn-version';
const xsrfHeader = 'kbn-xsrf';
server.ext('onPostAuth', function (req, reply) {
server.ext('onPostAuth', function onPostAuthXsrf(req, h) {
if (disabled) {
return reply.continue();
return h.continue;
}
if (whitelist.includes(req.path)) {
return reply.continue();
return h.continue;
}
const isSafeMethod = req.method === 'get' || req.method === 'head';
@ -39,9 +39,9 @@ export function setupXsrf(server, config) {
const hasXsrfHeader = xsrfHeader in req.headers;
if (!isSafeMethod && !hasVersionHeader && !hasXsrfHeader) {
return reply(badRequest(`Request must contain a ${xsrfHeader} header.`));
throw badRequest(`Request must contain a ${xsrfHeader} header.`);
}
return reply.continue();
return h.continue;
});
}

View file

@ -32,8 +32,8 @@ export function indexPatternsMixin(kbnServer, server) {
*/
getIndexPatternsService: {
assign: 'indexPatterns',
method(request, reply) {
reply(request.getIndexPatternsService());
method(request) {
return request.getIndexPatternsService();
}
}
};

View file

@ -31,7 +31,7 @@ export const createFieldsForTimePatternRoute = pre => ({
meta_fields: Joi.array().items(Joi.string()).default([]),
}).default()
},
handler(req, reply) {
async handler(req) {
const { indexPatterns } = req.pre;
const {
pattern,
@ -40,15 +40,14 @@ export const createFieldsForTimePatternRoute = pre => ({
meta_fields: metaFields,
} = req.query;
reply(
indexPatterns.getFieldsForTimePattern({
pattern,
interval,
lookBack,
metaFields
})
.then(fields => ({ fields }))
);
const fields = await indexPatterns.getFieldsForTimePattern({
pattern,
interval,
lookBack,
metaFields
});
return { fields };
}
}
});

View file

@ -30,20 +30,19 @@ export const createFieldsForWildcardRoute = pre => ({
meta_fields: Joi.array().items(Joi.string()).default([]),
}).default()
},
handler(req, reply) {
async handler(req) {
const { indexPatterns } = req.pre;
const {
pattern,
meta_fields: metaFields,
} = req.query;
reply(
indexPatterns.getFieldsForWildcard({
pattern,
metaFields
})
.then(fields => ({ fields }))
);
const fields = await indexPatterns.getFieldsForWildcard({
pattern,
metaFields
});
return { fields };
}
}
});

View file

@ -18,7 +18,6 @@
*/
import { constant, once, compact, flatten } from 'lodash';
import { fromNode } from 'bluebird';
import { isWorker } from 'cluster';
import { fromRoot, pkg } from '../utils';
import { Config } from './config';
@ -161,7 +160,7 @@ export default class KbnServer {
return;
}
await fromNode(cb => this.server.stop(cb));
await this.server.stop();
}
async inject(opts) {
@ -185,6 +184,6 @@ export default class KbnServer {
};
const plain = JSON.stringify(subset, null, 2);
this.server.log(['info', 'config'], 'New logging configuration:\n' + plain);
this.server.plugins['even-better'].monitor.reconfigure(loggingOptions);
this.server.plugins['@elastic/good'].reconfigure(loggingOptions);
}
}

View file

@ -18,7 +18,7 @@
*/
import _ from 'lodash';
import logReporter from './log_reporter';
import { getLoggerStream } from './log_reporter';
export default function loggingConfiguration(config) {
const events = config.get('logging.events');
@ -51,33 +51,37 @@ export default function loggingConfiguration(config) {
});
}
const options = {
opsInterval: config.get('ops.interval'),
requestHeaders: true,
requestPayload: true,
reporters: [
{
reporter: logReporter,
config: {
json: config.get('logging.json'),
dest: config.get('logging.dest'),
timezone: config.get('logging.timezone'),
const loggerStream = getLoggerStream({
config: {
json: config.get('logging.json'),
dest: config.get('logging.dest'),
timezone: config.get('logging.timezone'),
// I'm adding the default here because if you add another filter
// using the commandline it will remove authorization. I want users
// to have to explicitly set --logging.filter.authorization=none or
// --logging.filter.cookie=none to have it show up in the logs.
filter: _.defaults(config.get('logging.filter'), {
authorization: 'remove',
cookie: 'remove'
})
},
events: _.transform(events, function (filtered, val, key) {
// provide a string compatible way to remove events
if (val !== '!') filtered[key] = val;
}, {})
}
]
// I'm adding the default here because if you add another filter
// using the commandline it will remove authorization. I want users
// to have to explicitly set --logging.filter.authorization=none or
// --logging.filter.cookie=none to have it show up in the logs.
filter: _.defaults(config.get('logging.filter'), {
authorization: 'remove',
cookie: 'remove'
})
},
events: _.transform(events, function (filtered, val, key) {
// provide a string compatible way to remove events
if (val !== '!') filtered[key] = val;
}, {})
});
const options = {
ops: {
interval: config.get('ops.interval')
},
includes: {
request: ['headers', 'payload']
},
reporters: {
logReporter: [loggerStream]
}
};
return options;
}

View file

@ -17,16 +17,13 @@
* under the License.
*/
import { fromNode } from 'bluebird';
import evenBetter from 'even-better';
import good from '@elastic/good';
import loggingConfiguration from './configuration';
export function setupLogging(server, config) {
return fromNode((cb) => {
server.register({
register: evenBetter,
options: loggingConfiguration(config)
}, cb);
export async function setupLogging(server, config) {
return await server.register({
plugin: good,
options: loggingConfiguration(config)
});
}

View file

@ -35,7 +35,7 @@ function doesMessageMatch(errorMessage, match) {
// converts the given event into a debug log if it's an error of the given type
function downgradeIfErrorType(errorType, event, field = 'errno') {
const isClientError = doTagsMatch(event, ['connection', 'client', 'error']);
const matchesErrorType = isClientError && get(event, `data.${field}`) === errorType;
const matchesErrorType = isClientError && get(event, `error.${field}`) === errorType;
if (!matchesErrorType) return null;
@ -52,7 +52,7 @@ function downgradeIfErrorType(errorType, event, field = 'errno') {
function downgradeIfErrorMessage(match, event) {
const isClientError = doTagsMatch(event, ['connection', 'client', 'error']);
const errorMessage = get(event, 'data.message');
const errorMessage = get(event, 'error.message');
const matchesErrorMessage = isClientError && doesMessageMatch(errorMessage, match);
if (!matchesErrorMessage) return null;

View file

@ -27,7 +27,7 @@ function stubClientErrorEvent(errorMeta) {
pid: 1234,
timestamp: Date.now(),
tags: ['connection', 'client', 'error'],
data: error
error
};
}

View file

@ -24,38 +24,29 @@ import LogFormatJson from './log_format_json';
import LogFormatString from './log_format_string';
import { LogInterceptor } from './log_interceptor';
export default class KbnLogger {
constructor(events, config) {
this.squeeze = new Squeeze(events);
this.format = config.json ? new LogFormatJson(config) : new LogFormatString(config);
this.logInterceptor = new LogInterceptor();
export function getLoggerStream({ events, config }) {
const squeeze = new Squeeze(events);
const format = config.json ? new LogFormatJson(config) : new LogFormatString(config);
const logInterceptor = new LogInterceptor();
if (config.dest === 'stdout') {
this.dest = process.stdout;
} else {
this.dest = writeStr(config.dest, {
flags: 'a',
encoding: 'utf8'
});
}
}
init(readstream, emitter, callback) {
this.output = readstream
.pipe(this.logInterceptor)
.pipe(this.squeeze)
.pipe(this.format);
this.output.pipe(this.dest);
emitter.on('stop', () => {
this.output.unpipe(this.dest);
if (this.dest !== process.stdout) {
this.dest.end();
}
let dest;
if (config.dest === 'stdout') {
dest = process.stdout;
} else {
dest = writeStr(config.dest, {
flags: 'a',
encoding: 'utf8'
});
callback();
logInterceptor.on('end', () => {
dest.end();
});
}
logInterceptor
.pipe(squeeze)
.pipe(format)
.pipe(dest);
return logInterceptor;
}

View file

@ -40,7 +40,7 @@ export default Promise.method(function (kbnServer, server, config) {
};
if (config.get('pid.exclusive')) {
throw Boom.create(500, _.template(log.tmpl)(log), log);
throw Boom.internal(_.template(log.tmpl)(log), log);
} else {
server.log(['pid', 'warning'], log);
}

View file

@ -62,7 +62,7 @@ export class Plugin {
const { config } = kbnServer;
// setup the hapi register function and get on with it
const asyncRegister = async (server, options) => {
const register = async (server, options) => {
this._server = server;
this._options = options;
@ -85,15 +85,8 @@ export class Plugin {
}
};
const register = (server, options, next) => {
asyncRegister(server, options)
.then(() => next(), next);
};
register.attributes = { name: id, version: version };
await kbnServer.server.register({
register: register,
plugin: { register, name: id, version },
options: config.has(configPrefix) ? config.get(configPrefix) : null
});

View file

@ -17,6 +17,7 @@
* under the License.
*/
import Boom from 'boom';
import Joi from 'joi';
import { loadData } from './lib/load_data';
@ -75,13 +76,13 @@ export const createInstallRoute = () => ({
id: Joi.string().required(),
}).required()
},
handler: async (request, reply) => {
handler: async (request, h) => {
const server = request.server;
const sampleDataset = server.getSampleDatasets().find(sampleDataset => {
return sampleDataset.id === request.params.id;
});
if (!sampleDataset) {
return reply().code(404);
return h.response().code(404);
}
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
@ -122,7 +123,7 @@ export const createInstallRoute = () => ({
} catch (err) {
const errMsg = `Unable to create sample data index "${index}", error: ${err.message}`;
server.log(['warning'], errMsg);
return reply(errMsg).code(err.status);
return h.response(errMsg).code(err.status);
}
try {
@ -131,7 +132,7 @@ export const createInstallRoute = () => ({
counts[index] = count;
} catch (err) {
server.log(['warning'], `sample_data install errors while loading data. Error: ${err}`);
return reply(err.message).code(500);
return h.response(err.message).code(500);
}
}
@ -140,17 +141,17 @@ export const createInstallRoute = () => ({
createResults = await request.getSavedObjectsClient().bulkCreate(sampleDataset.savedObjects, { overwrite: true });
} catch (err) {
server.log(['warning'], `bulkCreate failed, error: ${err.message}`);
return reply(`Unable to load kibana saved objects, see kibana logs for details`).code(500);
return Boom.badImplementation(`Unable to load kibana saved objects, see kibana logs for details`);
}
const errors = createResults.saved_objects.filter(savedObjectCreateResult => {
return savedObjectCreateResult.hasOwnProperty('error');
});
if (errors.length > 0) {
server.log(['warning'], `sample_data install errors while loading saved objects. Errors: ${errors.join(',')}`);
return reply(`Unable to load kibana saved objects, see kibana logs for details`).code(403);
return h.response(`Unable to load kibana saved objects, see kibana logs for details`).code(403);
}
return reply({ elasticsearchIndicesCreated: counts, kibanaSavedObjectsLoaded: sampleDataset.savedObjects.length });
return h.response({ elasticsearchIndicesCreated: counts, kibanaSavedObjectsLoaded: sampleDataset.savedObjects.length });
}
}
});

View file

@ -28,7 +28,7 @@ export const createListRoute = () => ({
path: '/api/sample_data',
method: 'GET',
config: {
handler: async (request, reply) => {
handler: async (request) => {
const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('data');
const sampleDatasets = request.server.getSampleDatasets().map(sampleDataset => {
@ -86,7 +86,7 @@ export const createListRoute = () => ({
});
await Promise.all(isInstalledPromises);
reply(sampleDatasets);
return sampleDatasets;
}
}
});

View file

@ -31,15 +31,14 @@ export const createUninstallRoute = () => ({
id: Joi.string().required(),
}).required()
},
handler: async (request, reply) => {
handler: async (request, h) => {
const server = request.server;
const sampleDataset = server.getSampleDatasets().find(({ id }) => {
return id === request.params.id;
});
if (!sampleDataset) {
reply().code(404);
return;
return h.response().code(404);
}
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
@ -51,7 +50,7 @@ export const createUninstallRoute = () => ({
try {
await callWithRequest(request, 'indices.delete', { index: index });
} catch (err) {
return reply(`Unable to delete sample data index "${index}", error: ${err.message}`).code(err.status);
return h.response(`Unable to delete sample data index "${index}", error: ${err.message}`).code(err.status);
}
}
@ -63,11 +62,11 @@ export const createUninstallRoute = () => ({
} catch (err) {
// ignore 404s since users could have deleted some of the saved objects via the UI
if (_.get(err, 'output.statusCode') !== 404) {
return reply(`Unable to delete sample dataset saved objects, error: ${err.message}`).code(403);
return h.response(`Unable to delete sample dataset saved objects, error: ${err.message}`).code(403);
}
}
reply({});
return {};
}
}
});

View file

@ -17,14 +17,22 @@
* under the License.
*/
const Hapi = require('hapi');
import Hapi from 'hapi';
import { defaultValidationErrorHandler } from '../../../core/server/http/http_tools';
const defaultConfig = {
'kibana.index': '.kibana'
};
export function MockServer(config = defaultConfig) {
const server = new Hapi.Server();
server.connection({ port: 8080 });
const server = new Hapi.Server({
port: 0,
routes: {
validate: {
failAction: defaultValidationErrorHandler
}
}
});
server.config = function () {
return {
get: (key) => {

View file

@ -40,11 +40,11 @@ export const createBulkCreateRoute = prereqs => ({
}).required()
),
},
handler(request, reply) {
handler(request) {
const { overwrite } = request.query;
const { savedObjectsClient } = request.pre;
reply(savedObjectsClient.bulkCreate(request.payload, { overwrite }));
return savedObjectsClient.bulkCreate(request.payload, { overwrite });
},
},
});

View file

@ -30,10 +30,10 @@ export const createBulkGetRoute = (prereqs) => ({
id: Joi.string().required(),
}).required())
},
handler(request, reply) {
handler(request) {
const { savedObjectsClient } = request.pre;
reply(savedObjectsClient.bulkGet(request.payload));
return savedObjectsClient.bulkGet(request.payload);
}
}
});

View file

@ -22,7 +22,7 @@ import { createBulkGetRoute } from './bulk_get';
import { MockServer } from './_mock_server';
describe('POST /api/saved_objects/_bulk_get', () => {
const savedObjectsClient = { bulkGet: sinon.stub() };
const savedObjectsClient = { bulkGet: sinon.stub().returns('') };
let server;
beforeEach(() => {
@ -31,8 +31,8 @@ describe('POST /api/saved_objects/_bulk_get', () => {
const prereqs = {
getSavedObjectsClient: {
assign: 'savedObjectsClient',
method(request, reply) {
reply(savedObjectsClient);
method() {
return savedObjectsClient;
}
},
};

View file

@ -23,7 +23,7 @@ export const createCreateRoute = prereqs => {
return {
path: '/api/saved_objects/{type}/{id?}',
method: 'POST',
config: {
options: {
pre: [prereqs.getSavedObjectsClient],
validate: {
query: Joi.object()
@ -42,14 +42,14 @@ export const createCreateRoute = prereqs => {
migrationVersion: Joi.object().optional(),
}).required(),
},
handler(request, reply) {
handler(request) {
const { savedObjectsClient } = request.pre;
const { type, id } = request.params;
const { overwrite } = request.query;
const { migrationVersion } = request.payload;
const options = { id, overwrite, migrationVersion };
reply(savedObjectsClient.create(type, request.payload.attributes, options));
return savedObjectsClient.create(type, request.payload.attributes, options);
},
},
};

View file

@ -22,7 +22,7 @@ import { createCreateRoute } from './create';
import { MockServer } from './_mock_server';
describe('POST /api/saved_objects/{type}', () => {
const savedObjectsClient = { create: sinon.stub() };
const savedObjectsClient = { create: sinon.stub().returns('') };
let server;
beforeEach(() => {
@ -31,8 +31,8 @@ describe('POST /api/saved_objects/{type}', () => {
const prereqs = {
getSavedObjectsClient: {
assign: 'savedObjectsClient',
method(request, reply) {
reply(savedObjectsClient);
method() {
return savedObjectsClient;
}
},
};
@ -100,7 +100,7 @@ describe('POST /api/saved_objects/{type}', () => {
expect(savedObjectsClient.create.calledOnce).toBe(true);
const args = savedObjectsClient.create.getCall(0).args;
const options = { overwrite: false, id: undefined };
const options = { overwrite: false, id: undefined, migrationVersion: undefined };
const attributes = { title: 'Testing' };
expect(args).toEqual(['index-pattern', attributes, options]);

View file

@ -30,11 +30,11 @@ export const createDeleteRoute = (prereqs) => ({
id: Joi.string().required(),
}).required()
},
handler(request, reply) {
handler(request) {
const { savedObjectsClient } = request.pre;
const { type, id } = request.params;
reply(savedObjectsClient.delete(type, id));
return savedObjectsClient.delete(type, id);
}
}
});

View file

@ -31,8 +31,8 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => {
const prereqs = {
getSavedObjectsClient: {
assign: 'savedObjectsClient',
method(request, reply) {
reply(savedObjectsClient);
method() {
return savedObjectsClient;
}
},
};

View file

@ -36,9 +36,9 @@ export const createFindRoute = (prereqs) => ({
fields: Joi.array().items(Joi.string()).single()
}).default()
},
handler(request, reply) {
handler(request) {
const options = keysToCamelCaseShallow(request.query);
reply(request.pre.savedObjectsClient.find(options));
return request.pre.savedObjectsClient.find(options);
}
}
});

View file

@ -22,7 +22,7 @@ import { createFindRoute } from './find';
import { MockServer } from './_mock_server';
describe('GET /api/saved_objects/_find', () => {
const savedObjectsClient = { find: sinon.stub() };
const savedObjectsClient = { find: sinon.stub().returns('') };
let server;
beforeEach(() => {
@ -31,8 +31,8 @@ describe('GET /api/saved_objects/_find', () => {
const prereqs = {
getSavedObjectsClient: {
assign: 'savedObjectsClient',
method(request, reply) {
reply(savedObjectsClient);
method() {
return savedObjectsClient;
}
},
};

View file

@ -30,11 +30,11 @@ export const createGetRoute = (prereqs) => ({
id: Joi.string().required(),
}).required()
},
handler(request, reply) {
handler(request) {
const { savedObjectsClient } = request.pre;
const { type, id } = request.params;
reply(savedObjectsClient.get(type, id));
return savedObjectsClient.get(type, id);
}
}
});

View file

@ -22,7 +22,7 @@ import { createGetRoute } from './get';
import { MockServer } from './_mock_server';
describe('GET /api/saved_objects/{type}/{id}', () => {
const savedObjectsClient = { get: sinon.stub() };
const savedObjectsClient = { get: sinon.stub().returns('') };
let server;
beforeEach(() => {
@ -31,8 +31,8 @@ describe('GET /api/saved_objects/{type}/{id}', () => {
const prereqs = {
getSavedObjectsClient: {
assign: 'savedObjectsClient',
method(request, reply) {
reply(savedObjectsClient);
method() {
return savedObjectsClient;
}
},
};

View file

@ -35,13 +35,13 @@ export const createUpdateRoute = (prereqs) => {
version: Joi.number().min(1)
}).required()
},
handler(request, reply) {
handler(request) {
const { savedObjectsClient } = request.pre;
const { type, id } = request.params;
const { attributes, version } = request.payload;
const options = { version };
reply(savedObjectsClient.update(type, id, attributes, options));
return savedObjectsClient.update(type, id, attributes, options);
}
}
};

View file

@ -22,7 +22,7 @@ import { createUpdateRoute } from './update';
import { MockServer } from './_mock_server';
describe('PUT /api/saved_objects/{type}/{id?}', () => {
const savedObjectsClient = { update: sinon.stub() };
const savedObjectsClient = { update: sinon.stub().returns('') };
let server;
beforeEach(() => {
@ -31,8 +31,8 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => {
const prereqs = {
getSavedObjectsClient: {
assign: 'savedObjectsClient',
method(request, reply) {
reply(savedObjectsClient);
method() {
return savedObjectsClient;
}
},
};

View file

@ -51,8 +51,8 @@ export function savedObjectsMixin(kbnServer, server) {
const prereqs = {
getSavedObjectsClient: {
assign: 'savedObjectsClient',
method(req, reply) {
reply(req.getSavedObjectsClient());
method(req) {
return req.getSavedObjectsClient();
},
},
};

View file

@ -21,6 +21,8 @@ import ServerStatus from './server_status';
import { Metrics } from './lib/metrics';
import { registerStatusPage, registerStatusApi, registerStatsApi } from './routes';
import { getOpsStatsCollector } from './collectors';
import Oppsy from 'oppsy';
import { cloneDeep } from 'lodash';
export function statusMixin(kbnServer, server, config) {
kbnServer.status = new ServerStatus(kbnServer.server);
@ -29,15 +31,23 @@ export function statusMixin(kbnServer, server, config) {
const { collectorSet } = server.usage;
collectorSet.register(statsCollector);
const { ['even-better']: evenBetter } = server.plugins;
const metrics = new Metrics(config, server);
if (evenBetter) {
const metrics = new Metrics(config, server);
const oppsy = new Oppsy(server);
oppsy.on('ops', event => {
// Oppsy has a bad race condition that will modify this data before
// we ship it off to the buffer. Let's create our copy first.
event = cloneDeep(event);
// Oppsy used to provide this, but doesn't anymore. Grab it ourselves.
server.listener.getConnections((_, count) => {
event.concurrent_connections = count;
evenBetter.monitor.on('ops', event => {
metrics.capture(event).then(data => { kbnServer.metrics = data; }); // captures (performs transforms on) the latest event data and stashes the metrics for status/stats API payload
// captures (performs transforms on) the latest event data and stashes
// the metrics for status/stats API payload
metrics.capture(event).then(data => { kbnServer.metrics = data; });
});
}
});
oppsy.start(config.get('ops.interval'));
// init routes
registerStatusPage(kbnServer, server, config);

View file

@ -23,6 +23,12 @@ import { get, isObject, merge } from 'lodash';
import { keysToSnakeCaseShallow } from '../../../utils/case_conversion';
import { getAllStats as cGroupStats } from './cgroup';
const requestDefaults = {
disconnects: 0,
statusCodes: {},
total: 0,
};
export class Metrics {
constructor(config, server) {
this.config = config;
@ -98,8 +104,11 @@ export class Metrics {
avg_in_millis: isNaN(avgInMillis) ? undefined : avgInMillis, // convert NaN to undefined
max_in_millis: maxInMillis
},
requests: keysToSnakeCaseShallow(get(hapiEvent, ['requests', port])),
concurrent_connections: get(hapiEvent, ['concurrents', port]),
requests: {
...requestDefaults,
...keysToSnakeCaseShallow(get(hapiEvent, ['requests', port]))
},
concurrent_connections: hapiEvent.concurrent_connections
};
}

View file

@ -100,7 +100,7 @@ describe('Metrics', function () {
'osup': 1008991,
'psup': 7.168,
'psmem': { 'rss': 193716224, 'heapTotal': 168194048, 'heapUsed': 130553400, 'external': 1779619 },
'concurrents': { '5603': 0 },
'concurrent_connections': 0,
'psdelay': 1.6091690063476562,
'host': 'blahblah.local'
};

Some files were not shown because too many files have changed in this diff Show more