Skip to content

Commit

Permalink
[Health Gateway] Update response aggregation (#145761)
Browse files Browse the repository at this point in the history
  • Loading branch information
dokmic authored Nov 25, 2022
1 parent a287ea4 commit 2728ee3
Show file tree
Hide file tree
Showing 4 changed files with 378 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import type { IConfigService } from '@kbn/config';
import type { Logger, LoggerFactory } from '@kbn/logging';
import { ServerStart } from '../server';
import { createRootRoute } from './routes';
import { KibanaConfig } from './kibana_config';
import { RootRoute } from './routes';

interface KibanaServiceStartDependencies {
server: ServerStart;
Expand All @@ -25,15 +26,15 @@ interface KibanaServiceDependencies {
*/
export class KibanaService {
private readonly logger: Logger;
private readonly config: IConfigService;
private readonly kibanaConfig: KibanaConfig;

constructor({ logger, config }: KibanaServiceDependencies) {
this.logger = logger.get('kibana-service');
this.config = config;
this.kibanaConfig = new KibanaConfig({ config, logger: this.logger });
}

async start({ server }: KibanaServiceStartDependencies) {
server.addRoute(createRootRoute({ config: this.config, logger: this.logger }));
server.addRoute(new RootRoute(this.kibanaConfig, this.logger));
}

stop() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
* Side Public License, v 1.
*/

export { createRootRoute } from './root';
export { RootRoute } from './root';
252 changes: 252 additions & 0 deletions packages/kbn-health-gateway-server/src/kibana/routes/root.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { Server } from '@hapi/hapi';
import { duration } from 'moment';
import fetch, { Response } from 'node-fetch';
import { loggerMock, MockedLogger } from '@kbn/logging-mocks';
import type { KibanaConfig } from '../kibana_config';
import { RootRoute } from './root';

describe('RootRoute', () => {
let kibanaConfig: KibanaConfig;
let logger: MockedLogger;
let server: Server;

beforeAll(async () => {
jest.spyOn(await import('node-fetch'), 'default');
});

beforeEach(async () => {
kibanaConfig = {
hosts: ['http://localhost:5601'],
requestTimeout: duration(60, 's'),
} as unknown as typeof kibanaConfig;
logger = loggerMock.create();

server = new Server();
server.route(new RootRoute(kibanaConfig, logger));
await server.initialize();
});

afterEach(() => {
jest.clearAllMocks();
});

afterAll(() => {
jest.restoreAllMocks();
});

describe('handler', () => {
const ok = { status: 200 };
const noContent = { status: 204 };
const found = { status: 302 };
const badRequest = { status: 400 };
const unauthorized = { status: 401, headers: { 'www-authenticate': '' } };
const forbidden = { status: 403 };
const notFound = { status: 404 };
const serverError = { status: 500 };
const badGateway = { status: 502 };
const unavailable = { status: 503 };
const timeout = { status: 504 };

it.each`
config | status | code
${ok} | ${'healthy'} | ${200}
${noContent} | ${'healthy'} | ${200}
${found} | ${'healthy'} | ${200}
${unauthorized} | ${'healthy'} | ${200}
${forbidden} | ${'unhealthy'} | ${503}
${notFound} | ${'unhealthy'} | ${503}
${badRequest} | ${'unhealthy'} | ${503}
${serverError} | ${'unhealthy'} | ${503}
${badGateway} | ${'unhealthy'} | ${503}
${unavailable} | ${'unhealthy'} | ${503}
${timeout} | ${'unhealthy'} | ${503}
`(
"should return '$status' with $code when Kibana host returns $config.status",
async ({ config, status, code }) => {
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce(
new Response('', config)
);

const response = server.inject({
method: 'get',
url: '/',
});

await expect(response).resolves.toEqual(
expect.objectContaining({
statusCode: code,
result: expect.objectContaining({
status,
hosts: [
expect.objectContaining({
status,
code: config.status,
host: 'http://localhost:5601',
}),
],
}),
})
);
}
);

it("should return 'failure' with 502 when `fetch` throws an error", async () => {
(fetch as jest.MockedFunction<typeof fetch>).mockRejectedValueOnce(new Error('Fetch Error'));
const response = server.inject({
method: 'get',
url: '/',
});

await expect(response).resolves.toEqual(
expect.objectContaining({
statusCode: 502,
result: expect.objectContaining({
status: 'failure',
hosts: [
expect.objectContaining({
status: 'failure',
host: 'http://localhost:5601',
}),
],
}),
})
);
});

it("should return 'timeout' with 504 when `fetch` timeouts", async () => {
(fetch as jest.MockedFunction<typeof fetch>).mockImplementationOnce(
(url, { signal } = {}) => {
return new Promise((resolve, reject) => {
signal?.addEventListener('abort', () => {
reject(new DOMException('Fetch Aborted', 'AbortError'));
});

jest.advanceTimersByTime(60000);
});
}
);

jest.useFakeTimers({ doNotFake: ['nextTick'] });

const response = server.inject({
method: 'get',
url: '/',
});

try {
await expect(response).resolves.toEqual(
expect.objectContaining({
statusCode: 504,
result: expect.objectContaining({
status: 'timeout',
hosts: [
expect.objectContaining({
status: 'timeout',
host: 'http://localhost:5601',
}),
],
}),
})
);
} finally {
jest.useRealTimers();
}
});

it("should always return 'healthy' when there are no hosts", async () => {
kibanaConfig.hosts.splice(0);

const response = server.inject({
method: 'get',
url: '/',
});

await expect(response).resolves.toEqual(
expect.objectContaining({
statusCode: 200,
result: expect.objectContaining({
status: 'healthy',
hosts: [],
}),
})
);
expect(fetch).not.toHaveBeenCalled();
});

it("should return 'healthy' only when all the hosts healthy", async () => {
kibanaConfig.hosts.push('http://localhost:5602');

(fetch as jest.MockedFunction<typeof fetch>)
.mockResolvedValueOnce(new Response('', ok))
.mockResolvedValueOnce(new Response('', unauthorized));

const response = server.inject({
method: 'get',
url: '/',
});

await expect(response).resolves.toEqual(
expect.objectContaining({
statusCode: 200,
result: expect.objectContaining({
status: 'healthy',
hosts: expect.arrayContaining([
expect.objectContaining({
status: 'healthy',
code: ok.status,
host: 'http://localhost:5601',
}),
expect.objectContaining({
status: 'healthy',
code: unauthorized.status,
host: 'http://localhost:5602',
}),
]),
}),
})
);
});

it("should return 'unhealthy' when at least one host is not healthy", async () => {
kibanaConfig.hosts.push('http://localhost:5602');

(fetch as jest.MockedFunction<typeof fetch>)
.mockResolvedValueOnce(new Response('', ok))
.mockResolvedValueOnce(new Response('', serverError));

const response = server.inject({
method: 'get',
url: '/',
});

await expect(response).resolves.toEqual(
expect.objectContaining({
statusCode: 503,
result: expect.objectContaining({
status: 'unhealthy',
hosts: expect.arrayContaining([
expect.objectContaining({
status: 'healthy',
code: ok.status,
host: 'http://localhost:5601',
}),
expect.objectContaining({
status: 'unhealthy',
code: serverError.status,
host: 'http://localhost:5602',
}),
]),
}),
})
);
});
});
});
Loading

0 comments on commit 2728ee3

Please sign in to comment.