mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
interactive setup compatibility check (#119059)
* use root CA for interactive setup * Use intermediate CA for end-to-end test * Align setup CLI enrollment token param with ES * Add CA private key to certificate * Check cluster version during interactive setup * Fix setup CLI * add updated docs * Added suggestions rom code review * Update docs * Fix paths Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
09a1a74069
commit
fa5ccfedbd
22 changed files with 306 additions and 16 deletions
|
@ -128,6 +128,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. |
|
||||
| [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. |
|
||||
| [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. |
|
||||
| [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md) | |
|
||||
| [PrebootPlugin](./kibana-plugin-core-server.prebootplugin.md) | The interface that should be returned by a <code>PluginInitializer</code> for a <code>preboot</code> plugin. |
|
||||
| [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) | Kibana Preboot Service allows to control the boot flow of Kibana. Preboot plugins can use it to hold the boot until certain condition is met. |
|
||||
| [RegisterDeprecationsConfig](./kibana-plugin-core-server.registerdeprecationsconfig.md) | |
|
||||
|
@ -236,6 +237,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| --- | --- |
|
||||
| [APP\_WRAPPER\_CLASS](./kibana-plugin-core-server.app_wrapper_class.md) | The class name for top level \*and\* nested application wrappers to ensure proper layout |
|
||||
| [kibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) | Set of helpers used to create <code>KibanaResponse</code> to form HTTP response on an incoming request. Should be returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution. |
|
||||
| [pollEsNodesVersion](./kibana-plugin-core-server.pollesnodesversion.md) | |
|
||||
| [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md) | The current "level" of availability of a service. |
|
||||
| [validBodyOutput](./kibana-plugin-core-server.validbodyoutput.md) | The set of valid body.output |
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [pollEsNodesVersion](./kibana-plugin-core-server.pollesnodesversion.md)
|
||||
|
||||
## pollEsNodesVersion variable
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
pollEsNodesVersion: ({ internalClient, log, kibanaVersion, ignoreVersionMismatch, esVersionCheckInterval: healthCheckInterval, }: PollEsNodesVersionOptions) => Observable<NodesVersionCompatibility>
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md) > [esVersionCheckInterval](./kibana-plugin-core-server.pollesnodesversionoptions.esversioncheckinterval.md)
|
||||
|
||||
## PollEsNodesVersionOptions.esVersionCheckInterval property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
esVersionCheckInterval: number;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md) > [ignoreVersionMismatch](./kibana-plugin-core-server.pollesnodesversionoptions.ignoreversionmismatch.md)
|
||||
|
||||
## PollEsNodesVersionOptions.ignoreVersionMismatch property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
ignoreVersionMismatch: boolean;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md) > [internalClient](./kibana-plugin-core-server.pollesnodesversionoptions.internalclient.md)
|
||||
|
||||
## PollEsNodesVersionOptions.internalClient property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
internalClient: ElasticsearchClient;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md) > [kibanaVersion](./kibana-plugin-core-server.pollesnodesversionoptions.kibanaversion.md)
|
||||
|
||||
## PollEsNodesVersionOptions.kibanaVersion property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
kibanaVersion: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md) > [log](./kibana-plugin-core-server.pollesnodesversionoptions.log.md)
|
||||
|
||||
## PollEsNodesVersionOptions.log property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
log: Logger;
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PollEsNodesVersionOptions](./kibana-plugin-core-server.pollesnodesversionoptions.md)
|
||||
|
||||
## PollEsNodesVersionOptions interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface PollEsNodesVersionOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [esVersionCheckInterval](./kibana-plugin-core-server.pollesnodesversionoptions.esversioncheckinterval.md) | number | |
|
||||
| [ignoreVersionMismatch](./kibana-plugin-core-server.pollesnodesversionoptions.ignoreversionmismatch.md) | boolean | |
|
||||
| [internalClient](./kibana-plugin-core-server.pollesnodesversionoptions.internalclient.md) | ElasticsearchClient | |
|
||||
| [kibanaVersion](./kibana-plugin-core-server.pollesnodesversionoptions.kibanaversion.md) | string | |
|
||||
| [log](./kibana-plugin-core-server.pollesnodesversionoptions.log.md) | Logger | |
|
||||
|
|
@ -10,6 +10,7 @@ import { getConfigPath, getDataPath } from '@kbn/utils';
|
|||
import inquirer from 'inquirer';
|
||||
import { duration } from 'moment';
|
||||
import { merge } from 'lodash';
|
||||
import { kibanaPackageJson } from '@kbn/utils';
|
||||
|
||||
import { Logger } from '../core/server';
|
||||
import { ClusterClient } from '../core/server/elasticsearch/client';
|
||||
|
@ -31,7 +32,7 @@ const logger: Logger = {
|
|||
};
|
||||
|
||||
export const kibanaConfigWriter = new KibanaConfigWriter(getConfigPath(), getDataPath(), logger);
|
||||
export const elasticsearch = new ElasticsearchService(logger).setup({
|
||||
export const elasticsearch = new ElasticsearchService(logger, kibanaPackageJson.version).setup({
|
||||
connectionCheckInterval: duration(Infinity),
|
||||
elasticsearch: {
|
||||
createClient: (type, config) => {
|
||||
|
|
|
@ -9,7 +9,10 @@
|
|||
export { ElasticsearchService } from './elasticsearch_service';
|
||||
export { config, configSchema } from './elasticsearch_config';
|
||||
export { ElasticsearchConfig } from './elasticsearch_config';
|
||||
export type { NodesVersionCompatibility } from './version_check/ensure_es_version';
|
||||
export type {
|
||||
NodesVersionCompatibility,
|
||||
PollEsNodesVersionOptions,
|
||||
} from './version_check/ensure_es_version';
|
||||
export type {
|
||||
ElasticsearchServicePreboot,
|
||||
ElasticsearchServiceSetup,
|
||||
|
@ -38,3 +41,4 @@ export type {
|
|||
ElasticsearchErrorDetails,
|
||||
} from './client';
|
||||
export { getRequestDebugMeta, getErrorMessage } from './client';
|
||||
export { pollEsNodesVersion } from './version_check/ensure_es_version';
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { Logger } from '../../logging';
|
||||
import type { ElasticsearchClient } from '../client';
|
||||
|
||||
/** @public */
|
||||
export interface PollEsNodesVersionOptions {
|
||||
internalClient: ElasticsearchClient;
|
||||
log: Logger;
|
||||
|
@ -139,6 +140,7 @@ function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompati
|
|||
);
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const pollEsNodesVersion = ({
|
||||
internalClient,
|
||||
log,
|
||||
|
|
|
@ -115,7 +115,7 @@ export type { CoreId } from './core_context';
|
|||
export { CspConfig } from './csp';
|
||||
export type { ICspConfig } from './csp';
|
||||
|
||||
export { ElasticsearchConfig } from './elasticsearch';
|
||||
export { ElasticsearchConfig, pollEsNodesVersion } from './elasticsearch';
|
||||
export type {
|
||||
ElasticsearchServicePreboot,
|
||||
ElasticsearchServiceSetup,
|
||||
|
@ -137,6 +137,7 @@ export type {
|
|||
DeleteDocumentResponse,
|
||||
ElasticsearchConfigPreboot,
|
||||
ElasticsearchErrorDetails,
|
||||
PollEsNodesVersionOptions,
|
||||
} from './elasticsearch';
|
||||
|
||||
export type { IExternalUrlConfig, IExternalUrlPolicy } from './external_url';
|
||||
|
|
|
@ -1697,6 +1697,23 @@ export enum PluginType {
|
|||
standard = "standard"
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const pollEsNodesVersion: ({ internalClient, log, kibanaVersion, ignoreVersionMismatch, esVersionCheckInterval: healthCheckInterval, }: PollEsNodesVersionOptions) => Observable<NodesVersionCompatibility>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface PollEsNodesVersionOptions {
|
||||
// (undocumented)
|
||||
esVersionCheckInterval: number;
|
||||
// (undocumented)
|
||||
ignoreVersionMismatch: boolean;
|
||||
// (undocumented)
|
||||
internalClient: ElasticsearchClient;
|
||||
// (undocumented)
|
||||
kibanaVersion: string;
|
||||
// (undocumented)
|
||||
log: Logger;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface PrebootPlugin<TSetup = void, TPluginsSetup extends object = object> {
|
||||
// (undocumented)
|
||||
|
|
|
@ -15,3 +15,4 @@ export const ERROR_KIBANA_CONFIG_FAILURE = 'kibana_config_failure';
|
|||
export const ERROR_ENROLL_FAILURE = 'enroll_failure';
|
||||
export const ERROR_CONFIGURE_FAILURE = 'configure_failure';
|
||||
export const ERROR_PING_FAILURE = 'ping_failure';
|
||||
export const ERROR_COMPATIBILITY_FAILURE = 'compatibility_failure';
|
||||
|
|
|
@ -16,5 +16,6 @@ export {
|
|||
ERROR_KIBANA_CONFIG_NOT_WRITABLE,
|
||||
ERROR_OUTSIDE_PREBOOT_STAGE,
|
||||
ERROR_PING_FAILURE,
|
||||
ERROR_COMPATIBILITY_FAILURE,
|
||||
VERIFICATION_CODE_LENGTH,
|
||||
} from './constants';
|
||||
|
|
|
@ -14,6 +14,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import type { IHttpFetchError, ResponseErrorBody } from 'kibana/public';
|
||||
|
||||
import {
|
||||
ERROR_COMPATIBILITY_FAILURE,
|
||||
ERROR_CONFIGURE_FAILURE,
|
||||
ERROR_ELASTICSEARCH_CONNECTION_CONFIGURED,
|
||||
ERROR_ENROLL_FAILURE,
|
||||
|
@ -119,6 +120,15 @@ export const SubmitErrorCallout: FunctionComponent<SubmitErrorCalloutProps> = (p
|
|||
id="interactiveSetup.submitErrorCallout.pingFailureErrorDescription"
|
||||
defaultMessage="Check the address and retry."
|
||||
/>
|
||||
) : error.body?.attributes?.type === ERROR_COMPATIBILITY_FAILURE ? (
|
||||
<FormattedMessage
|
||||
id="interactiveSetup.submitErrorCallout.compatibilityFailureErrorDescription"
|
||||
defaultMessage="The Elasticsearch cluster (v{elasticsearchVersion}) is incompatible with this version of Kibana (v{kibanaVersion})."
|
||||
values={{
|
||||
elasticsearchVersion: error.body?.attributes?.elasticsearchVersion as string,
|
||||
kibanaVersion: error.body?.attributes?.kibanaVersion as string,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
error.body?.message || error.message
|
||||
)}
|
||||
|
|
26
src/plugins/interactive_setup/server/compatibility_error.ts
Normal file
26
src/plugins/interactive_setup/server/compatibility_error.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 type { NodesVersionCompatibility } from 'src/core/server';
|
||||
|
||||
export class CompatibilityError extends Error {
|
||||
constructor(private meta: NodesVersionCompatibility) {
|
||||
super('Compatibility Error');
|
||||
Error.captureStackTrace(this, CompatibilityError);
|
||||
this.name = 'CompatibilityError';
|
||||
this.message = meta.message!;
|
||||
}
|
||||
|
||||
public get elasticsearchVersion() {
|
||||
return this.meta.incompatibleNodes[0].version;
|
||||
}
|
||||
|
||||
public get kibanaVersion() {
|
||||
return this.meta.kibanaVersion;
|
||||
}
|
||||
}
|
|
@ -7,11 +7,14 @@
|
|||
*/
|
||||
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import tls from 'tls';
|
||||
|
||||
import { nextTick } from '@kbn/test/jest';
|
||||
import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks';
|
||||
|
||||
import { pollEsNodesVersion } from '../../../../src/core/server';
|
||||
import type { NodesVersionCompatibility } from '../../../../src/core/server';
|
||||
import { ElasticsearchConnectionStatus } from '../common';
|
||||
import { ConfigSchema } from './config';
|
||||
import type { ElasticsearchServiceSetup } from './elasticsearch_service';
|
||||
|
@ -19,14 +22,24 @@ import { ElasticsearchService } from './elasticsearch_service';
|
|||
import { interactiveSetupMock } from './mocks';
|
||||
|
||||
jest.mock('tls');
|
||||
jest.mock('../../../../src/core/server', () => ({
|
||||
pollEsNodesVersion: jest.fn(),
|
||||
}));
|
||||
|
||||
const tlsConnectMock = tls.connect as jest.MockedFunction<typeof tls.connect>;
|
||||
const mockPollEsNodesVersion = pollEsNodesVersion as jest.MockedFunction<typeof pollEsNodesVersion>;
|
||||
|
||||
function mockCompatibility(isCompatible: boolean, message?: string) {
|
||||
mockPollEsNodesVersion.mockReturnValue(
|
||||
new BehaviorSubject({ isCompatible, message } as NodesVersionCompatibility).asObservable()
|
||||
);
|
||||
}
|
||||
|
||||
describe('ElasticsearchService', () => {
|
||||
let service: ElasticsearchService;
|
||||
let mockElasticsearchPreboot: ReturnType<typeof elasticsearchServiceMock.createPreboot>;
|
||||
beforeEach(() => {
|
||||
service = new ElasticsearchService(loggingSystemMock.createLogger());
|
||||
service = new ElasticsearchService(loggingSystemMock.createLogger(), '8.0.0');
|
||||
mockElasticsearchPreboot = elasticsearchServiceMock.createPreboot();
|
||||
});
|
||||
|
||||
|
@ -64,6 +77,7 @@ describe('ElasticsearchService', () => {
|
|||
headers: { 'x-elastic-product': 'Elasticsearch' },
|
||||
})
|
||||
);
|
||||
mockCompatibility(true);
|
||||
|
||||
setupContract = service.setup({
|
||||
elasticsearch: mockElasticsearchPreboot,
|
||||
|
@ -383,6 +397,38 @@ describe('ElasticsearchService', () => {
|
|||
expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('fails if version is incompatible', async () => {
|
||||
const mockEnrollScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockEnrollScopedClusterClient.asCurrentUser.transport.request.mockResolvedValue(
|
||||
interactiveSetupMock.createApiResponse({
|
||||
statusCode: 200,
|
||||
body: {
|
||||
token: { name: 'some-name', value: 'some-value' },
|
||||
http_ca: '\n\nsome weird-ca_with\n content\n\n',
|
||||
},
|
||||
})
|
||||
);
|
||||
mockEnrollClient.asScoped.mockReturnValue(mockEnrollScopedClusterClient);
|
||||
|
||||
mockAuthenticateClient.asInternalUser.security.authenticate.mockResolvedValue(
|
||||
interactiveSetupMock.createApiResponse({ statusCode: 200, body: {} as any })
|
||||
);
|
||||
|
||||
mockCompatibility(false, 'Oh no!');
|
||||
|
||||
await expect(
|
||||
setupContract.enroll({
|
||||
apiKey: 'apiKey',
|
||||
hosts: ['host1', 'host2'],
|
||||
caFingerprint: 'DE:AD:BE:EF',
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`[CompatibilityError: Oh no!]`);
|
||||
|
||||
// Check that we properly closed all clients.
|
||||
expect(mockEnrollClient.close).toHaveBeenCalledTimes(1);
|
||||
expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('iterates through all provided hosts until find an accessible one', async () => {
|
||||
mockElasticsearchPreboot.createClient.mockClear();
|
||||
|
||||
|
@ -510,6 +556,20 @@ some weird+ca/with
|
|||
await expect(
|
||||
setupContract.authenticate({ host: 'http://localhost:9200' })
|
||||
).rejects.toMatchInlineSnapshot(`[ConnectionError: some-message]`);
|
||||
expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('fails if version is incompatible', async () => {
|
||||
mockAuthenticateClient.asInternalUser.ping.mockResolvedValue(
|
||||
interactiveSetupMock.createApiResponse({ statusCode: 200, body: true })
|
||||
);
|
||||
|
||||
mockCompatibility(false, 'Oh no!');
|
||||
|
||||
await expect(
|
||||
setupContract.authenticate({ host: 'http://localhost:9200' })
|
||||
).rejects.toMatchInlineSnapshot(`[CompatibilityError: Oh no!]`);
|
||||
expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('succeeds if ping call succeeds', async () => {
|
||||
|
@ -520,6 +580,7 @@ some weird+ca/with
|
|||
await expect(
|
||||
setupContract.authenticate({ host: 'http://localhost:9200' })
|
||||
).resolves.toEqual(undefined);
|
||||
expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -535,6 +596,7 @@ some weird+ca/with
|
|||
await expect(setupContract.ping('http://localhost:9200')).rejects.toMatchInlineSnapshot(
|
||||
`[ConnectionError: some-message]`
|
||||
);
|
||||
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('fails if host is not supported', async () => {
|
||||
|
@ -546,6 +608,7 @@ some weird+ca/with
|
|||
await expect(setupContract.ping('http://localhost:9200')).rejects.toMatchInlineSnapshot(
|
||||
`[ProductNotSupportedError: The client noticed that the server is not Elasticsearch and we do not support this unknown product.]`
|
||||
);
|
||||
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('fails if host is not Elasticsearch', async () => {
|
||||
|
@ -559,6 +622,7 @@ some weird+ca/with
|
|||
await expect(setupContract.ping('http://localhost:9200')).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Host did not respond with valid Elastic product header.]`
|
||||
);
|
||||
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('succeeds if host does not require authentication', async () => {
|
||||
|
@ -570,6 +634,7 @@ some weird+ca/with
|
|||
authRequired: false,
|
||||
certificateChain: undefined,
|
||||
});
|
||||
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('succeeds if host requires authentication', async () => {
|
||||
|
@ -583,6 +648,7 @@ some weird+ca/with
|
|||
authRequired: true,
|
||||
certificateChain: undefined,
|
||||
});
|
||||
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('succeeds if host requires SSL', async () => {
|
||||
|
@ -616,6 +682,7 @@ some weird+ca/with
|
|||
port: 9200,
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('fails if peer certificate cannot be fetched', async () => {
|
||||
|
@ -636,6 +703,7 @@ some weird+ca/with
|
|||
await expect(setupContract.ping('https://localhost:9200')).rejects.toMatchInlineSnapshot(
|
||||
`[Error: some-message]`
|
||||
);
|
||||
expect(mockPingClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
catchError,
|
||||
distinctUntilChanged,
|
||||
exhaustMap,
|
||||
first,
|
||||
map,
|
||||
shareReplay,
|
||||
takeWhile,
|
||||
|
@ -22,14 +23,17 @@ import {
|
|||
import tls from 'tls';
|
||||
|
||||
import type {
|
||||
ElasticsearchClient,
|
||||
ElasticsearchServicePreboot,
|
||||
ICustomClusterClient,
|
||||
Logger,
|
||||
ScopeableRequest,
|
||||
} from 'src/core/server';
|
||||
|
||||
import { pollEsNodesVersion } from '../../../../src/core/server';
|
||||
import { ElasticsearchConnectionStatus } from '../common';
|
||||
import type { Certificate, PingResult } from '../common';
|
||||
import { CompatibilityError } from './compatibility_error';
|
||||
import { getDetailedErrorMessage, getErrorStatusCode } from './errors';
|
||||
|
||||
export interface EnrollParameters {
|
||||
|
@ -101,6 +105,11 @@ export interface EnrollResult {
|
|||
serviceAccountToken: { name: string; value: string };
|
||||
}
|
||||
|
||||
export interface EnrollKibanaResponse {
|
||||
token: { name: string; value: string };
|
||||
http_ca: string;
|
||||
}
|
||||
|
||||
export interface AuthenticateResult {
|
||||
host: string;
|
||||
username?: string;
|
||||
|
@ -113,7 +122,7 @@ export class ElasticsearchService {
|
|||
* Elasticsearch client used to check Elasticsearch connection status.
|
||||
*/
|
||||
private connectionStatusClient?: ICustomClusterClient;
|
||||
constructor(private readonly logger: Logger) {}
|
||||
constructor(private readonly logger: Logger, private kibanaVersion: string) {}
|
||||
|
||||
public setup({
|
||||
elasticsearch,
|
||||
|
@ -186,14 +195,14 @@ export class ElasticsearchService {
|
|||
ssl: { verificationMode: 'none' },
|
||||
});
|
||||
|
||||
let enrollmentResponse;
|
||||
let enrollmentResponse: TransportResult<EnrollKibanaResponse>;
|
||||
try {
|
||||
enrollmentResponse = (await enrollClient
|
||||
enrollmentResponse = await enrollClient
|
||||
.asScoped(scopeableRequest)
|
||||
.asCurrentUser.transport.request({
|
||||
.asCurrentUser.transport.request<EnrollKibanaResponse>({
|
||||
method: 'GET',
|
||||
path: '/_security/enroll/kibana',
|
||||
})) as TransportResult<{ token: { name: string; value: string }; http_ca: string }>;
|
||||
});
|
||||
} catch (err) {
|
||||
// We expect that all hosts belong to exactly same node and any non-connection error for one host would mean
|
||||
// that enrollment will fail for any other host and we should bail out.
|
||||
|
@ -245,9 +254,17 @@ export class ElasticsearchService {
|
|||
enrollmentResponse.body.token.name
|
||||
}" token to host "${host}": ${getDetailedErrorMessage(err)}.`
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
await authenticateClient.close();
|
||||
throw err;
|
||||
}
|
||||
|
||||
const versionCompatibility = await this.checkCompatibility(authenticateClient.asInternalUser);
|
||||
await authenticateClient.close();
|
||||
if (!versionCompatibility.isCompatible) {
|
||||
this.logger.error(
|
||||
`Failed compatibility check of host "${host}": ${versionCompatibility.message}`
|
||||
);
|
||||
throw new CompatibilityError(versionCompatibility);
|
||||
}
|
||||
|
||||
return enrollResult;
|
||||
|
@ -275,9 +292,17 @@ export class ElasticsearchService {
|
|||
this.logger.error(
|
||||
`Failed to authenticate with host "${host}": ${getDetailedErrorMessage(error)}`
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
await client.close();
|
||||
throw error;
|
||||
}
|
||||
|
||||
const versionCompatibility = await this.checkCompatibility(client.asInternalUser);
|
||||
await client.close();
|
||||
if (!versionCompatibility.isCompatible) {
|
||||
this.logger.error(
|
||||
`Failed compatibility check of host "${host}": ${versionCompatibility.message}`
|
||||
);
|
||||
throw new CompatibilityError(versionCompatibility);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -303,6 +328,7 @@ export class ElasticsearchService {
|
|||
error instanceof errors.ProductNotSupportedError
|
||||
) {
|
||||
this.logger.error(`Unable to connect to host "${host}": ${getDetailedErrorMessage(error)}`);
|
||||
await client.close();
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
@ -354,6 +380,18 @@ export class ElasticsearchService {
|
|||
};
|
||||
}
|
||||
|
||||
private async checkCompatibility(internalClient: ElasticsearchClient) {
|
||||
return pollEsNodesVersion({
|
||||
internalClient,
|
||||
log: this.logger,
|
||||
kibanaVersion: this.kibanaVersion,
|
||||
ignoreVersionMismatch: false,
|
||||
esVersionCheckInterval: -1, // Passing a negative number here will result in immediate completion after the first value is emitted
|
||||
})
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
private static fetchPeerCertificate(host: string, port: string | number) {
|
||||
return new Promise<tls.DetailedPeerCertificate>((resolve, reject) => {
|
||||
const socket = tls.connect({ host, port: Number(port), rejectUnauthorized: false });
|
||||
|
|
|
@ -46,7 +46,8 @@ export class InteractiveSetupPlugin implements PrebootPlugin {
|
|||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.#logger = this.initializerContext.logger.get();
|
||||
this.#elasticsearch = new ElasticsearchService(
|
||||
this.initializerContext.logger.get('elasticsearch')
|
||||
this.initializerContext.logger.get('elasticsearch'),
|
||||
initializerContext.env.packageInfo.version
|
||||
);
|
||||
this.#verification = new VerificationService(
|
||||
this.initializerContext.logger.get('verification')
|
||||
|
|
|
@ -13,12 +13,14 @@ import { schema } from '@kbn/config-schema';
|
|||
import type { RouteDefinitionParams } from '.';
|
||||
import {
|
||||
ElasticsearchConnectionStatus,
|
||||
ERROR_COMPATIBILITY_FAILURE,
|
||||
ERROR_CONFIGURE_FAILURE,
|
||||
ERROR_ELASTICSEARCH_CONNECTION_CONFIGURED,
|
||||
ERROR_KIBANA_CONFIG_FAILURE,
|
||||
ERROR_KIBANA_CONFIG_NOT_WRITABLE,
|
||||
ERROR_OUTSIDE_PREBOOT_STAGE,
|
||||
} from '../../common';
|
||||
import { CompatibilityError } from '../compatibility_error';
|
||||
import type { AuthenticateParameters } from '../elasticsearch_service';
|
||||
import { ElasticsearchService } from '../elasticsearch_service';
|
||||
import type { WriteConfigParameters } from '../kibana_config_writer';
|
||||
|
@ -121,7 +123,19 @@ export function defineConfigureRoute({
|
|||
|
||||
try {
|
||||
await elasticsearch.authenticate(configToWrite);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (error instanceof CompatibilityError) {
|
||||
return response.badRequest({
|
||||
body: {
|
||||
message: 'Failed to configure due to version incompatibility.',
|
||||
attributes: {
|
||||
type: ERROR_COMPATIBILITY_FAILURE,
|
||||
elasticsearchVersion: error.elasticsearchVersion,
|
||||
kibanaVersion: error.kibanaVersion,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
// For security reasons, we shouldn't leak to the user whether Elasticsearch node couldn't process enrollment
|
||||
// request or we just couldn't connect to any of the provided hosts.
|
||||
return response.customError({
|
||||
|
|
|
@ -12,12 +12,14 @@ import { schema } from '@kbn/config-schema';
|
|||
|
||||
import {
|
||||
ElasticsearchConnectionStatus,
|
||||
ERROR_COMPATIBILITY_FAILURE,
|
||||
ERROR_ELASTICSEARCH_CONNECTION_CONFIGURED,
|
||||
ERROR_ENROLL_FAILURE,
|
||||
ERROR_KIBANA_CONFIG_FAILURE,
|
||||
ERROR_KIBANA_CONFIG_NOT_WRITABLE,
|
||||
ERROR_OUTSIDE_PREBOOT_STAGE,
|
||||
} from '../../common';
|
||||
import { CompatibilityError } from '../compatibility_error';
|
||||
import { ElasticsearchService } from '../elasticsearch_service';
|
||||
import type { EnrollResult } from '../elasticsearch_service';
|
||||
import type { WriteConfigParameters } from '../kibana_config_writer';
|
||||
|
@ -100,7 +102,19 @@ export function defineEnrollRoutes({
|
|||
hosts: request.body.hosts,
|
||||
caFingerprint: ElasticsearchService.formatFingerprint(request.body.caFingerprint),
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (error instanceof CompatibilityError) {
|
||||
return response.badRequest({
|
||||
body: {
|
||||
message: 'Failed to enroll due to version incompatibility.',
|
||||
attributes: {
|
||||
type: ERROR_COMPATIBILITY_FAILURE,
|
||||
elasticsearchVersion: error.elasticsearchVersion,
|
||||
kibanaVersion: error.kibanaVersion,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
// For security reasons, we shouldn't leak to the user whether Elasticsearch node couldn't process enrollment
|
||||
// request or we just couldn't connect to any of the provided hosts.
|
||||
return response.customError({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue