diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts new file mode 100644 index 0000000000000..6a57f667312c6 --- /dev/null +++ b/x-pack/plugins/monitoring/server/config.test.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createConfig, configSchema } from './config'; +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + readFileSync: jest.fn().mockImplementation((path: string) => `contents-of-${path}`), +})); + +describe('config schema', () => { + it('generates proper defaults', () => { + expect(configSchema.validate({})).toMatchInlineSnapshot(` + Object { + "agent": Object { + "interval": "10s", + }, + "cluster_alerts": Object { + "email_notifications": Object { + "email_address": "", + "enabled": true, + }, + "enabled": true, + }, + "elasticsearch": Object { + "apiVersion": "master", + "customHeaders": Object {}, + "healthCheck": Object { + "delay": "PT2.5S", + }, + "ignoreVersionMismatch": false, + "logFetchCount": 10, + "logQueries": false, + "pingTimeout": "PT30S", + "preserveHost": true, + "requestHeadersWhitelist": Array [ + "authorization", + ], + "requestTimeout": "PT30S", + "shardTimeout": "PT30S", + "sniffInterval": false, + "sniffOnConnectionFault": false, + "sniffOnStart": false, + "ssl": Object { + "alwaysPresentCertificate": false, + "keystore": Object {}, + "truststore": Object {}, + "verificationMode": "full", + }, + "startupTimeout": "PT5S", + }, + "enabled": true, + "kibana": Object { + "collection": Object { + "enabled": true, + "interval": 10000, + }, + }, + "licensing": Object { + "api_polling_frequency": "PT30S", + }, + "tests": Object { + "cloud_detector": Object { + "enabled": true, + }, + }, + "ui": Object { + "ccs": Object { + "enabled": true, + }, + "container": Object { + "elasticsearch": Object { + "enabled": false, + }, + "logstash": Object { + "enabled": false, + }, + }, + "elasticsearch": Object { + "apiVersion": "master", + "customHeaders": Object {}, + "healthCheck": Object { + "delay": "PT2.5S", + }, + "ignoreVersionMismatch": false, + "logFetchCount": 10, + "logQueries": false, + "pingTimeout": "PT30S", + "preserveHost": true, + "requestHeadersWhitelist": Array [ + "authorization", + ], + "requestTimeout": "PT30S", + "shardTimeout": "PT30S", + "sniffInterval": false, + "sniffOnConnectionFault": false, + "sniffOnStart": false, + "ssl": Object { + "alwaysPresentCertificate": false, + "keystore": Object {}, + "truststore": Object {}, + "verificationMode": "full", + }, + "startupTimeout": "PT5S", + }, + "enabled": true, + "logs": Object { + "index": "filebeat-*", + }, + "max_bucket_size": 10000, + "min_interval_seconds": 10, + "show_license_expiration": true, + }, + } + `); + }); +}); + +describe('createConfig()', () => { + it('should wrap in Elasticsearch config', async () => { + const config = createConfig( + configSchema.validate({ + elasticsearch: { + hosts: 'http://localhost:9200', + }, + ui: { + elasticsearch: { + hosts: 'http://localhost:9200', + }, + }, + }) + ); + expect(config.elasticsearch.hosts).toEqual(['http://localhost:9200']); + expect(config.ui.elasticsearch.hosts).toEqual(['http://localhost:9200']); + }); + + it('should attempt to read PEM files', async () => { + const ssl = { + certificate: 'packages/kbn-dev-utils/certs/elasticsearch.crt', + key: 'packages/kbn-dev-utils/certs/elasticsearch.key', + certificateAuthorities: 'packages/kbn-dev-utils/certs/ca.crt', + }; + const config = createConfig( + configSchema.validate({ + elasticsearch: { + ssl, + }, + ui: { + elasticsearch: { + ssl, + }, + }, + }) + ); + const expected = expect.objectContaining({ + certificate: 'contents-of-packages/kbn-dev-utils/certs/elasticsearch.crt', + key: 'contents-of-packages/kbn-dev-utils/certs/elasticsearch.key', + certificateAuthorities: ['contents-of-packages/kbn-dev-utils/certs/ca.crt'], + }); + expect(config.elasticsearch.ssl).toEqual(expected); + expect(config.ui.elasticsearch.ssl).toEqual(expected); + }); +}); diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts index 0c051f47d7494..a430be8da6a5f 100644 --- a/x-pack/plugins/monitoring/server/config.ts +++ b/x-pack/plugins/monitoring/server/config.ts @@ -4,92 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ import { schema, TypeOf } from '@kbn/config-schema'; +import { + config as ElasticsearchBaseConfig, + ElasticsearchConfig, +} from '../../../../src/core/server/'; const hostURISchema = schema.uri({ scheme: ['http', 'https'] }); -const DEFAULT_API_VERSION = 'master'; + +const elasticsearchConfigSchema = ElasticsearchBaseConfig.elasticsearch.schema; +type ElasticsearchConfigType = TypeOf; + +export const monitoringElasticsearchConfigSchema = elasticsearchConfigSchema.extends({ + logFetchCount: schema.number({ defaultValue: 10 }), + hosts: schema.maybe(schema.oneOf([hostURISchema, schema.arrayOf(hostURISchema, { minSize: 1 })])), +}); export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), - elasticsearch: schema.object({ - logFetchCount: schema.number({ defaultValue: 10 }), - sniffOnStart: schema.boolean({ defaultValue: false }), - sniffInterval: schema.oneOf([schema.duration(), schema.literal(false)], { - defaultValue: false, - }), - sniffOnConnectionFault: schema.boolean({ defaultValue: false }), - hosts: schema.maybe( - schema.oneOf([hostURISchema, schema.arrayOf(hostURISchema, { minSize: 1 })]) - ), - preserveHost: schema.boolean({ defaultValue: true }), - username: schema.maybe( - schema.conditional( - schema.contextRef('dist'), - false, - schema.string({ - validate: () => {}, - }), - schema.string() - ) - ), - password: schema.maybe(schema.string()), - requestHeadersWhitelist: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { - defaultValue: ['authorization'], - }), - customHeaders: schema.recordOf(schema.string(), schema.string(), { defaultValue: {} }), - shardTimeout: schema.duration({ defaultValue: '30s' }), - requestTimeout: schema.duration({ defaultValue: '30s' }), - pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }), - startupTimeout: schema.duration({ defaultValue: '5s' }), - logQueries: schema.boolean({ defaultValue: false }), - 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()), - key: schema.maybe(schema.string()), - keyPassphrase: schema.maybe(schema.string()), - keystore: schema.object({ - path: schema.maybe(schema.string()), - password: schema.maybe(schema.string()), - }), - truststore: schema.object({ - path: schema.maybe(schema.string()), - password: schema.maybe(schema.string()), - }), - alwaysPresentCertificate: schema.boolean({ defaultValue: false }), - }, - { - validate: (rawConfig) => { - if (rawConfig.key && rawConfig.keystore.path) { - return 'cannot use [key] when [keystore.path] is specified'; - } - if (rawConfig.certificate && rawConfig.keystore.path) { - return 'cannot use [certificate] when [keystore.path] is specified'; - } - }, - } - ), - apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }), - healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }), - ignoreVersionMismatch: schema.conditional( - schema.contextRef('dev'), - false, - schema.boolean({ - validate: (rawValue) => { - if (rawValue === true) { - return '"ignoreVersionMismatch" can only be set to true in development mode'; - } - }, - defaultValue: false, - }), - schema.boolean({ defaultValue: false }) - ), - }), + elasticsearch: monitoringElasticsearchConfigSchema, ui: schema.object({ enabled: schema.boolean({ defaultValue: true }), ccs: schema.object({ @@ -99,93 +31,7 @@ export const configSchema = schema.object({ index: schema.string({ defaultValue: 'filebeat-*' }), }), max_bucket_size: schema.number({ defaultValue: 10000 }), - elasticsearch: schema.object({ - logFetchCount: schema.number({ defaultValue: 10 }), - sniffOnStart: schema.boolean({ defaultValue: false }), - sniffInterval: schema.oneOf([schema.duration(), schema.literal(false)], { - defaultValue: false, - }), - sniffOnConnectionFault: schema.boolean({ defaultValue: false }), - hosts: schema.maybe( - schema.oneOf([hostURISchema, schema.arrayOf(hostURISchema, { minSize: 1 })]) - ), - preserveHost: schema.boolean({ defaultValue: true }), - username: schema.maybe( - schema.conditional( - schema.contextRef('dist'), - false, - schema.string({ - validate: (rawConfig) => { - if (rawConfig === 'elastic') { - return ( - 'value of "elastic" is forbidden. This is a superuser account that can obfuscate ' + - 'privilege-related issues. You should use the "kibana" user instead.' - ); - } - }, - }), - schema.string() - ) - ), - password: schema.maybe(schema.string()), - requestHeadersWhitelist: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { - defaultValue: ['authorization'], - }), - customHeaders: schema.recordOf(schema.string(), schema.string(), { defaultValue: {} }), - shardTimeout: schema.duration({ defaultValue: '30s' }), - requestTimeout: schema.duration({ defaultValue: '30s' }), - pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }), - startupTimeout: schema.duration({ defaultValue: '5s' }), - logQueries: schema.boolean({ defaultValue: false }), - 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()), - key: schema.maybe(schema.string()), - keyPassphrase: schema.maybe(schema.string()), - keystore: schema.object({ - path: schema.maybe(schema.string()), - password: schema.maybe(schema.string()), - }), - truststore: schema.object({ - path: schema.maybe(schema.string()), - password: schema.maybe(schema.string()), - }), - alwaysPresentCertificate: schema.boolean({ defaultValue: false }), - }, - { - validate: (rawConfig) => { - if (rawConfig.key && rawConfig.keystore.path) { - return 'cannot use [key] when [keystore.path] is specified'; - } - if (rawConfig.certificate && rawConfig.keystore.path) { - return 'cannot use [certificate] when [keystore.path] is specified'; - } - }, - } - ), - apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }), - healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }), - ignoreVersionMismatch: schema.conditional( - schema.contextRef('dev'), - false, - schema.boolean({ - validate: (rawValue) => { - if (rawValue === true) { - return '"ignoreVersionMismatch" can only be set to true in development mode'; - } - }, - defaultValue: false, - }), - schema.boolean({ defaultValue: false }) - ), - }), + elasticsearch: monitoringElasticsearchConfigSchema, container: schema.object({ elasticsearch: schema.object({ enabled: schema.boolean({ defaultValue: false }), @@ -227,4 +73,23 @@ export const configSchema = schema.object({ }), }); -export type MonitoringConfig = TypeOf; +export class MonitoringElasticsearchConfig extends ElasticsearchConfig { + public readonly logFetchCount?: number; + + constructor(rawConfig: TypeOf) { + super(rawConfig as ElasticsearchConfigType); + this.logFetchCount = rawConfig.logFetchCount; + } +} + +export type MonitoringConfig = ReturnType; +export function createConfig(config: TypeOf) { + return { + ...config, + elasticsearch: new ElasticsearchConfig(config.elasticsearch as ElasticsearchConfigType), + ui: { + ...config.ui, + elasticsearch: new MonitoringElasticsearchConfig(config.ui.elasticsearch), + }, + }; +} diff --git a/x-pack/plugins/monitoring/server/es_client/instantiate_client.ts b/x-pack/plugins/monitoring/server/es_client/instantiate_client.ts index 280d8aab70300..5b097220352cf 100644 --- a/x-pack/plugins/monitoring/server/es_client/instantiate_client.ts +++ b/x-pack/plugins/monitoring/server/es_client/instantiate_client.ts @@ -3,11 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { Logger, ElasticsearchClientConfig, ICustomClusterClient } from 'kibana/server'; +import { ConfigOptions } from 'elasticsearch'; +import { Logger, ICustomClusterClient } from 'kibana/server'; // @ts-ignore import { monitoringBulk } from '../kibana_monitoring/lib/monitoring_bulk'; -import { MonitoringElasticsearchConfig } from '../types'; +import { MonitoringElasticsearchConfig } from '../config'; /* Provide a dedicated Elasticsearch client for Monitoring * The connection options can be customized for the Monitoring application @@ -15,20 +15,19 @@ import { MonitoringElasticsearchConfig } from '../types'; * Kibana itself is connected to a production cluster. */ +type ESClusterConfig = MonitoringElasticsearchConfig & Pick; + export function instantiateClient( - elasticsearchConfig: any, + elasticsearchConfig: MonitoringElasticsearchConfig, log: Logger, - createClient: ( - type: string, - clientConfig?: Partial - ) => ICustomClusterClient + createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient ) { const isMonitoringCluster = hasMonitoringCluster(elasticsearchConfig); const cluster = createClient('monitoring', { ...(isMonitoringCluster ? elasticsearchConfig : {}), plugins: [monitoringBulk], logQueries: Boolean(elasticsearchConfig.logQueries), - }); + } as ESClusterConfig); const configSource = isMonitoringCluster ? 'monitoring' : 'production'; log.info(`config sourced from: ${configSource} cluster`); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index b639933471635..21af7e36614a5 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -5,9 +5,10 @@ */ import Boom from 'boom'; import { combineLatest } from 'rxjs'; -import { first } from 'rxjs/operators'; +import { first, map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { has, get } from 'lodash'; +import { TypeOf } from '@kbn/config-schema'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { @@ -30,7 +31,7 @@ import { KIBANA_ALERTING_ENABLED, KIBANA_STATS_TYPE_MONITORING, } from '../common/constants'; -import { MonitoringConfig } from './config'; +import { MonitoringConfig, createConfig, configSchema } from './config'; // @ts-ignore import { requireUIRoutes } from './routes'; // @ts-ignore @@ -121,7 +122,9 @@ export class Plugin { async setup(core: CoreSetup, plugins: PluginsSetup) { const [config, legacyConfig] = await combineLatest([ - this.initializerContext.config.create(), + this.initializerContext.config + .create>() + .pipe(map(rawConfig => createConfig(rawConfig))), this.initializerContext.config.legacy.globalConfig$, ]) .pipe(first()) diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 9f30ed1ba5035..9b3725d007fd9 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -15,7 +15,3 @@ export interface MonitoringLicenseService { getSecurityFeature: () => LicenseFeature; stop: () => void; } - -export interface MonitoringElasticsearchConfig { - hosts: string[]; -}