[Health Gateway] Add integration tests (#146334)

This commit is contained in:
Michael Dokolin 2022-12-08 22:51:04 +01:00 committed by GitHub
parent 866101eda6
commit 1bf581af01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 411 additions and 7 deletions

View file

@ -96,6 +96,7 @@ enabled:
- test/functional/apps/visualize/replaced_vislib_chart_types/config.ts
- test/functional/config.ccs.ts
- test/functional/config.firefox.js
- test/health_gateway/config.ts
- test/interactive_setup_api_integration/enrollment_flow.config.ts
- test/interactive_setup_api_integration/manual_configuration_flow_without_tls.config.ts
- test/interactive_setup_api_integration/manual_configuration_flow.config.ts

View file

@ -35,6 +35,7 @@ export class KibanaService {
async start({ server }: KibanaServiceStartDependencies) {
server.addRoute(new RootRoute(this.kibanaConfig, this.logger));
this.logger.info('Server is ready');
}
stop() {

View file

@ -40,7 +40,6 @@ export class RootRoute implements ServerRoute {
return (status >= 200 && status <= 299) || status === 302;
}
private static readonly POLL_ROUTE = '/';
private static readonly STATUS_CODE: Record<Status, number> = {
healthy: 200,
unhealthy: 503,
@ -78,13 +77,14 @@ export class RootRoute implements ServerRoute {
}
private async pollHost(host: string): Promise<HostStatus> {
const url = `${host}${RootRoute.POLL_ROUTE}`;
this.logger.debug(`Requesting ${url}`);
this.logger.debug(`Requesting '${host}'`);
try {
const response = await this.fetch(url);
const response = await this.fetch(host);
const status = RootRoute.isHealthy(response) ? 'healthy' : 'unhealthy';
this.logger.debug(`${capitalize(status)} response from ${url} with code ${response.status}`);
this.logger.debug(
`${capitalize(status)} response from '${host}' with code ${response.status}`
);
return {
host,
@ -95,7 +95,7 @@ export class RootRoute implements ServerRoute {
this.logger.error(error);
if (error.name === 'AbortError') {
this.logger.error(`Request timeout for ${url}`);
this.logger.error(`Request timeout for '${host}'`);
return {
host,
@ -103,7 +103,7 @@ export class RootRoute implements ServerRoute {
};
}
this.logger.error(`Failed response from ${url}: ${error.message}`);
this.logger.error(`Failed response from '${host}': ${error.message}`);
return {
host,

View file

@ -23,6 +23,7 @@ function getPluginSearchPaths({ rootDir, oss, examples, testPlugins }) {
...(testPlugins
? [
resolve(rootDir, 'test/analytics/fixtures/plugins'),
resolve(rootDir, 'test/health_gateway/plugins'),
resolve(rootDir, 'test/plugin_functional/plugins'),
resolve(rootDir, 'test/interpreter_functional/plugins'),
resolve(rootDir, 'test/common/fixtures/plugins'),

View file

@ -0,0 +1,34 @@
/*
* 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 path from 'path';
import { FtrConfigProviderContext } from '@kbn/test';
import { services } from './services';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js'));
return {
services,
rootTags: ['runOutsideOfCiGroups'],
esTestCluster: functionalConfig.get('esTestCluster'),
servers: functionalConfig.get('servers'),
testFiles: [require.resolve('./tests')],
junit: {
reportName: 'Health Gateway Functional Tests',
},
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
'--env.name=development',
`--plugin-path=${path.resolve(__dirname, 'plugins/status')}`,
],
},
};
}

View file

@ -0,0 +1,13 @@
server:
port: ${PORT}
host: ${HOST}
kibana:
hosts:
- ${KIBANA_URL}/api/status/ok
- ${KIBANA_URL}/api/status/flaky?session=${SESSION}
logging:
root:
appenders: ['console']
level: 'all'

View file

@ -0,0 +1,14 @@
server:
port: ${PORT}
host: ${HOST}
kibana:
hosts:
- ${KIBANA_URL}/api/status/ok
- ${KIBANA_URL}/api/status/redirect
- ${KIBANA_URL}/api/status/unauthorized
logging:
root:
appenders: ['console']
level: 'all'

View file

@ -0,0 +1,13 @@
server:
port: ${PORT}
host: ${HOST}
kibana:
hosts:
- http://localhost:65537/api/status/ok
requestTimeout: 2s
logging:
root:
appenders: ['console']
level: 'all'

View file

@ -0,0 +1,13 @@
server:
port: ${PORT}
host: ${HOST}
kibana:
hosts:
- ${KIBANA_URL}/api/status/ok
- ${KIBANA_URL}/api/status/not-found
logging:
root:
appenders: ['console']
level: 'all'

View file

@ -0,0 +1,13 @@
server:
port: ${PORT}
host: ${HOST}
kibana:
hosts:
- ${KIBANA_URL}/api/status/slow
requestTimeout: 2s
logging:
root:
appenders: ['console']
level: 'all'

View file

@ -0,0 +1,12 @@
server:
port: ${PORT}
host: ${HOST}
kibana:
hosts:
- ${KIBANA_URL}/api/status/not-found
logging:
root:
appenders: ['console']
level: 'all'

View file

@ -0,0 +1,12 @@
{
"id": "kbnHealthGatewayStatus",
"owner": {
"name": "Core",
"githubTeam": "kibana-core"
},
"version": "0.1.0",
"kibanaVersion": "kibana",
"requiredPlugins": [],
"server": true,
"requiredBundles": []
}

View file

@ -0,0 +1,10 @@
{
"name": "kbn_health_gateway_status",
"version": "0.1.0",
"main": "target/test/health_gateway/plugins/status",
"kibana": {
"version": "kibana",
"templateVersion": "0.1.0"
},
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,13 @@
/*
* 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 { HealthGatewayStatusPlugin } from './plugin';
export function plugin() {
return new HealthGatewayStatusPlugin();
}

View file

@ -0,0 +1,62 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { CoreSetup, KibanaRequest, Plugin } from '@kbn/core/server';
export class HealthGatewayStatusPlugin implements Plugin<void, void> {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
router.get({ path: '/api/status/ok', validate: {} }, async (context, req, res) => res.ok());
router.get({ path: '/api/status/redirect', validate: {} }, async (context, req, res) =>
res.redirected({ headers: { location: '/api/status/ok' } })
);
router.get({ path: '/api/status/unauthorized', validate: {} }, async (context, req, res) =>
res.unauthorized({
headers: { 'www-authenticate': 'Basic' },
})
);
router.get({ path: '/api/status/not-found', validate: {} }, async (context, req, res) =>
res.notFound()
);
router.get({ path: '/api/status/slow', validate: {} }, async (context, req, res) => {
await new Promise((resolve) => setTimeout(resolve, 5000));
return res.ok();
});
const sessions = new Set<string>();
router.get(
{
path: '/api/status/flaky',
validate: {
query: schema.object({ session: schema.string() }),
},
},
async (context, req: KibanaRequest<void, { session: string }>, res) => {
if (sessions.has(req.query.session)) {
sessions.delete(req.query.session);
return res.custom({ statusCode: 500, body: 'Flaky' });
}
sessions.add(req.query.session);
return res.ok();
}
);
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,13 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target/types"
},
"include": [
"server/**/*.ts",
],
"exclude": [],
"kbn_references": [
{ "path": "../../../../src/core/tsconfig.json" }
]
}

View file

@ -0,0 +1,74 @@
/*
* 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 { resolve } from 'path';
import { format } from 'url';
import getPort from 'get-port';
import supertest from 'supertest';
import { ProcRunner } from '@kbn/dev-proc-runner';
import { REPO_ROOT } from '@kbn/utils';
import { FtrService } from '../../functional/ftr_provider_context';
interface HealthGatewayOptions {
env?: Record<string, string>;
}
export class HealthGatewayService extends FtrService {
private runner = new ProcRunner(this.ctx.getService('log'));
private kibanaUrl = format(this.ctx.getService('config').get('servers.kibana'));
private host = 'localhost';
private port?: number;
private assertRunning() {
if (!this.port) {
throw new Error('Health gateway is not running');
}
}
async start(config: string, { env = {} }: HealthGatewayOptions = {}) {
if (this.port) {
throw new Error('Health gateway is already running');
}
this.port = await getPort({ port: getPort.makeRange(1024, 65536) });
await this.runner.run(`health-gateway-${this.port}`, {
cmd: 'yarn',
args: [
'kbn',
'run-in-packages',
'--filter=@kbn/health-gateway-server',
'start',
'--config',
resolve(__dirname, '..', config),
],
cwd: REPO_ROOT,
env: {
...env,
KIBANA_URL: this.kibanaUrl,
HOST: this.host,
PORT: `${this.port}`,
CI: '', // Override in the CI environment to capture the logs.
},
wait: /Server is ready/,
});
}
async stop() {
this.assertRunning();
await this.runner?.stop(`health-gateway-${this.port}`);
this.port = undefined;
}
poll() {
this.assertRunning();
return supertest(`http://${this.host}:${this.port}`).get('/').send();
}
}

View file

@ -0,0 +1,16 @@
/*
* 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 { services as commonServices } from '../../common/services';
import { HealthGatewayService } from './health_gateway';
export const services = {
...commonServices,
healthGateway: HealthGatewayService,
};

View file

@ -0,0 +1,12 @@
/*
* 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 type { GenericFtrProviderContext } from '@kbn/test';
import type { services } from '../services';
export type FtrProviderContext = GenericFtrProviderContext<typeof services, {}>;

View file

@ -0,0 +1,63 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from './ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const healthGateway = getService('healthGateway');
describe('health gateway', () => {
it('returns 200 on healthy hosts', async () => {
await healthGateway.start('fixtures/healthy.yaml');
const { body } = await healthGateway.poll().expect(200);
expect(body).to.have.property('status', 'healthy');
});
it('returns 503 on unhealthy host', async () => {
await healthGateway.start('fixtures/unhealthy.yaml');
const { body } = await healthGateway.poll().expect(503);
expect(body).to.have.property('status', 'unhealthy');
});
it('returns 503 on mixed responses', async () => {
await healthGateway.start('fixtures/mixed.yaml');
const { body } = await healthGateway.poll().expect(503);
expect(body).to.have.property('status', 'unhealthy');
});
it('returns 504 on timeout', async () => {
await healthGateway.start('fixtures/timeout.yaml');
const { body } = await healthGateway.poll().expect(504);
expect(body).to.have.property('status', 'timeout');
});
it('returns 502 on exception', async () => {
await healthGateway.start('fixtures/invalid.yaml');
const { body } = await healthGateway.poll().expect(502);
expect(body).to.have.property('status', 'failure');
});
it('returns different status codes on state changes', async () => {
await healthGateway.start('fixtures/flaky.yaml', { env: { SESSION: `${Math.random()}` } });
await healthGateway.poll().expect(200);
await healthGateway.poll().expect(503);
await healthGateway.poll().expect(200);
});
afterEach(async () => {
await healthGateway.stop();
});
});
}

View file

@ -5,6 +5,7 @@ source src/dev/ci_setup/setup_env.sh
echo " -> building kibana platform plugins"
node scripts/build_kibana_platform_plugins \
--scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \
--scan-dir "$KIBANA_DIR/test/health_gateway/plugins" \
--scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \
--scan-dir "$KIBANA_DIR/test/common/fixtures/plugins" \
--scan-dir "$KIBANA_DIR/examples" \

View file

@ -13,6 +13,7 @@ if [[ -z "$CODE_COVERAGE" ]]; then
if [[ ! "$TASK_QUEUE_PROCESS_ID" && "$CI_GROUP" == "1" ]]; then
source test/scripts/jenkins_build_kbn_sample_panel_action.sh
./test/scripts/test/plugin_functional.sh
./test/scripts/test/health_gateway.sh
./test/scripts/test/interpreter_functional.sh
fi
else

View file

@ -11,4 +11,5 @@ cd -;
pwd
./test/scripts/test/plugin_functional.sh
./test/scripts/test/health_gateway.sh
./test/scripts/test/interpreter_functional.sh

View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
source test/scripts/jenkins_test_setup_oss.sh
node scripts/functional_tests \
--config test/health_gateway/config.ts \
--bail \
--debug \
--kibana-install-dir $KIBANA_INSTALL_DIR

View file

@ -962,6 +962,8 @@
"@kbn/newsfeed-fixtures-plugin/*": ["test/common/fixtures/plugins/newsfeed/*"],
"@kbn/open-telemetry-instrumented-plugin": ["test/common/fixtures/plugins/otel_metrics"],
"@kbn/open-telemetry-instrumented-plugin/*": ["test/common/fixtures/plugins/otel_metrics/*"],
"@kbn/kbn-health-gateway-status-plugin": ["test/health_gateway/plugins/status"],
"@kbn/kbn-health-gateway-status-plugin/*": ["test/health_gateway/plugins/status/*"],
"@kbn/kbn-tp-run-pipeline-plugin": ["test/interpreter_functional/plugins/kbn_tp_run_pipeline"],
"@kbn/kbn-tp-run-pipeline-plugin/*": ["test/interpreter_functional/plugins/kbn_tp_run_pipeline/*"],
"@kbn/app-link-test-plugin": ["test/plugin_functional/plugins/app_link_test"],