|
| 1 | +/* |
| 2 | + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one |
| 3 | + * or more contributor license agreements. Licensed under the Elastic License |
| 4 | + * 2.0 and the Server Side Public License, v 1; you may not use this file except |
| 5 | + * in compliance with, at your election, the Elastic License 2.0 or the Server |
| 6 | + * Side Public License, v 1. |
| 7 | + */ |
| 8 | + |
| 9 | +import type { Subscription } from 'rxjs'; |
| 10 | + |
| 11 | +import type { TypeOf } from '@kbn/config-schema'; |
| 12 | +import type { CorePreboot, Logger, PluginInitializerContext, PrebootPlugin } from 'src/core/server'; |
| 13 | + |
| 14 | +import { ElasticsearchConnectionStatus } from '../common'; |
| 15 | +import type { ConfigSchema, ConfigType } from './config'; |
| 16 | +import { defineRoutes } from './routes'; |
| 17 | + |
| 18 | +export class UserSetupPlugin implements PrebootPlugin { |
| 19 | + readonly #logger: Logger; |
| 20 | + |
| 21 | + #configSubscription?: Subscription; |
| 22 | + #config?: ConfigType; |
| 23 | + readonly #getConfig = () => { |
| 24 | + if (!this.#config) { |
| 25 | + throw new Error('Config is not available.'); |
| 26 | + } |
| 27 | + return this.#config; |
| 28 | + }; |
| 29 | + |
| 30 | + #elasticsearchConnectionStatus = ElasticsearchConnectionStatus.Unknown; |
| 31 | + readonly #getElasticsearchConnectionStatus = () => { |
| 32 | + return this.#elasticsearchConnectionStatus; |
| 33 | + }; |
| 34 | + |
| 35 | + constructor(private readonly initializerContext: PluginInitializerContext) { |
| 36 | + this.#logger = this.initializerContext.logger.get(); |
| 37 | + } |
| 38 | + |
| 39 | + public setup(core: CorePreboot) { |
| 40 | + this.#configSubscription = this.initializerContext.config |
| 41 | + .create<TypeOf<typeof ConfigSchema>>() |
| 42 | + .subscribe((config) => { |
| 43 | + this.#config = config; |
| 44 | + }); |
| 45 | + |
| 46 | + // We shouldn't activate interactive setup mode if we detect that user has already configured |
| 47 | + // Elasticsearch connection manually: either if Kibana system user credentials are specified or |
| 48 | + // user specified non-default host for the Elasticsearch. |
| 49 | + const shouldActiveSetupMode = |
| 50 | + !core.elasticsearch.config.credentialsSpecified && |
| 51 | + core.elasticsearch.config.hosts.length === 1 && |
| 52 | + core.elasticsearch.config.hosts[0] === 'http://localhost:9200'; |
| 53 | + if (!shouldActiveSetupMode) { |
| 54 | + this.#logger.debug( |
| 55 | + 'Interactive setup mode will not be activated since Elasticsearch connection is already configured.' |
| 56 | + ); |
| 57 | + return; |
| 58 | + } |
| 59 | + |
| 60 | + let completeSetup: (result: { shouldReloadConfig: boolean }) => void; |
| 61 | + core.preboot.holdSetupUntilResolved( |
| 62 | + 'Validating Elasticsearch connection configuration…', |
| 63 | + new Promise((resolve) => { |
| 64 | + completeSetup = resolve; |
| 65 | + }) |
| 66 | + ); |
| 67 | + |
| 68 | + // If preliminary check above indicates that user didn't alter default Elasticsearch connection |
| 69 | + // details, it doesn't mean Elasticsearch connection isn't configured. There is a chance that they |
| 70 | + // already disabled security features in Elasticsearch and everything should work by default. |
| 71 | + // We should check if we can connect to Elasticsearch with default configuration to know if we |
| 72 | + // need to activate interactive setup. This check can take some time, so we should register our |
| 73 | + // routes to let interactive setup UI to handle user requests until the check is complete. |
| 74 | + core.elasticsearch |
| 75 | + .createClient('ping') |
| 76 | + .asInternalUser.ping() |
| 77 | + .then( |
| 78 | + (pingResponse) => { |
| 79 | + if (pingResponse.body) { |
| 80 | + this.#logger.debug( |
| 81 | + 'Kibana is already properly configured to connect to Elasticsearch. Interactive setup mode will not be activated.' |
| 82 | + ); |
| 83 | + this.#elasticsearchConnectionStatus = ElasticsearchConnectionStatus.Configured; |
| 84 | + completeSetup({ shouldReloadConfig: false }); |
| 85 | + } else { |
| 86 | + this.#logger.debug( |
| 87 | + 'Kibana is not properly configured to connect to Elasticsearch. Interactive setup mode will be activated.' |
| 88 | + ); |
| 89 | + this.#elasticsearchConnectionStatus = ElasticsearchConnectionStatus.NotConfigured; |
| 90 | + } |
| 91 | + }, |
| 92 | + () => { |
| 93 | + // TODO: we should probably react differently to different errors. 401 - credentials aren't correct, etc. |
| 94 | + // Do we want to constantly ping ES if interactive mode UI isn't active? Just in case user runs Kibana and then |
| 95 | + // configure Elasticsearch so that it can eventually connect to it without any configuration changes? |
| 96 | + this.#elasticsearchConnectionStatus = ElasticsearchConnectionStatus.NotConfigured; |
| 97 | + } |
| 98 | + ); |
| 99 | + |
| 100 | + core.http.registerRoutes('', (router) => { |
| 101 | + defineRoutes({ |
| 102 | + router, |
| 103 | + basePath: core.http.basePath, |
| 104 | + logger: this.#logger.get('routes'), |
| 105 | + getConfig: this.#getConfig.bind(this), |
| 106 | + getElasticsearchConnectionStatus: this.#getElasticsearchConnectionStatus.bind(this), |
| 107 | + }); |
| 108 | + }); |
| 109 | + } |
| 110 | + |
| 111 | + public stop() { |
| 112 | + this.#logger.debug('Stopping plugin'); |
| 113 | + |
| 114 | + if (this.#configSubscription) { |
| 115 | + this.#configSubscription.unsubscribe(); |
| 116 | + this.#configSubscription = undefined; |
| 117 | + } |
| 118 | + } |
| 119 | +} |
0 commit comments