"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigService = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _lodash = require("lodash"); var _rxjs = require("rxjs"); var _operators = require("rxjs/operators"); var _docLinks = require("@kbn/doc-links"); var _config = require("./config"); var _deprecation = require("./deprecation"); var _object_to_config_adapter = require("./object_to_config_adapter"); /* * 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. */ /** @internal */ class ConfigService { /** * Whenever a config if read at a path, we mark that path as 'handled'. We can * then list all unhandled config paths when the startup process is completed. */ constructor(rawConfigProvider, env, logger) { (0, _defineProperty2.default)(this, "log", void 0); (0, _defineProperty2.default)(this, "deprecationLog", void 0); (0, _defineProperty2.default)(this, "docLinks", void 0); (0, _defineProperty2.default)(this, "validated", false); (0, _defineProperty2.default)(this, "config$", void 0); (0, _defineProperty2.default)(this, "lastConfig", void 0); (0, _defineProperty2.default)(this, "deprecatedConfigPaths", new _rxjs.BehaviorSubject({ set: [], unset: [] })); (0, _defineProperty2.default)(this, "handledPaths", new Set()); (0, _defineProperty2.default)(this, "schemas", new Map()); (0, _defineProperty2.default)(this, "deprecations", new _rxjs.BehaviorSubject([])); (0, _defineProperty2.default)(this, "handledDeprecatedConfigs", new Map()); this.rawConfigProvider = rawConfigProvider; this.env = env; this.log = logger.get('config'); this.deprecationLog = logger.get('config', 'deprecation'); this.docLinks = (0, _docLinks.getDocLinks)({ kibanaBranch: env.packageInfo.branch }); this.config$ = (0, _rxjs.combineLatest)([this.rawConfigProvider.getConfig$(), this.deprecations]).pipe((0, _operators.map)(([rawConfig, deprecations]) => { const migrated = (0, _deprecation.applyDeprecations)(rawConfig, deprecations); this.deprecatedConfigPaths.next(migrated.changedPaths); return new _object_to_config_adapter.ObjectToConfigAdapter(migrated.config); }), (0, _operators.tap)(config => { this.lastConfig = config; }), (0, _operators.shareReplay)(1)); } /** * Set config schema for a path and performs its validation */ setSchema(path, schema) { const namespace = pathToString(path); if (this.schemas.has(namespace)) { throw new Error(`Validation schema for [${path}] was already registered.`); } this.schemas.set(namespace, schema); this.markAsHandled(path); } /** * Register a {@link ConfigDeprecationProvider} to be used when validating and migrating the configuration */ addDeprecationProvider(path, provider) { const flatPath = pathToString(path); this.deprecations.next([...this.deprecations.value, ...provider(_deprecation.configDeprecationFactory).map(deprecation => ({ deprecation, path: flatPath, context: this.createDeprecationContext() }))]); } /** * returns all handled deprecated configs */ getHandledDeprecatedConfigs() { return [...this.handledDeprecatedConfigs.entries()]; } /** * Validate the whole configuration and log the deprecation warnings. * * This must be done after every schemas and deprecation providers have been registered. */ async validate(params = { logDeprecations: true }) { const namespaces = [...this.schemas.keys()]; for (let i = 0; i < namespaces.length; i++) { await this.getValidatedConfigAtPath$(namespaces[i]).pipe((0, _operators.first)()).toPromise(); } if (params.logDeprecations) { await this.logDeprecation(); } this.validated = true; } /** * Returns the full config object observable. This is not intended for * "normal use", but for internal features that _need_ access to the full object. */ getConfig$() { return this.config$; } /** * Reads the subset of the config at the specified `path` and validates it * against its registered schema. * * @param path - The path to the desired subset of the config. */ atPath(path) { return this.getValidatedConfigAtPath$(path); } /** * Similar to {@link atPath}, but return the last emitted value synchronously instead of an * observable. * * @param path - The path to the desired subset of the config. */ atPathSync(path) { if (!this.validated) { throw new Error('`atPathSync` called before config was validated'); } const configAtPath = this.lastConfig.get(path); return this.validateAtPath(path, configAtPath); } async isEnabledAtPath(path) { const namespace = pathToString(path); const hasSchema = this.schemas.has(namespace); const config = await (0, _rxjs.firstValueFrom)(this.config$); if (!hasSchema && config.has(path)) { // Throw if there is no schema, but a config exists at the path. throw new Error(`No validation schema has been defined for [${namespace}]`); } const validatedConfig = hasSchema ? await this.atPath(path).pipe((0, _operators.first)()).toPromise() : undefined; const isDisabled = (validatedConfig === null || validatedConfig === void 0 ? void 0 : validatedConfig.enabled) === false; if (isDisabled) { // If the plugin is explicitly disabled, we mark the entire plugin // path as handled, as it's expected that it won't be used. this.markAsHandled(path); return false; } // If the schema exists and the config is explicitly set to true, // _or_ if the `enabled` config is undefined, then we treat the // plugin as enabled. return true; } async getUnusedPaths() { const config = await (0, _rxjs.firstValueFrom)(this.config$); const handledPaths = [...this.handledPaths.values()].map(pathToString); return config.getFlattenedPaths().filter(path => !isPathHandled(path, handledPaths)); } async getUsedPaths() { const config = await (0, _rxjs.firstValueFrom)(this.config$); const handledPaths = [...this.handledPaths.values()].map(pathToString); return config.getFlattenedPaths().filter(path => isPathHandled(path, handledPaths)); } getDeprecatedConfigPath$() { return this.deprecatedConfigPaths.asObservable(); } async logDeprecation() { const rawConfig = await (0, _rxjs.firstValueFrom)(this.rawConfigProvider.getConfig$()); const deprecations = await (0, _rxjs.firstValueFrom)(this.deprecations); const deprecationMessages = []; const createAddDeprecation = domainId => context => { if (!context.silent) { deprecationMessages.push(context.message); } this.markDeprecatedConfigAsHandled(domainId, context); }; (0, _deprecation.applyDeprecations)(rawConfig, deprecations, createAddDeprecation); deprecationMessages.forEach(msg => { this.deprecationLog.warn(msg); }); } validateAtPath(path, config) { const namespace = pathToString(path); const schema = this.schemas.get(namespace); if (!schema) { throw new Error(`No validation schema has been defined for [${namespace}]`); } return schema.validate(config, { dev: this.env.mode.dev, prod: this.env.mode.prod, serverless: this.env.cliArgs.serverless === true, ...this.env.packageInfo }, `config validation of [${namespace}]`); } getValidatedConfigAtPath$(path) { return this.config$.pipe((0, _operators.map)(config => config.get(path)), (0, _operators.distinctUntilChanged)(_lodash.isEqual), (0, _operators.map)(config => this.validateAtPath(path, config))); } markAsHandled(path) { this.log.debug(`Marking config path as handled: ${path}`); this.handledPaths.add(path); } markDeprecatedConfigAsHandled(domainId, config) { const handledDeprecatedConfig = this.handledDeprecatedConfigs.get(domainId) || []; handledDeprecatedConfig.push(config); this.handledDeprecatedConfigs.set(domainId, handledDeprecatedConfig); } createDeprecationContext() { return { branch: this.env.packageInfo.branch, version: this.env.packageInfo.version, docLinks: this.docLinks }; } } exports.ConfigService = ConfigService; const pathToString = path => Array.isArray(path) ? path.join('.') : path; /** * A path is considered 'handled' if it is a subset of any of the already * handled paths. */ const isPathHandled = (path, handledPaths) => handledPaths.some(handledPath => (0, _config.hasConfigPathIntersection)(path, handledPath));