mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Adds base implementation of the Kibana Health Gateway. (#141172)
This commit is contained in:
parent
20b8741263
commit
4b863abc18
48 changed files with 1812 additions and 1 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -931,6 +931,7 @@ packages/kbn-get-repo-files @elastic/kibana-operations
|
|||
packages/kbn-guided-onboarding @elastic/platform-onboarding
|
||||
packages/kbn-handlebars @elastic/kibana-security
|
||||
packages/kbn-hapi-mocks @elastic/kibana-core
|
||||
packages/kbn-health-gateway-server @elastic/kibana-core
|
||||
packages/kbn-i18n @elastic/kibana-core
|
||||
packages/kbn-i18n-react @elastic/kibana-core
|
||||
packages/kbn-import-resolver @elastic/kibana-operations
|
||||
|
|
|
@ -335,6 +335,7 @@
|
|||
"@kbn/guided-onboarding": "link:bazel-bin/packages/kbn-guided-onboarding",
|
||||
"@kbn/handlebars": "link:bazel-bin/packages/kbn-handlebars",
|
||||
"@kbn/hapi-mocks": "link:bazel-bin/packages/kbn-hapi-mocks",
|
||||
"@kbn/health-gateway-server": "link:bazel-bin/packages/kbn-health-gateway-server",
|
||||
"@kbn/home-sample-data-card": "link:bazel-bin/packages/home/sample_data_card",
|
||||
"@kbn/home-sample-data-tab": "link:bazel-bin/packages/home/sample_data_tab",
|
||||
"@kbn/home-sample-data-types": "link:bazel-bin/packages/home/sample_data_types",
|
||||
|
|
|
@ -244,6 +244,7 @@ filegroup(
|
|||
"//packages/kbn-guided-onboarding:build",
|
||||
"//packages/kbn-handlebars:build",
|
||||
"//packages/kbn-hapi-mocks:build",
|
||||
"//packages/kbn-health-gateway-server:build",
|
||||
"//packages/kbn-i18n:build",
|
||||
"//packages/kbn-i18n-react:build",
|
||||
"//packages/kbn-import-resolver:build",
|
||||
|
@ -593,6 +594,7 @@ filegroup(
|
|||
"//packages/kbn-guided-onboarding:build_types",
|
||||
"//packages/kbn-handlebars:build_types",
|
||||
"//packages/kbn-hapi-mocks:build_types",
|
||||
"//packages/kbn-health-gateway-server:build_types",
|
||||
"//packages/kbn-i18n:build_types",
|
||||
"//packages/kbn-i18n-react:build_types",
|
||||
"//packages/kbn-import-resolver:build_types",
|
||||
|
|
1
packages/kbn-health-gateway-server/.gitignore
vendored
Normal file
1
packages/kbn-health-gateway-server/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
scripts/.env
|
127
packages/kbn-health-gateway-server/BUILD.bazel
Normal file
127
packages/kbn-health-gateway-server/BUILD.bazel
Normal file
|
@ -0,0 +1,127 @@
|
|||
load("@npm//@bazel/typescript:index.bzl", "ts_config")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
|
||||
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
|
||||
|
||||
PKG_DIRNAME = "kbn-health-gateway-server"
|
||||
PKG_REQUIRE_NAME = "@kbn/health-gateway-server"
|
||||
|
||||
SOURCE_FILES = glob(
|
||||
[
|
||||
"**/*.ts",
|
||||
],
|
||||
exclude = [
|
||||
"**/*.config.js",
|
||||
"**/*.mock.*",
|
||||
"**/*.test.*",
|
||||
"**/*.stories.*",
|
||||
"**/__snapshots__",
|
||||
"**/integration_tests",
|
||||
"**/mocks",
|
||||
"**/scripts",
|
||||
"**/storybook",
|
||||
"**/test_fixtures",
|
||||
"**/test_helpers",
|
||||
],
|
||||
)
|
||||
|
||||
SRCS = SOURCE_FILES
|
||||
|
||||
filegroup(
|
||||
name = "srcs",
|
||||
srcs = SRCS,
|
||||
)
|
||||
|
||||
NPM_MODULE_EXTRA_FILES = [
|
||||
"package.json",
|
||||
]
|
||||
|
||||
RUNTIME_DEPS = [
|
||||
"@npm//@hapi/hapi",
|
||||
"@npm//node-fetch",
|
||||
"//packages/kbn-config",
|
||||
"//packages/kbn-config-mocks",
|
||||
"//packages/kbn-config-schema",
|
||||
"//packages/kbn-logging-mocks",
|
||||
"//packages/kbn-server-http-tools",
|
||||
"//packages/kbn-utils",
|
||||
"//packages/core/logging/core-logging-server-internal",
|
||||
]
|
||||
|
||||
TYPES_DEPS = [
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/jest",
|
||||
"@npm//@types/hapi__hapi",
|
||||
"@npm//@types/node-fetch",
|
||||
"@npm//moment",
|
||||
"//packages/core/base/core-base-server-internal:npm_module_types",
|
||||
"//packages/core/logging/core-logging-server-internal:npm_module_types",
|
||||
"//packages/kbn-config:npm_module_types",
|
||||
"//packages/kbn-config-mocks:npm_module_types",
|
||||
"//packages/kbn-config-schema:npm_module_types",
|
||||
"//packages/kbn-logging:npm_module_types",
|
||||
"//packages/kbn-logging-mocks:npm_module_types",
|
||||
"//packages/kbn-server-http-tools:npm_module_types",
|
||||
"//packages/kbn-utils:npm_module_types",
|
||||
"//packages/kbn-utility-types:npm_module_types",
|
||||
"//packages/kbn-utility-types-jest:npm_module_types",
|
||||
]
|
||||
|
||||
jsts_transpiler(
|
||||
name = "target_node",
|
||||
srcs = SRCS,
|
||||
build_pkg_name = package_name(),
|
||||
)
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
deps = [
|
||||
"//:tsconfig.base.json",
|
||||
"//:tsconfig.bazel.json",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "tsc_types",
|
||||
args = ['--pretty'],
|
||||
srcs = SRCS,
|
||||
deps = TYPES_DEPS,
|
||||
declaration = True,
|
||||
emit_declaration_only = True,
|
||||
out_dir = "target_types",
|
||||
root_dir = ".",
|
||||
tsconfig = ":tsconfig",
|
||||
)
|
||||
|
||||
js_library(
|
||||
name = PKG_DIRNAME,
|
||||
srcs = NPM_MODULE_EXTRA_FILES,
|
||||
deps = RUNTIME_DEPS + [":target_node"],
|
||||
package_name = PKG_REQUIRE_NAME,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
js_library(
|
||||
name = "npm_module_types",
|
||||
srcs = NPM_MODULE_EXTRA_FILES,
|
||||
deps = RUNTIME_DEPS + [":target_node", ":tsc_types"],
|
||||
package_name = PKG_REQUIRE_NAME,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
pkg_npm(
|
||||
name = "npm_module",
|
||||
deps = [":" + PKG_DIRNAME],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "build",
|
||||
srcs = [":npm_module"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
pkg_npm(
|
||||
name = "build_types",
|
||||
deps = [":npm_module_types"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
77
packages/kbn-health-gateway-server/README.md
Normal file
77
packages/kbn-health-gateway-server/README.md
Normal file
|
@ -0,0 +1,77 @@
|
|||
# @kbn/health-gateway-server
|
||||
|
||||
This package runs a small server called the Health Gateway, which exists to query
|
||||
the status APIs of multiple Kibana instances and return an aggregated result.
|
||||
|
||||
This is used by the Elastic Cloud infrastructure to run two different Kibana processes
|
||||
with different `node.roles`: one process for handling UI requests, and one for background
|
||||
tasks.
|
||||
|
||||
## Configuration
|
||||
|
||||
Similar to Kibana, the gateway has a yml configuration file that it reads from. By default
|
||||
this lives alongside the `kibana.yml` at `<REPO_ROOT>/config/gateway.yml`. Like Kibana,
|
||||
you can provide a `-c` or `--config` CLI argument to override the location of the config
|
||||
file.
|
||||
|
||||
For example:
|
||||
```bash
|
||||
$ yarn start --config /path/to/some/other/config.yml
|
||||
```
|
||||
Here is a sample configuration file recommended for use in development:
|
||||
|
||||
```yaml
|
||||
# config/gateway.yml
|
||||
server:
|
||||
port: 3000
|
||||
host: 'localhost'
|
||||
ssl:
|
||||
enabled: true
|
||||
# Using Kibana test certs
|
||||
key: /path/to/packages/kbn-dev-utils/certs/kibana.key
|
||||
certificate: /path/to/packages/kbn-dev-utils/certs/kibana.crt
|
||||
certificateAuthorities: /path/to/packages/kbn-dev-utils/certs/ca.crt
|
||||
|
||||
kibana:
|
||||
hosts:
|
||||
- 'https://localhost:5605'
|
||||
- 'https://localhost:5606'
|
||||
ssl:
|
||||
# Using Kibana test certs
|
||||
certificate: /path/to/packages/kbn-dev-utils/certs/kibana.crt
|
||||
certificateAuthorities: /path/to/packages/kbn-dev-utils/certs/ca.crt
|
||||
verificationMode: certificate
|
||||
|
||||
logging:
|
||||
root:
|
||||
appenders: ['console']
|
||||
level: 'all'
|
||||
```
|
||||
|
||||
Note that the gateway supports the same logging configuration as Kibana, including
|
||||
all of the same appenders.
|
||||
|
||||
## Development & Testing
|
||||
|
||||
To run this locally, first you need to create a `config/gateway.yml` file. There's a
|
||||
`docker-compose.yml` intended for development, which will run Elasticsearch and
|
||||
two different Kibana instances for testing. Before using it, you'll want to create
|
||||
a `.env` file:
|
||||
|
||||
```bash
|
||||
# From the /packages/kbn-health-gateway-server/scripts directory
|
||||
$ cp .env.example .env
|
||||
# (modify the .env settings if desired)
|
||||
$ docker-compose up
|
||||
```
|
||||
|
||||
This will automatically run Kibana on the ports from the sample `gateway.yml`
|
||||
above (5605-5606).
|
||||
|
||||
Once you have your `gateway.yml` and have started docker-compose, you can run the
|
||||
server from the `/packages/kbn-health-gateway-server` directory with `yarn start`. Then you should
|
||||
be able to make requests to the `/api/status` endpoint:
|
||||
|
||||
```bash
|
||||
$ curl "https://localhost:3000/api/status"
|
||||
```
|
9
packages/kbn-health-gateway-server/index.ts
Normal file
9
packages/kbn-health-gateway-server/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { bootstrap } from './src';
|
13
packages/kbn-health-gateway-server/jest.config.js
Normal file
13
packages/kbn-health-gateway-server/jest.config.js
Normal 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-health-gateway-server'],
|
||||
};
|
7
packages/kbn-health-gateway-server/kibana.jsonc
Normal file
7
packages/kbn-health-gateway-server/kibana.jsonc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-server",
|
||||
"id": "@kbn/health-gateway-server",
|
||||
"owner": "@elastic/kibana-core",
|
||||
"runtimeDeps": [],
|
||||
"typeDeps": []
|
||||
}
|
12
packages/kbn-health-gateway-server/package.json
Normal file
12
packages/kbn-health-gateway-server/package.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "@kbn/health-gateway-server",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "./target_node/index.js",
|
||||
"author": "Kibana Core",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"scripts": {
|
||||
"start": "node ../../bazel-bin/packages/kbn-health-gateway-server/target_node/scripts/init.js"
|
||||
},
|
||||
"types": "./target_types/index.d.ts"
|
||||
}
|
22
packages/kbn-health-gateway-server/scripts/.env.example
Normal file
22
packages/kbn-health-gateway-server/scripts/.env.example
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Password for the 'elastic' user (at least 6 characters)
|
||||
ELASTIC_PASSWORD=changeme
|
||||
|
||||
# Password for the 'kibana_system' user (at least 6 characters)
|
||||
KIBANA_PASSWORD=changeme
|
||||
|
||||
# Version of Elastic products
|
||||
STACK_VERSION=8.4.0
|
||||
|
||||
# Set to 'basic' or 'trial' to automatically start the 30-day trial
|
||||
LICENSE=basic
|
||||
|
||||
# Port to expose Elasticsearch HTTP API to the host
|
||||
ES_PORT=9205
|
||||
|
||||
# Ports to expose Kibana to the host
|
||||
KIBANA_01_PORT=5605
|
||||
KIBANA_02_PORT=5606
|
||||
|
||||
# Increase or decrease based on the available host memory (in bytes)
|
||||
MEM_LIMIT=2147483648
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
setup:
|
||||
depends_on:
|
||||
elasticsearch:
|
||||
condition: service_healthy
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
|
||||
# Adapted from https://github.com/elastic/elasticsearch/blob/main/docs/reference/setup/install/docker/docker-compose.yml
|
||||
command: >
|
||||
bash -c '
|
||||
echo "Setting kibana_system password";
|
||||
until curl -s -X POST -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" http://elasticsearch:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done;
|
||||
echo "All done!";
|
||||
'
|
||||
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
|
||||
volumes:
|
||||
- ../../kbn-dev-utils/certs:/usr/share/elasticsearch/config/certs
|
||||
ports:
|
||||
- ${ES_PORT}:9200
|
||||
environment:
|
||||
node.name: elasticsearch
|
||||
cluster.name: health-gateway-test-cluster
|
||||
discovery.type: single-node
|
||||
ELASTIC_PASSWORD: ${ELASTIC_PASSWORD}
|
||||
bootstrap.memory_lock: true
|
||||
xpack.security.enabled: true
|
||||
xpack.license.self_generated.type: ${LICENSE}
|
||||
mem_limit: ${MEM_LIMIT}
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"curl -s http://localhost:9200 | grep -q 'missing authentication credentials'",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
|
||||
kbn01:
|
||||
depends_on:
|
||||
elasticsearch:
|
||||
condition: service_healthy
|
||||
image: docker.elastic.co/kibana/kibana:${STACK_VERSION}
|
||||
volumes:
|
||||
- ../../kbn-dev-utils/certs:/usr/share/kibana/config/certs
|
||||
ports:
|
||||
- ${KIBANA_01_PORT}:5601
|
||||
environment:
|
||||
SERVERNAME: kbn01
|
||||
NODE_ROLES: '["ui"]'
|
||||
STATUS_ALLOWANONYMOUS: true
|
||||
SERVER_SSL_ENABLED: true
|
||||
SERVER_SSL_KEY: config/certs/kibana.key
|
||||
SERVER_SSL_CERTIFICATE: config/certs/kibana.crt
|
||||
SERVER_SSL_CERTIFICATEAUTHORITIES: config/certs/ca.crt
|
||||
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
|
||||
ELASTICSEARCH_USERNAME: kibana_system
|
||||
ELASTICSEARCH_PASSWORD: ${KIBANA_PASSWORD}
|
||||
mem_limit: ${MEM_LIMIT}
|
||||
|
||||
kbn02:
|
||||
depends_on:
|
||||
elasticsearch:
|
||||
condition: service_healthy
|
||||
image: docker.elastic.co/kibana/kibana:${STACK_VERSION}
|
||||
volumes:
|
||||
- ../../kbn-dev-utils/certs:/usr/share/kibana/config/certs
|
||||
ports:
|
||||
- ${KIBANA_02_PORT}:5601
|
||||
environment:
|
||||
SERVERNAME: kbn02
|
||||
NODE_ROLES: '["background_tasks"]'
|
||||
STATUS_ALLOWANONYMOUS: true
|
||||
SERVER_SSL_ENABLED: true
|
||||
SERVER_SSL_KEY: config/certs/kibana.key
|
||||
SERVER_SSL_CERTIFICATE: config/certs/kibana.crt
|
||||
SERVER_SSL_CERTIFICATEAUTHORITIES: config/certs/ca.crt
|
||||
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
|
||||
ELASTICSEARCH_USERNAME: kibana_system
|
||||
ELASTICSEARCH_PASSWORD: ${KIBANA_PASSWORD}
|
||||
mem_limit: ${MEM_LIMIT}
|
||||
|
13
packages/kbn-health-gateway-server/scripts/init.ts
Normal file
13
packages/kbn-health-gateway-server/scripts/init.ts
Normal 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 { bootstrap } from '../src';
|
||||
|
||||
(async () => {
|
||||
await bootstrap();
|
||||
})();
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 {
|
||||
createTestEnv,
|
||||
configServiceMock as configMock,
|
||||
rawConfigServiceMock as rawMock,
|
||||
} from '@kbn/config-mocks';
|
||||
|
||||
export const envCreateDefaultMock = jest.fn().mockImplementation(() => createTestEnv);
|
||||
export const configServiceMock = jest.fn().mockImplementation(() => configMock.create());
|
||||
export const rawConfigServiceMock = jest.fn().mockImplementation(() => rawMock.create());
|
||||
jest.doMock('@kbn/config', () => ({
|
||||
Env: {
|
||||
createDefault: envCreateDefaultMock,
|
||||
},
|
||||
ConfigService: configServiceMock,
|
||||
RawConfigService: rawConfigServiceMock,
|
||||
}));
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 {
|
||||
envCreateDefaultMock,
|
||||
configServiceMock,
|
||||
rawConfigServiceMock,
|
||||
} from './config_service.test.mocks';
|
||||
import { loggerMock, MockedLogger } from '@kbn/logging-mocks';
|
||||
import { fromRoot } from '@kbn/utils';
|
||||
import { getConfigService } from './config_service';
|
||||
|
||||
const DEFAULT_CONFIG_PATH = fromRoot('config/gateway.yml');
|
||||
|
||||
describe('getConfigService', () => {
|
||||
let logger: MockedLogger;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = loggerMock.create();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
envCreateDefaultMock.mockClear();
|
||||
configServiceMock.mockClear();
|
||||
rawConfigServiceMock.mockClear();
|
||||
});
|
||||
|
||||
test('instantiates RawConfigService with the default config path', () => {
|
||||
const oldArgv = process.argv;
|
||||
process.argv = [];
|
||||
|
||||
getConfigService({ logger });
|
||||
expect(rawConfigServiceMock).toHaveBeenCalledTimes(1);
|
||||
expect(rawConfigServiceMock).toHaveBeenCalledWith([DEFAULT_CONFIG_PATH]);
|
||||
|
||||
process.argv = oldArgv;
|
||||
});
|
||||
|
||||
test('instantiates RawConfigService with a custom config path provided via -c flag', () => {
|
||||
const oldArgv = process.argv;
|
||||
process.argv = ['-a', 'bc', '-c', 'a/b/c.yml', '-x', 'yz'];
|
||||
|
||||
getConfigService({ logger });
|
||||
|
||||
expect(rawConfigServiceMock).toHaveBeenCalledTimes(1);
|
||||
expect(rawConfigServiceMock).toHaveBeenCalledWith(['a/b/c.yml']);
|
||||
|
||||
process.argv = oldArgv;
|
||||
});
|
||||
|
||||
test('instantiates RawConfigService with a custom config path provided via --config flag', () => {
|
||||
const oldArgv = process.argv;
|
||||
process.argv = ['-a', 'bc', '--config', 'a/b/c.yml', '-x', 'yz'];
|
||||
|
||||
getConfigService({ logger });
|
||||
|
||||
expect(rawConfigServiceMock).toHaveBeenCalledTimes(1);
|
||||
expect(rawConfigServiceMock).toHaveBeenCalledWith(['a/b/c.yml']);
|
||||
|
||||
process.argv = oldArgv;
|
||||
});
|
||||
|
||||
test('creates default env', async () => {
|
||||
const oldArgv = process.argv;
|
||||
process.argv = [];
|
||||
|
||||
getConfigService({ logger });
|
||||
expect(envCreateDefaultMock).toHaveBeenCalledTimes(1);
|
||||
expect(envCreateDefaultMock.mock.calls[0][1].configs).toEqual([DEFAULT_CONFIG_PATH]);
|
||||
|
||||
process.argv = oldArgv;
|
||||
});
|
||||
|
||||
test('attempts to load the config', () => {
|
||||
const mockLoadConfig = jest.fn();
|
||||
rawConfigServiceMock.mockImplementationOnce(() => ({
|
||||
loadConfig: mockLoadConfig,
|
||||
}));
|
||||
getConfigService({ logger });
|
||||
expect(mockLoadConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('instantiates the config service', async () => {
|
||||
getConfigService({ logger });
|
||||
expect(configServiceMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { fromRoot, REPO_ROOT } from '@kbn/utils';
|
||||
import type { LoggerFactory } from '@kbn/logging';
|
||||
import { ConfigService as KbnConfigService, CliArgs, Env, RawConfigService } from '@kbn/config';
|
||||
import { getArgValues } from './read_argv';
|
||||
|
||||
const CONFIG_CLI_FLAGS = ['-c', '--config'];
|
||||
const DEFAULT_CONFIG_PATH = fromRoot('config/gateway.yml');
|
||||
|
||||
// These `cliArgs` are required by `Env` for use with Kibana,
|
||||
// however they have no effect on the health gateway.
|
||||
const KIBANA_CLI_ARGS: CliArgs = {
|
||||
dev: false,
|
||||
silent: false,
|
||||
watch: false,
|
||||
basePath: false,
|
||||
disableOptimizer: true,
|
||||
cache: false,
|
||||
dist: false,
|
||||
oss: false,
|
||||
runExamples: false,
|
||||
};
|
||||
|
||||
export function getConfigService({ logger }: { logger: LoggerFactory }) {
|
||||
const configPathOverride = getArgValues(process.argv, CONFIG_CLI_FLAGS);
|
||||
const configPath = configPathOverride.length ? configPathOverride : [DEFAULT_CONFIG_PATH];
|
||||
|
||||
const rawConfigService = new RawConfigService(configPath);
|
||||
rawConfigService.loadConfig();
|
||||
|
||||
const env = Env.createDefault(REPO_ROOT, {
|
||||
configs: configPath,
|
||||
cliArgs: KIBANA_CLI_ARGS,
|
||||
});
|
||||
|
||||
return new KbnConfigService(rawConfigService, env, logger);
|
||||
}
|
9
packages/kbn-health-gateway-server/src/config/index.ts
Normal file
9
packages/kbn-health-gateway-server/src/config/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { getConfigService } from './config_service';
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { getArgValues } from './read_argv';
|
||||
|
||||
describe('getArgValues', () => {
|
||||
it('retrieve the arg value from the provided argv arguments', () => {
|
||||
const argValues = getArgValues(
|
||||
['--config', 'my-config', '--foo', '-b', 'bar', '--config', 'other-config', '--baz'],
|
||||
'--config'
|
||||
);
|
||||
expect(argValues).toEqual(['my-config', 'other-config']);
|
||||
});
|
||||
|
||||
it('accept aliases', () => {
|
||||
const argValues = getArgValues(
|
||||
['--config', 'my-config', '--foo', '-b', 'bar', '-c', 'other-config', '--baz'],
|
||||
['--config', '-c']
|
||||
);
|
||||
expect(argValues).toEqual(['my-config', 'other-config']);
|
||||
});
|
||||
|
||||
it('returns an empty array when the arg is not found', () => {
|
||||
const argValues = getArgValues(
|
||||
['--config', 'my-config', '--foo', '-b', 'bar', '-c', 'other-config', '--baz'],
|
||||
'--unicorn'
|
||||
);
|
||||
expect(argValues).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores the flag when no value is provided', () => {
|
||||
const argValues = getArgValues(
|
||||
['-c', 'my-config', '--foo', '-b', 'bar', '--config'],
|
||||
['--config', '-c']
|
||||
);
|
||||
expect(argValues).toEqual(['my-config']);
|
||||
});
|
||||
});
|
21
packages/kbn-health-gateway-server/src/config/read_argv.ts
Normal file
21
packages/kbn-health-gateway-server/src/config/read_argv.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Borrowed from @kbn/apm-config-loader.
|
||||
*/
|
||||
export const getArgValues = (argv: string[], flag: string | string[]): string[] => {
|
||||
const flags = typeof flag === 'string' ? [flag] : flag;
|
||||
const values: string[] = [];
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
if (flags.includes(argv[i]) && argv[i + 1]) {
|
||||
values.push(argv[++i]);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
};
|
67
packages/kbn-health-gateway-server/src/index.ts
Normal file
67
packages/kbn-health-gateway-server/src/index.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { ServiceConfigDescriptor } from '@kbn/core-base-server-internal';
|
||||
import {
|
||||
config as loggingConfig,
|
||||
LoggingSystem,
|
||||
LoggingConfigType,
|
||||
} from '@kbn/core-logging-server-internal';
|
||||
import { getConfigService } from './config';
|
||||
import { config as kibanaConfig, KibanaService } from './kibana';
|
||||
import { config as serverConfig, Server, ServerStart } from './server';
|
||||
|
||||
export async function bootstrap() {
|
||||
const loggingSystem = new LoggingSystem();
|
||||
const logger = loggingSystem.asLoggerFactory();
|
||||
const configService = getConfigService({ logger });
|
||||
|
||||
const configDescriptors: ServiceConfigDescriptor[] = [loggingConfig, kibanaConfig, serverConfig];
|
||||
for (const { path, schema } of configDescriptors) {
|
||||
configService.setSchema(path, schema);
|
||||
}
|
||||
|
||||
await configService.validate();
|
||||
|
||||
await loggingSystem.upgrade(configService.atPathSync<LoggingConfigType>('logging'));
|
||||
const log = logger.get('root');
|
||||
|
||||
let server: Server;
|
||||
let serverStart: ServerStart;
|
||||
try {
|
||||
server = new Server({ config: configService, logger });
|
||||
serverStart = await server.start();
|
||||
} catch (e) {
|
||||
log.error(`Failed to start Server: ${e}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let kibanaService: KibanaService;
|
||||
try {
|
||||
kibanaService = new KibanaService({ config: configService, logger });
|
||||
await kibanaService.start({ server: serverStart });
|
||||
} catch (e) {
|
||||
log.error(`Failed to start Kibana service: ${e}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const attemptGracefulShutdown = async (exitCode: number = 0) => {
|
||||
await server.stop();
|
||||
kibanaService.stop();
|
||||
await loggingSystem.stop();
|
||||
process.exit(exitCode);
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', async (err: Error) => {
|
||||
log.error(err);
|
||||
await attemptGracefulShutdown(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => await attemptGracefulShutdown());
|
||||
process.on('SIGTERM', async () => await attemptGracefulShutdown());
|
||||
}
|
10
packages/kbn-health-gateway-server/src/kibana/index.ts
Normal file
10
packages/kbn-health-gateway-server/src/kibana/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { config } from './kibana_config';
|
||||
export { KibanaService } from './kibana_service';
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { config } from './kibana_config';
|
||||
|
||||
describe('kibana config', () => {
|
||||
test('has defaults for config', () => {
|
||||
const configSchema = config.schema;
|
||||
const obj = {
|
||||
hosts: ['http://localhost:5601'],
|
||||
};
|
||||
expect(configSchema.validate(obj)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"hosts": Array [
|
||||
"http://localhost:5601",
|
||||
],
|
||||
"requestTimeout": "PT30S",
|
||||
"ssl": Object {
|
||||
"verificationMode": "full",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
describe('hosts', () => {
|
||||
test('accepts valid hosts', () => {
|
||||
const configSchema = config.schema;
|
||||
const validHosts = ['http://some.host:1234', 'https://some.other.host'];
|
||||
expect(configSchema.validate({ hosts: validHosts })).toEqual(
|
||||
expect.objectContaining({ hosts: validHosts })
|
||||
);
|
||||
});
|
||||
|
||||
test('throws if invalid hosts', () => {
|
||||
const invalidHosts = ['https://localhost:3000', 'abcxyz'];
|
||||
const configSchema = config.schema;
|
||||
expect(() => configSchema.validate({ hosts: invalidHosts })).toThrowError(
|
||||
'[hosts.1]: expected URI with scheme [http|https].'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ssl', () => {
|
||||
test('accepts valid ssl config', () => {
|
||||
const configSchema = config.schema;
|
||||
const valid = {
|
||||
certificate: '/herp/derp',
|
||||
certificateAuthorities: ['/beep/boop'],
|
||||
verificationMode: 'certificate',
|
||||
};
|
||||
expect(
|
||||
configSchema.validate({
|
||||
hosts: ['http://localhost:5601'],
|
||||
ssl: valid,
|
||||
})
|
||||
).toEqual(expect.objectContaining({ ssl: valid }));
|
||||
});
|
||||
|
||||
test('throws if invalid ssl config', () => {
|
||||
const configSchema = config.schema;
|
||||
const hosts = ['http://localhost:5601'];
|
||||
const invalid = {
|
||||
verificationMode: 'nope',
|
||||
};
|
||||
expect(() => configSchema.validate({ hosts, ssl: invalid }))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"[ssl.verificationMode]: types that failed validation:
|
||||
- [ssl.verificationMode.0]: expected value to equal [none]
|
||||
- [ssl.verificationMode.1]: expected value to equal [certificate]
|
||||
- [ssl.verificationMode.2]: expected value to equal [full]"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
109
packages/kbn-health-gateway-server/src/kibana/kibana_config.ts
Normal file
109
packages/kbn-health-gateway-server/src/kibana/kibana_config.ts
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { readFileSync } from 'fs';
|
||||
import type { Duration } from 'moment';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { ServiceConfigDescriptor } from '@kbn/core-base-server-internal';
|
||||
|
||||
const hostURISchema = schema.uri({ scheme: ['http', 'https'] });
|
||||
|
||||
const configSchema = schema.object({
|
||||
hosts: schema.arrayOf(hostURISchema, {
|
||||
minSize: 1,
|
||||
}),
|
||||
requestTimeout: schema.duration({ defaultValue: '30s' }),
|
||||
ssl: schema.object({
|
||||
verificationMode: schema.oneOf(
|
||||
[schema.literal('none'), schema.literal('certificate'), schema.literal('full')],
|
||||
{ defaultValue: 'full' }
|
||||
),
|
||||
certificateAuthorities: schema.maybe(
|
||||
schema.oneOf([schema.string(), schema.arrayOf(schema.string(), { minSize: 1 })])
|
||||
),
|
||||
certificate: schema.maybe(schema.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
export type KibanaConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: ServiceConfigDescriptor<KibanaConfigType> = {
|
||||
path: 'kibana' as const,
|
||||
schema: configSchema,
|
||||
};
|
||||
|
||||
export class KibanaConfig {
|
||||
/**
|
||||
* Kibana hosts that the gateway will connect to.
|
||||
*/
|
||||
public readonly hosts: string[];
|
||||
|
||||
/**
|
||||
* Timeout after which HTTP requests to the Kibana hosts will be aborted.
|
||||
*/
|
||||
public readonly requestTimeout: Duration;
|
||||
|
||||
/**
|
||||
* Settings to configure SSL connection between the gateway and Kibana hosts.
|
||||
*/
|
||||
public readonly ssl: SslConfig;
|
||||
|
||||
constructor(rawConfig: KibanaConfigType) {
|
||||
this.hosts = rawConfig.hosts;
|
||||
this.requestTimeout = rawConfig.requestTimeout;
|
||||
|
||||
const { verificationMode } = rawConfig.ssl;
|
||||
const { certificate, certificateAuthorities } = readKeyAndCerts(rawConfig);
|
||||
|
||||
this.ssl = {
|
||||
certificate,
|
||||
certificateAuthorities,
|
||||
verificationMode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface SslConfig {
|
||||
verificationMode: 'none' | 'certificate' | 'full';
|
||||
certificate?: string;
|
||||
certificateAuthorities?: string[];
|
||||
}
|
||||
|
||||
const readKeyAndCerts = (rawConfig: KibanaConfigType) => {
|
||||
let certificate: string | undefined;
|
||||
let certificateAuthorities: string[] | undefined;
|
||||
|
||||
const addCAs = (ca: string[] | undefined) => {
|
||||
if (ca && ca.length) {
|
||||
certificateAuthorities = [...(certificateAuthorities || []), ...ca];
|
||||
}
|
||||
};
|
||||
|
||||
if (rawConfig.ssl.certificate) {
|
||||
certificate = readFile(rawConfig.ssl.certificate);
|
||||
}
|
||||
|
||||
const ca = rawConfig.ssl.certificateAuthorities;
|
||||
if (ca) {
|
||||
const parsed: string[] = [];
|
||||
const paths = Array.isArray(ca) ? ca : [ca];
|
||||
if (paths.length > 0) {
|
||||
for (const path of paths) {
|
||||
parsed.push(readFile(path));
|
||||
}
|
||||
addCAs(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateAuthorities,
|
||||
};
|
||||
};
|
||||
|
||||
const readFile = (file: string) => readFileSync(file, 'utf8');
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const mockReadFileSync = jest.fn();
|
||||
jest.doMock('fs', () => ({ readFileSync: mockReadFileSync }));
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { configServiceMock, IConfigServiceMock } from '@kbn/config-mocks';
|
||||
import { loggerMock, MockedLogger } from '@kbn/logging-mocks';
|
||||
import type { ServerStart } from '../server';
|
||||
import { serverMock } from '../server/server.mock';
|
||||
import { mockReadFileSync } from './kibana_service.test.mocks';
|
||||
import { KibanaService } from './kibana_service';
|
||||
|
||||
describe('KibanaService', () => {
|
||||
let config: IConfigServiceMock;
|
||||
let logger: MockedLogger;
|
||||
let server: ServerStart;
|
||||
const mockConfig = {
|
||||
hosts: ['https://localhost:5605', 'https://localhost:5606'],
|
||||
requestTimeout: '30s',
|
||||
ssl: {
|
||||
certificate: '/herp/derp',
|
||||
certificateAuthorities: '/beep/boop',
|
||||
verificationMode: 'certificate',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockReadFileSync.mockReset();
|
||||
mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`);
|
||||
config = configServiceMock.create();
|
||||
config.atPathSync.mockReturnValue(mockConfig);
|
||||
logger = loggerMock.create();
|
||||
server = serverMock.createStartContract();
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
test(`doesn't return a start contract`, async () => {
|
||||
const kibanaService = new KibanaService({ config, logger });
|
||||
const kibanaStart = await kibanaService.start({ server });
|
||||
expect(kibanaStart).toBeUndefined();
|
||||
});
|
||||
|
||||
test('registers /api/status route with the server', async () => {
|
||||
const kibanaService = new KibanaService({ config, logger });
|
||||
await kibanaService.start({ server });
|
||||
expect(server.addRoute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
path: '/api/status',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { IConfigService } from '@kbn/config';
|
||||
import type { Logger, LoggerFactory } from '@kbn/logging';
|
||||
import { ServerStart } from '../server';
|
||||
import { createStatusRoute } from './routes';
|
||||
|
||||
interface KibanaServiceStartDependencies {
|
||||
server: ServerStart;
|
||||
}
|
||||
|
||||
interface KibanaServiceDependencies {
|
||||
logger: LoggerFactory;
|
||||
config: IConfigService;
|
||||
}
|
||||
|
||||
/**
|
||||
* A service to interact with the configured `kibana.hosts`.
|
||||
*/
|
||||
export class KibanaService {
|
||||
private readonly log: Logger;
|
||||
private readonly config: IConfigService;
|
||||
|
||||
constructor({ logger, config }: KibanaServiceDependencies) {
|
||||
this.log = logger.get('kibana-service');
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async start({ server }: KibanaServiceStartDependencies) {
|
||||
server.addRoute(createStatusRoute({ config: this.config, log: this.log }));
|
||||
}
|
||||
|
||||
stop() {
|
||||
// nothing to do here yet
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { createStatusRoute } from './status';
|
146
packages/kbn-health-gateway-server/src/kibana/routes/status.ts
Normal file
146
packages/kbn-health-gateway-server/src/kibana/routes/status.ts
Normal file
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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 https from 'https';
|
||||
import { URL } from 'url';
|
||||
import type { Request, ResponseToolkit } from '@hapi/hapi';
|
||||
import nodeFetch, { RequestInit, Response } from 'node-fetch';
|
||||
import type { IConfigService } from '@kbn/config';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import type { KibanaConfigType } from '../kibana_config';
|
||||
import { KibanaConfig } from '../kibana_config';
|
||||
|
||||
const HTTPS = 'https:';
|
||||
|
||||
const GATEWAY_STATUS_ROUTE = '/api/status';
|
||||
const KIBANA_STATUS_ROUTE = '/api/status';
|
||||
|
||||
interface StatusRouteDependencies {
|
||||
log: Logger;
|
||||
config: IConfigService;
|
||||
}
|
||||
|
||||
type Fetch = (path: string) => Promise<Response>;
|
||||
|
||||
export function createStatusRoute({ config, log }: StatusRouteDependencies) {
|
||||
const kibanaConfig = new KibanaConfig(config.atPathSync<KibanaConfigType>('kibana'));
|
||||
const fetch = configureFetch(kibanaConfig);
|
||||
|
||||
return {
|
||||
method: 'GET',
|
||||
path: GATEWAY_STATUS_ROUTE,
|
||||
handler: async (req: Request, h: ResponseToolkit) => {
|
||||
const responses = await fetchKibanaStatuses({ fetch, kibanaConfig, log });
|
||||
const { body, statusCode } = mergeStatusResponses(responses);
|
||||
return h.response(body).type('application/json').code(statusCode);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchKibanaStatuses({
|
||||
fetch,
|
||||
kibanaConfig,
|
||||
log,
|
||||
}: {
|
||||
fetch: Fetch;
|
||||
kibanaConfig: KibanaConfig;
|
||||
log: Logger;
|
||||
}) {
|
||||
const requests = await Promise.allSettled(
|
||||
kibanaConfig.hosts.map(async (host) => {
|
||||
log.debug(`Fetching response from ${host}${KIBANA_STATUS_ROUTE}`);
|
||||
const response = fetch(`${host}${KIBANA_STATUS_ROUTE}`).then((res) => res.json());
|
||||
return response;
|
||||
})
|
||||
);
|
||||
|
||||
return requests.map((r, i) => {
|
||||
if (r.status === 'rejected') {
|
||||
log.error(`Unable to retrieve status from ${kibanaConfig.hosts[i]}${KIBANA_STATUS_ROUTE}`);
|
||||
} else {
|
||||
log.info(
|
||||
`Got response from ${kibanaConfig.hosts[i]}${KIBANA_STATUS_ROUTE}: ${JSON.stringify(
|
||||
r.value.status?.overall ? r.value.status.overall : r.value
|
||||
)}`
|
||||
);
|
||||
}
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
function mergeStatusResponses(
|
||||
responses: Array<PromiseFulfilledResult<Response> | PromiseRejectedResult>
|
||||
) {
|
||||
let statusCode = 200;
|
||||
for (const response of responses) {
|
||||
if (response.status === 'rejected') {
|
||||
statusCode = 503;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
body: {}, // Need to determine what response body, if any, we want to include
|
||||
statusCode,
|
||||
};
|
||||
}
|
||||
|
||||
function generateAgentConfig(sslConfig: KibanaConfig['ssl']) {
|
||||
const options: https.AgentOptions = {
|
||||
ca: sslConfig.certificateAuthorities,
|
||||
cert: sslConfig.certificate,
|
||||
};
|
||||
|
||||
const verificationMode = sslConfig.verificationMode;
|
||||
switch (verificationMode) {
|
||||
case 'none':
|
||||
options.rejectUnauthorized = false;
|
||||
break;
|
||||
case 'certificate':
|
||||
options.rejectUnauthorized = true;
|
||||
// by default, NodeJS is checking the server identify
|
||||
options.checkServerIdentity = () => undefined;
|
||||
break;
|
||||
case 'full':
|
||||
options.rejectUnauthorized = true;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown ssl verificationMode: ${verificationMode}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function configureFetch(kibanaConfig: KibanaConfig) {
|
||||
let agent: https.Agent;
|
||||
|
||||
return async (url: string) => {
|
||||
const { protocol } = new URL(url);
|
||||
if (protocol === HTTPS && !agent) {
|
||||
agent = new https.Agent(generateAgentConfig(kibanaConfig.ssl));
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
kibanaConfig.requestTimeout.asMilliseconds()
|
||||
);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
...(protocol === HTTPS && { agent }),
|
||||
signal: controller.signal,
|
||||
};
|
||||
try {
|
||||
const response = await nodeFetch(url, fetchOptions);
|
||||
clearTimeout(timeoutId);
|
||||
return response;
|
||||
} catch (e) {
|
||||
clearTimeout(timeoutId);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
11
packages/kbn-health-gateway-server/src/server/index.ts
Normal file
11
packages/kbn-health-gateway-server/src/server/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { config } from './server_config';
|
||||
export type { ServerStart } from './server';
|
||||
export { Server } from './server';
|
32
packages/kbn-health-gateway-server/src/server/server.mock.ts
Normal file
32
packages/kbn-health-gateway-server/src/server/server.mock.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import type { ServerStart } from './server';
|
||||
import { Server } from './server';
|
||||
|
||||
const createStartMock = (): jest.Mocked<ServerStart> => ({
|
||||
addRoute: jest.fn(),
|
||||
});
|
||||
|
||||
type ServerContract = PublicMethodsOf<Server>;
|
||||
const createMock = (): jest.Mocked<ServerContract> => {
|
||||
const service: jest.Mocked<ServerContract> = {
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
};
|
||||
|
||||
service.start.mockResolvedValue(createStartMock());
|
||||
|
||||
return service;
|
||||
};
|
||||
|
||||
export const serverMock = {
|
||||
create: createMock,
|
||||
createStartContract: createStartMock,
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { sslSchema, getServerOptions, getListenerOptions } from '@kbn/server-http-tools';
|
||||
|
||||
export const hapiStartMock = jest.fn();
|
||||
export const hapiStopMock = jest.fn();
|
||||
export const hapiRouteMock = jest.fn();
|
||||
export const createServerMock = jest.fn().mockImplementation(() => ({
|
||||
info: { uri: 'http://localhost:3000' },
|
||||
start: hapiStartMock,
|
||||
stop: hapiStopMock,
|
||||
route: hapiRouteMock,
|
||||
}));
|
||||
export const getServerOptionsMock = jest.fn().mockImplementation(getServerOptions);
|
||||
export const getListenerOptionsMock = jest.fn().mockImplementation(getListenerOptions);
|
||||
|
||||
jest.doMock('@kbn/server-http-tools', () => ({
|
||||
createServer: createServerMock,
|
||||
getServerOptions: getServerOptionsMock,
|
||||
getListenerOptions: getListenerOptionsMock,
|
||||
sslSchema,
|
||||
SslConfig: jest.fn(),
|
||||
}));
|
95
packages/kbn-health-gateway-server/src/server/server.test.ts
Normal file
95
packages/kbn-health-gateway-server/src/server/server.test.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 {
|
||||
createServerMock,
|
||||
getServerOptionsMock,
|
||||
getListenerOptionsMock,
|
||||
hapiStartMock,
|
||||
hapiStopMock,
|
||||
hapiRouteMock,
|
||||
} from './server.test.mocks';
|
||||
import { configServiceMock, IConfigServiceMock } from '@kbn/config-mocks';
|
||||
import { loggerMock, MockedLogger } from '@kbn/logging-mocks';
|
||||
import { Server } from './server';
|
||||
|
||||
const mockConfig = {
|
||||
port: 3000,
|
||||
host: 'localhost',
|
||||
maxPayload: { getValueInBytes: () => '1048576b' },
|
||||
keepaliveTimeout: 120000,
|
||||
shutdownTimeout: '30s',
|
||||
socketTimeout: 120000,
|
||||
};
|
||||
|
||||
describe('Server', () => {
|
||||
let config: IConfigServiceMock;
|
||||
let logger: MockedLogger;
|
||||
|
||||
beforeEach(() => {
|
||||
config = configServiceMock.create();
|
||||
config.atPathSync.mockReturnValue(mockConfig);
|
||||
logger = loggerMock.create();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
test('logs the uri on server start', async () => {
|
||||
const server = new Server({ config, logger });
|
||||
await server.start();
|
||||
expect(logger.info).toHaveBeenCalledWith('Server running on http://localhost:3000');
|
||||
});
|
||||
|
||||
test('provides the correct server options', async () => {
|
||||
const server = new Server({ config, logger });
|
||||
await server.start();
|
||||
expect(createServerMock).toHaveBeenCalledTimes(1);
|
||||
expect(getServerOptionsMock).toHaveBeenCalledTimes(1);
|
||||
expect(getServerOptionsMock.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({ ...mockConfig })
|
||||
);
|
||||
expect(getListenerOptionsMock.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({ ...mockConfig })
|
||||
);
|
||||
});
|
||||
|
||||
test('starts the Hapi server', async () => {
|
||||
const server = new Server({ config, logger });
|
||||
await server.start();
|
||||
expect(hapiStartMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('addRoute', () => {
|
||||
test('registers route with Hapi', async () => {
|
||||
const server = new Server({ config, logger });
|
||||
const { addRoute } = await server.start();
|
||||
addRoute({
|
||||
method: 'GET',
|
||||
path: '/api/whatever',
|
||||
});
|
||||
expect(hapiRouteMock).toHaveBeenCalledTimes(1);
|
||||
expect(hapiRouteMock).toHaveBeenCalledWith({
|
||||
method: 'GET',
|
||||
path: '/api/whatever',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
test('attempts graceful shutdown', async () => {
|
||||
const server = new Server({ config, logger });
|
||||
await server.start();
|
||||
await server.stop();
|
||||
expect(hapiStopMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
62
packages/kbn-health-gateway-server/src/server/server.ts
Normal file
62
packages/kbn-health-gateway-server/src/server/server.ts
Normal 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 type { Server as HapiServer, ServerRoute as HapiServerRoute } from '@hapi/hapi';
|
||||
import { createServer, getServerOptions, getListenerOptions } from '@kbn/server-http-tools';
|
||||
import type { IConfigService } from '@kbn/config';
|
||||
import type { Logger, LoggerFactory } from '@kbn/logging';
|
||||
import { ServerConfig } from './server_config';
|
||||
import type { ServerConfigType } from './server_config';
|
||||
|
||||
interface ServerDeps {
|
||||
logger: LoggerFactory;
|
||||
config: IConfigService;
|
||||
}
|
||||
|
||||
type RouteDefinition = HapiServerRoute;
|
||||
|
||||
export interface ServerStart {
|
||||
addRoute: (routeDefinition: RouteDefinition) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A very thin wrapper around Hapi, which only exposes the functionality we
|
||||
* need for this app.
|
||||
*/
|
||||
export class Server {
|
||||
private readonly log: Logger;
|
||||
private readonly config: IConfigService;
|
||||
private server?: HapiServer;
|
||||
|
||||
constructor({ logger, config }: ServerDeps) {
|
||||
this.log = logger.get('server');
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async start(): Promise<ServerStart> {
|
||||
const serverConfig = new ServerConfig(this.config.atPathSync<ServerConfigType>('server'));
|
||||
this.server = createServer(getServerOptions(serverConfig), getListenerOptions(serverConfig));
|
||||
|
||||
await this.server.start();
|
||||
this.log.info(`Server running on ${this.server.info.uri}`);
|
||||
|
||||
return {
|
||||
addRoute: (definition) => {
|
||||
this.log.debug(`registering route handler for [${definition.path}]`);
|
||||
this.server!.route(definition);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.log.debug('Attempting graceful shutdown');
|
||||
if (this.server) {
|
||||
await this.server.stop();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* 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 { config, ServerConfig } from './server_config';
|
||||
|
||||
describe('server config', () => {
|
||||
test('has defaults for config', () => {
|
||||
const configSchema = config.schema;
|
||||
const obj = {};
|
||||
expect(configSchema.validate(obj)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"host": "localhost",
|
||||
"keepaliveTimeout": 120000,
|
||||
"maxPayload": ByteSizeValue {
|
||||
"valueInBytes": 1048576,
|
||||
},
|
||||
"port": 3000,
|
||||
"shutdownTimeout": "PT30S",
|
||||
"socketTimeout": 120000,
|
||||
"ssl": Object {
|
||||
"cipherSuites": Array [
|
||||
"TLS_AES_256_GCM_SHA384",
|
||||
"TLS_CHACHA20_POLY1305_SHA256",
|
||||
"TLS_AES_128_GCM_SHA256",
|
||||
"ECDHE-RSA-AES128-GCM-SHA256",
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256",
|
||||
"ECDHE-RSA-AES256-GCM-SHA384",
|
||||
"ECDHE-ECDSA-AES256-GCM-SHA384",
|
||||
"DHE-RSA-AES128-GCM-SHA256",
|
||||
"ECDHE-RSA-AES128-SHA256",
|
||||
"DHE-RSA-AES128-SHA256",
|
||||
"ECDHE-RSA-AES256-SHA384",
|
||||
"DHE-RSA-AES256-SHA384",
|
||||
"ECDHE-RSA-AES256-SHA256",
|
||||
"DHE-RSA-AES256-SHA256",
|
||||
"HIGH",
|
||||
"!aNULL",
|
||||
"!eNULL",
|
||||
"!EXPORT",
|
||||
"!DES",
|
||||
"!RC4",
|
||||
"!MD5",
|
||||
"!PSK",
|
||||
"!SRP",
|
||||
"!CAMELLIA",
|
||||
],
|
||||
"clientAuthentication": "none",
|
||||
"enabled": false,
|
||||
"keystore": Object {},
|
||||
"supportedProtocols": Array [
|
||||
"TLSv1.1",
|
||||
"TLSv1.2",
|
||||
"TLSv1.3",
|
||||
],
|
||||
"truststore": Object {},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
describe('host', () => {
|
||||
const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost', '0.0.0.0'];
|
||||
const invalidHostnames = ['asdf$%^', '0'];
|
||||
|
||||
test('accepts valid hostnames', () => {
|
||||
for (const val of validHostnames) {
|
||||
const { host } = config.schema.validate({ host: val });
|
||||
expect(host).toBe(val);
|
||||
}
|
||||
});
|
||||
|
||||
test('throws if invalid hostname', () => {
|
||||
for (const host of invalidHostnames) {
|
||||
const configSchema = config.schema;
|
||||
expect(() => configSchema.validate({ host })).toThrowError(
|
||||
'[host]: value must be a valid hostname (see RFC 1123).'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('port', () => {
|
||||
test('accepts valid ports', () => {
|
||||
const validPorts = [80, 3000, 5601];
|
||||
for (const val of validPorts) {
|
||||
const { port } = config.schema.validate({ port: val });
|
||||
expect(port).toBe(val);
|
||||
}
|
||||
});
|
||||
|
||||
test('throws if invalid ports', () => {
|
||||
const configSchema = config.schema;
|
||||
expect(() => configSchema.validate({ port: false })).toThrowError(
|
||||
'port]: expected value of type [number] but got [boolean]'
|
||||
);
|
||||
expect(() => configSchema.validate({ port: 'oops' })).toThrowError(
|
||||
'port]: expected value of type [number] but got [string]'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxPayload', () => {
|
||||
test('can specify max payload as string', () => {
|
||||
const obj = {
|
||||
maxPayload: '2mb',
|
||||
};
|
||||
const configValue = config.schema.validate(obj);
|
||||
expect(configValue.maxPayload.getValueInBytes()).toBe(2 * 1024 * 1024);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shutdownTimeout', () => {
|
||||
test('can specify a valid shutdownTimeout', () => {
|
||||
const configValue = config.schema.validate({ shutdownTimeout: '5s' });
|
||||
expect(configValue.shutdownTimeout.asMilliseconds()).toBe(5000);
|
||||
});
|
||||
|
||||
test('can specify a valid shutdownTimeout (lower-edge of 1 second)', () => {
|
||||
const configValue = config.schema.validate({ shutdownTimeout: '1s' });
|
||||
expect(configValue.shutdownTimeout.asMilliseconds()).toBe(1000);
|
||||
});
|
||||
|
||||
test('can specify a valid shutdownTimeout (upper-edge of 2 minutes)', () => {
|
||||
const configValue = config.schema.validate({ shutdownTimeout: '2m' });
|
||||
expect(configValue.shutdownTimeout.asMilliseconds()).toBe(120000);
|
||||
});
|
||||
|
||||
test('should error if below 1s', () => {
|
||||
expect(() => config.schema.validate({ shutdownTimeout: '100ms' })).toThrow(
|
||||
'[shutdownTimeout]: the value should be between 1 second and 2 minutes'
|
||||
);
|
||||
});
|
||||
|
||||
test('should error if over 2 minutes', () => {
|
||||
expect(() => config.schema.validate({ shutdownTimeout: '3m' })).toThrow(
|
||||
'[shutdownTimeout]: the value should be between 1 second and 2 minutes'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with TLS', () => {
|
||||
test('throws if TLS is enabled but `redirectHttpFromPort` is equal to `port`', () => {
|
||||
const configSchema = config.schema;
|
||||
const obj = {
|
||||
port: 1234,
|
||||
ssl: {
|
||||
certificate: '/path/to/certificate',
|
||||
enabled: true,
|
||||
key: '/path/to/key',
|
||||
redirectHttpFromPort: 1234,
|
||||
},
|
||||
};
|
||||
expect(() => configSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"The health gateway does not accept http traffic to [port] when ssl is enabled (only https is allowed), so [ssl.redirectHttpFromPort] cannot be configured to the same value. Both are [1234]."`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('socketTimeout', () => {
|
||||
test('can specify socket timeouts', () => {
|
||||
const obj = {
|
||||
keepaliveTimeout: 1e5,
|
||||
socketTimeout: 5e5,
|
||||
};
|
||||
const { keepaliveTimeout, socketTimeout } = config.schema.validate(obj);
|
||||
expect(keepaliveTimeout).toBe(1e5);
|
||||
expect(socketTimeout).toBe(5e5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cors', () => {
|
||||
test('is always disabled', () => {
|
||||
const configSchema = config.schema;
|
||||
const obj = {};
|
||||
expect(new ServerConfig(configSchema.validate(obj)).cors).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"allowCredentials": false,
|
||||
"allowOrigin": Array [
|
||||
"*",
|
||||
],
|
||||
"enabled": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { Duration } from 'moment';
|
||||
import { schema, TypeOf, ByteSizeValue } from '@kbn/config-schema';
|
||||
import { ServiceConfigDescriptor } from '@kbn/core-base-server-internal';
|
||||
import type { ISslConfig, ICorsConfig, IHttpConfig } from '@kbn/server-http-tools';
|
||||
import { sslSchema, SslConfig } from '@kbn/server-http-tools';
|
||||
|
||||
const configSchema = schema.object(
|
||||
{
|
||||
host: schema.string({
|
||||
defaultValue: 'localhost',
|
||||
hostname: true,
|
||||
}),
|
||||
port: schema.number({
|
||||
defaultValue: 3000,
|
||||
}),
|
||||
maxPayload: schema.byteSize({
|
||||
defaultValue: '1048576b',
|
||||
}),
|
||||
keepaliveTimeout: schema.number({
|
||||
defaultValue: 120000,
|
||||
}),
|
||||
shutdownTimeout: schema.duration({
|
||||
defaultValue: '30s',
|
||||
validate: (duration) => {
|
||||
const durationMs = duration.asMilliseconds();
|
||||
if (durationMs < 1000 || durationMs > 2 * 60 * 1000) {
|
||||
return 'the value should be between 1 second and 2 minutes';
|
||||
}
|
||||
},
|
||||
}),
|
||||
socketTimeout: schema.number({
|
||||
defaultValue: 120000,
|
||||
}),
|
||||
ssl: sslSchema,
|
||||
},
|
||||
{
|
||||
validate: (rawConfig) => {
|
||||
if (
|
||||
rawConfig.ssl.enabled &&
|
||||
rawConfig.ssl.redirectHttpFromPort !== undefined &&
|
||||
rawConfig.ssl.redirectHttpFromPort === rawConfig.port
|
||||
) {
|
||||
return (
|
||||
'The health gateway does not accept http traffic to [port] when ssl is ' +
|
||||
'enabled (only https is allowed), so [ssl.redirectHttpFromPort] ' +
|
||||
`cannot be configured to the same value. Both are [${rawConfig.port}].`
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export type ServerConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: ServiceConfigDescriptor<ServerConfigType> = {
|
||||
path: 'server' as const,
|
||||
schema: configSchema,
|
||||
};
|
||||
|
||||
export class ServerConfig implements IHttpConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
maxPayload: ByteSizeValue;
|
||||
keepaliveTimeout: number;
|
||||
shutdownTimeout: Duration;
|
||||
socketTimeout: number;
|
||||
ssl: ISslConfig;
|
||||
cors: ICorsConfig;
|
||||
|
||||
constructor(rawConfig: ServerConfigType) {
|
||||
this.host = rawConfig.host;
|
||||
this.port = rawConfig.port;
|
||||
this.maxPayload = rawConfig.maxPayload;
|
||||
this.keepaliveTimeout = rawConfig.keepaliveTimeout;
|
||||
this.shutdownTimeout = rawConfig.shutdownTimeout;
|
||||
this.socketTimeout = rawConfig.socketTimeout;
|
||||
this.ssl = new SslConfig(rawConfig.ssl);
|
||||
this.cors = {
|
||||
enabled: false,
|
||||
allowCredentials: false,
|
||||
allowOrigin: ['*'],
|
||||
};
|
||||
}
|
||||
}
|
16
packages/kbn-health-gateway-server/tsconfig.json
Normal file
16
packages/kbn-health-gateway-server/tsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../../tsconfig.bazel.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "target_types",
|
||||
"stripInternal": false,
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
]
|
||||
}
|
26
src/cli_health_gateway/cli_health_gateway.ts
Normal file
26
src/cli_health_gateway/cli_health_gateway.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { Command } from 'commander';
|
||||
import { kibanaPackageJson } from '@kbn/utils';
|
||||
import { bootstrap } from '@kbn/health-gateway-server';
|
||||
|
||||
const program = new Command('bin/kibana-health-gateway');
|
||||
|
||||
program
|
||||
.version(kibanaPackageJson.version)
|
||||
.description(
|
||||
'This command starts up a health gateway server that can be ' +
|
||||
'configured to send requests to multiple Kibana instances'
|
||||
)
|
||||
.option('-c, --config', 'Path to a gateway.yml configuration file')
|
||||
.action(async () => {
|
||||
return await bootstrap();
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
10
src/cli_health_gateway/dev.js
Normal file
10
src/cli_health_gateway/dev.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require('../setup_node_env');
|
||||
require('./cli_health_gateway');
|
10
src/cli_health_gateway/dist.js
Normal file
10
src/cli_health_gateway/dist.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require('../setup_node_env/dist');
|
||||
require('./cli_health_gateway');
|
15
src/cli_health_gateway/tsconfig.json
Normal file
15
src/cli_health_gateway/tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target/types",
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
},
|
||||
"include": [
|
||||
"*.js",
|
||||
"*.ts",
|
||||
],
|
||||
"kbn_references": [
|
||||
{ "path": "../cli/tsconfig.json" },
|
||||
]
|
||||
}
|
29
src/dev/build/tasks/bin/scripts/kibana-health-gateway
Executable file
29
src/dev/build/tasks/bin/scripts/kibana-health-gateway
Executable file
|
@ -0,0 +1,29 @@
|
|||
#!/bin/sh
|
||||
SCRIPT=$0
|
||||
|
||||
# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path.
|
||||
while [ -h "$SCRIPT" ] ; do
|
||||
ls=$(ls -ld "$SCRIPT")
|
||||
# Drop everything prior to ->
|
||||
link=$(expr "$ls" : '.*-> \(.*\)$')
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
SCRIPT="$link"
|
||||
else
|
||||
SCRIPT=$(dirname "$SCRIPT")/"$link"
|
||||
fi
|
||||
done
|
||||
|
||||
DIR="$(dirname "${SCRIPT}")/.."
|
||||
CONFIG_DIR=${KBN_PATH_CONF:-"$DIR/config"}
|
||||
NODE="${DIR}/node/bin/node"
|
||||
test -x "$NODE"
|
||||
if [ ! -x "$NODE" ]; then
|
||||
echo "unable to find usable node.js executable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "${CONFIG_DIR}/node.options" ]; then
|
||||
KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)"
|
||||
fi
|
||||
|
||||
NODE_OPTIONS="$KBN_NODE_OPTS $NODE_OPTIONS" NODE_ENV=production exec "${NODE}" "${DIR}/src/cli_health_gateway/dist" "$@"
|
36
src/dev/build/tasks/bin/scripts/kibana-health-gateway.bat
Executable file
36
src/dev/build/tasks/bin/scripts/kibana-health-gateway.bat
Executable file
|
@ -0,0 +1,36 @@
|
|||
@echo off
|
||||
|
||||
SETLOCAL ENABLEDELAYEDEXPANSION
|
||||
|
||||
set SCRIPT_DIR=%~dp0
|
||||
for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI
|
||||
|
||||
set NODE=%DIR%\node\node.exe
|
||||
set NODE_ENV=production
|
||||
|
||||
If Not Exist "%NODE%" (
|
||||
Echo unable to find usable node.js executable.
|
||||
Exit /B 1
|
||||
)
|
||||
|
||||
set CONFIG_DIR=%KBN_PATH_CONF%
|
||||
If ["%KBN_PATH_CONF%"] == [] (
|
||||
set "CONFIG_DIR=%DIR%\config"
|
||||
)
|
||||
|
||||
IF EXIST "%CONFIG_DIR%\node.options" (
|
||||
for /F "usebackq eol=# tokens=*" %%i in ("%CONFIG_DIR%\node.options") do (
|
||||
If [!NODE_OPTIONS!] == [] (
|
||||
set "NODE_OPTIONS=%%i"
|
||||
) Else (
|
||||
set "NODE_OPTIONS=!NODE_OPTIONS! %%i"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
TITLE Health Gateway
|
||||
"%NODE%" "%DIR%\src\cli_health_gateway\dist" %*
|
||||
|
||||
:finally
|
||||
|
||||
ENDLOCAL
|
|
@ -7,6 +7,7 @@
|
|||
- assert_encryption_keys_cli
|
||||
- assert_plugin_cli
|
||||
- assert_setup_cli
|
||||
- assert_health_gateway_cli
|
||||
- assert_verification_code_cli
|
||||
- assert_kibana_yml
|
||||
- assert_kibana_listening
|
||||
|
|
13
test/package/roles/assert_health_gateway_cli/tasks/main.yml
Normal file
13
test/package/roles/assert_health_gateway_cli/tasks/main.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
- name: "--help"
|
||||
become: true
|
||||
command:
|
||||
cmd: /usr/share/kibana/bin/kibana-health-gateway --help
|
||||
register: health_gateway_help
|
||||
|
||||
- debug:
|
||||
msg: "{{ health_gateway_help }}"
|
||||
|
||||
- name: assert health-gateway provides help
|
||||
assert:
|
||||
that:
|
||||
- health_gateway_help.failed == false
|
|
@ -7,6 +7,7 @@
|
|||
- assert_encryption_keys_cli
|
||||
- assert_plugin_cli
|
||||
- assert_setup_cli
|
||||
- assert_health_gateway_cli
|
||||
- assert_verification_code_cli
|
||||
- assert_kibana_yml
|
||||
- assert_kibana_listening
|
||||
|
|
|
@ -476,6 +476,8 @@
|
|||
"@kbn/handlebars/*": ["packages/kbn-handlebars/*"],
|
||||
"@kbn/hapi-mocks": ["packages/kbn-hapi-mocks"],
|
||||
"@kbn/hapi-mocks/*": ["packages/kbn-hapi-mocks/*"],
|
||||
"@kbn/health-gateway-server": ["packages/kbn-health-gateway-server"],
|
||||
"@kbn/health-gateway-server/*": ["packages/kbn-health-gateway-server/*"],
|
||||
"@kbn/i18n": ["packages/kbn-i18n"],
|
||||
"@kbn/i18n/*": ["packages/kbn-i18n/*"],
|
||||
"@kbn/i18n-react": ["packages/kbn-i18n-react"],
|
||||
|
|
|
@ -10,4 +10,4 @@
|
|||
"kbn_references": [
|
||||
{ "path": "./src/core/tsconfig.json" },
|
||||
]
|
||||
}
|
||||
}
|
|
@ -3595,6 +3595,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/health-gateway-server@link:bazel-bin/packages/kbn-health-gateway-server":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/home-sample-data-card@link:bazel-bin/packages/home/sample_data_card":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue