From da27f23adac590a3c1d9f5daca8dc21daa0c4275 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Fri, 14 May 2021 10:37:16 -0700 Subject: [PATCH] Adds error from es call to nodes.info to the nodes version compatibility response message (#100005) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...gin-core-server.elasticsearchstatusmeta.md | 1 + ...csearchstatusmeta.nodesinforequesterror.md | 11 ++ ...n-core-server.nodesversioncompatibility.md | 1 + ...sioncompatibility.nodesinforequesterror.md | 11 ++ src/core/server/elasticsearch/status.test.ts | 115 +++++++++++++++- src/core/server/elasticsearch/status.ts | 7 +- src/core/server/elasticsearch/types.ts | 1 + .../version_check/ensure_es_version.test.ts | 123 +++++++++++++++++- .../version_check/ensure_es_version.ts | 38 ++++-- src/core/server/server.api.md | 4 + 10 files changed, 295 insertions(+), 17 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md index 2398410fa4b84..90aa2f0100d88 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md @@ -16,5 +16,6 @@ export interface ElasticsearchStatusMeta | Property | Type | Description | | --- | --- | --- | | [incompatibleNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md) | NodesVersionCompatibility['incompatibleNodes'] | | +| [nodesInfoRequestError](./kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md) | NodesVersionCompatibility['nodesInfoRequestError'] | | | [warningNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md) | NodesVersionCompatibility['warningNodes'] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md new file mode 100644 index 0000000000000..1b46078a1a453 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) > [nodesInfoRequestError](./kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md) + +## ElasticsearchStatusMeta.nodesInfoRequestError property + +Signature: + +```typescript +nodesInfoRequestError?: NodesVersionCompatibility['nodesInfoRequestError']; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md index 6fcfacc3bc908..cbdac9d5455b0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md @@ -18,5 +18,6 @@ export interface NodesVersionCompatibility | [isCompatible](./kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md) | boolean | | | [kibanaVersion](./kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md) | string | | | [message](./kibana-plugin-core-server.nodesversioncompatibility.message.md) | string | | +| [nodesInfoRequestError](./kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md) | Error | | | [warningNodes](./kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md) | NodeInfo[] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md new file mode 100644 index 0000000000000..aa9421afed6e8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [nodesInfoRequestError](./kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md) + +## NodesVersionCompatibility.nodesInfoRequestError property + +Signature: + +```typescript +nodesInfoRequestError?: Error; +``` diff --git a/src/core/server/elasticsearch/status.test.ts b/src/core/server/elasticsearch/status.test.ts index 6f21fc204a1c2..c1f7cf0e35892 100644 --- a/src/core/server/elasticsearch/status.test.ts +++ b/src/core/server/elasticsearch/status.test.ts @@ -54,7 +54,7 @@ describe('calculateStatus', () => { }); }); - it('changes to available with a differemnt message when isCompatible and warningNodes present', async () => { + it('changes to available with a different message when isCompatible and warningNodes present', async () => { expect( await calculateStatus$( of({ @@ -204,4 +204,117 @@ describe('calculateStatus', () => { ] `); }); + + it('emits status updates when node info request error changes', () => { + const nodeCompat$ = new Subject(); + + const statusUpdates: ServiceStatus[] = []; + const subscription = calculateStatus$(nodeCompat$).subscribe((status) => + statusUpdates.push(status) + ); + + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '1.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info. connect ECONNREFUSED', + nodesInfoRequestError: new Error('connect ECONNREFUSED'), + }); + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '1.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info. security_exception', + nodesInfoRequestError: new Error('security_exception'), + }); + + subscription.unsubscribe(); + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "level": unavailable, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Waiting for Elasticsearch", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "nodesInfoRequestError": [Error: connect ECONNREFUSED], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info. connect ECONNREFUSED", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "nodesInfoRequestError": [Error: security_exception], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info. security_exception", + }, + ] + `); + }); + + it('changes to available when a request error is resolved', () => { + const nodeCompat$ = new Subject(); + + const statusUpdates: ServiceStatus[] = []; + const subscription = calculateStatus$(nodeCompat$).subscribe((status) => + statusUpdates.push(status) + ); + + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '1.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info. security_exception', + nodesInfoRequestError: new Error('security_exception'), + }); + nodeCompat$.next({ + isCompatible: true, + kibanaVersion: '1.1.1', + warningNodes: [], + incompatibleNodes: [], + }); + + subscription.unsubscribe(); + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "level": unavailable, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Waiting for Elasticsearch", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "nodesInfoRequestError": [Error: security_exception], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info. security_exception", + }, + Object { + "level": available, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Elasticsearch is available", + }, + ] + `); + }); }); diff --git a/src/core/server/elasticsearch/status.ts b/src/core/server/elasticsearch/status.ts index 68a61b07f498e..23e44b71863f1 100644 --- a/src/core/server/elasticsearch/status.ts +++ b/src/core/server/elasticsearch/status.ts @@ -32,6 +32,7 @@ export const calculateStatus$ = ( message, incompatibleNodes, warningNodes, + nodesInfoRequestError, }): ServiceStatus => { if (!isCompatible) { return { @@ -40,7 +41,11 @@ export const calculateStatus$ = ( // Message should always be present, but this is a safe fallback message ?? `Some Elasticsearch nodes are not compatible with this version of Kibana`, - meta: { warningNodes, incompatibleNodes }, + meta: { + warningNodes, + incompatibleNodes, + ...(nodesInfoRequestError && { nodesInfoRequestError }), + }, }; } else if (warningNodes.length > 0) { return { diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 85678c21f03b0..8bbf665cbc096 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -179,6 +179,7 @@ export type InternalElasticsearchServiceStart = ElasticsearchServiceStart; export interface ElasticsearchStatusMeta { warningNodes: NodesVersionCompatibility['warningNodes']; incompatibleNodes: NodesVersionCompatibility['incompatibleNodes']; + nodesInfoRequestError?: NodesVersionCompatibility['nodesInfoRequestError']; } /** diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts index 0e08fd2ddc4c5..70166704679fe 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts @@ -19,7 +19,8 @@ const mockLogger = mockLoggerFactory.get('mock logger'); const KIBANA_VERSION = '5.1.0'; const createEsSuccess = elasticsearchClientMock.createSuccessTransportRequestPromise; -const createEsError = elasticsearchClientMock.createErrorTransportRequestPromise; +const createEsErrorReturn = (err: any) => + elasticsearchClientMock.createErrorTransportRequestPromise(err); function createNodes(...versions: string[]): NodesInfo { const nodes = {} as any; @@ -102,6 +103,28 @@ describe('mapNodesVersionCompatibility', () => { `"You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.1.1 @ http_address (ip)"` ); }); + + it('returns isCompatible=false without an extended message when a nodesInfoRequestError is not provided', async () => { + const result = mapNodesVersionCompatibility({ nodes: {} }, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(false); + expect(result.nodesInfoRequestError).toBeUndefined(); + expect(result.message).toMatchInlineSnapshot( + `"Unable to retrieve version information from Elasticsearch nodes."` + ); + }); + + it('returns isCompatible=false with an extended message when a nodesInfoRequestError is present', async () => { + const result = mapNodesVersionCompatibility( + { nodes: {}, nodesInfoRequestError: new Error('connection refused') }, + KIBANA_VERSION, + false + ); + expect(result.isCompatible).toBe(false); + expect(result.nodesInfoRequestError).toBeTruthy(); + expect(result.message).toMatchInlineSnapshot( + `"Unable to retrieve version information from Elasticsearch nodes. connection refused"` + ); + }); }); describe('pollEsNodesVersion', () => { @@ -119,10 +142,10 @@ describe('pollEsNodesVersion', () => { internalClient.nodes.info.mockImplementationOnce(() => createEsSuccess(infos)); }; const nodeInfosErrorOnce = (error: any) => { - internalClient.nodes.info.mockImplementationOnce(() => createEsError(error)); + internalClient.nodes.info.mockImplementationOnce(() => createEsErrorReturn(new Error(error))); }; - it('returns iscCompatible=false and keeps polling when a poll request throws', (done) => { + it('returns isCompatible=false and keeps polling when a poll request throws', (done) => { expect.assertions(3); const expectedCompatibilityResults = [false, false, true]; jest.clearAllMocks(); @@ -148,6 +171,100 @@ describe('pollEsNodesVersion', () => { }); }); + it('returns the error from a failed nodes.info call when a poll request throws', (done) => { + expect.assertions(2); + const expectedCompatibilityResults = [false]; + const expectedMessageResults = [ + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + ]; + jest.clearAllMocks(); + + nodeInfosErrorOnce('mock request error'); + + pollEsNodesVersion({ + internalClient, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(1)) + .subscribe({ + next: (result) => { + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + expect(result.message).toBe(expectedMessageResults.shift()); + }, + complete: done, + error: done, + }); + }); + + it('only emits if the error from a failed nodes.info call changed from the previous poll', (done) => { + expect.assertions(4); + const expectedCompatibilityResults = [false, false]; + const expectedMessageResults = [ + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + 'Unable to retrieve version information from Elasticsearch nodes. mock request error 2', + ]; + jest.clearAllMocks(); + + nodeInfosErrorOnce('mock request error'); // emit + nodeInfosErrorOnce('mock request error'); // ignore, same error message + nodeInfosErrorOnce('mock request error 2'); // emit + + pollEsNodesVersion({ + internalClient, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(2)) + .subscribe({ + next: (result) => { + expect(result.message).toBe(expectedMessageResults.shift()); + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + }, + complete: done, + error: done, + }); + }); + + it('returns isCompatible=false and keeps polling when a poll request throws, only responding again if the error message has changed', (done) => { + expect.assertions(8); + const expectedCompatibilityResults = [false, false, true, false]; + const expectedMessageResults = [ + 'This version of Kibana (v5.1.0) is incompatible with the following Elasticsearch nodes in your cluster: v5.0.0 @ http_address (ip)', + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + "You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.2.0 @ http_address (ip), v5.1.1-Beta1 @ http_address (ip)", + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + ]; + jest.clearAllMocks(); + + nodeInfosSuccessOnce(createNodes('5.1.0', '5.2.0', '5.0.0')); // emit + nodeInfosErrorOnce('mock request error'); // emit + nodeInfosErrorOnce('mock request error'); // ignore + nodeInfosSuccessOnce(createNodes('5.1.0', '5.2.0', '5.1.1-Beta1')); // emit + nodeInfosErrorOnce('mock request error'); // emit + + pollEsNodesVersion({ + internalClient, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(4)) + .subscribe({ + next: (result) => { + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + expect(result.message).toBe(expectedMessageResults.shift()); + }, + complete: done, + error: done, + }); + }); + it('returns compatibility results', (done) => { expect.assertions(1); const nodes = createNodes('5.1.0', '5.2.0', '5.0.0'); diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.ts index fb7ef0583e4a4..43cd52f1b5721 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.ts @@ -49,6 +49,7 @@ export interface NodesVersionCompatibility { incompatibleNodes: NodeInfo[]; warningNodes: NodeInfo[]; kibanaVersion: string; + nodesInfoRequestError?: Error; } function getHumanizedNodeName(node: NodeInfo) { @@ -57,22 +58,28 @@ function getHumanizedNodeName(node: NodeInfo) { } export function mapNodesVersionCompatibility( - nodesInfo: NodesInfo, + nodesInfoResponse: NodesInfo & { nodesInfoRequestError?: Error }, kibanaVersion: string, ignoreVersionMismatch: boolean ): NodesVersionCompatibility { - if (Object.keys(nodesInfo.nodes ?? {}).length === 0) { + if (Object.keys(nodesInfoResponse.nodes ?? {}).length === 0) { + // Note: If the a nodesInfoRequestError is present, the message contains the nodesInfoRequestError.message as a suffix + let message = `Unable to retrieve version information from Elasticsearch nodes.`; + if (nodesInfoResponse.nodesInfoRequestError) { + message = message + ` ${nodesInfoResponse.nodesInfoRequestError.message}`; + } return { isCompatible: false, - message: 'Unable to retrieve version information from Elasticsearch nodes.', + message, incompatibleNodes: [], warningNodes: [], kibanaVersion, + nodesInfoRequestError: nodesInfoResponse.nodesInfoRequestError, }; } - const nodes = Object.keys(nodesInfo.nodes) + const nodes = Object.keys(nodesInfoResponse.nodes) .sort() // Sorting ensures a stable node ordering for comparison - .map((key) => nodesInfo.nodes[key]) + .map((key) => nodesInfoResponse.nodes[key]) .map((node) => Object.assign({}, node, { name: getHumanizedNodeName(node) })); // Aggregate incompatible ES nodes. @@ -112,7 +119,13 @@ export function mapNodesVersionCompatibility( kibanaVersion, }; } - +// Returns true if NodesVersionCompatibility nodesInfoRequestError is the same +function compareNodesInfoErrorMessages( + prev: NodesVersionCompatibility, + curr: NodesVersionCompatibility +): boolean { + return prev.nodesInfoRequestError?.message === curr.nodesInfoRequestError?.message; +} // Returns true if two NodesVersionCompatibility entries match function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompatibility) { const nodesEqual = (n: NodeInfo, m: NodeInfo) => n.ip === m.ip && n.version === m.version; @@ -121,7 +134,8 @@ function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompati curr.incompatibleNodes.length === prev.incompatibleNodes.length && curr.warningNodes.length === prev.warningNodes.length && curr.incompatibleNodes.every((node, i) => nodesEqual(node, prev.incompatibleNodes[i])) && - curr.warningNodes.every((node, i) => nodesEqual(node, prev.warningNodes[i])) + curr.warningNodes.every((node, i) => nodesEqual(node, prev.warningNodes[i])) && + compareNodesInfoErrorMessages(curr, prev) ); } @@ -141,14 +155,14 @@ export const pollEsNodesVersion = ({ }) ).pipe( map(({ body }) => body), - catchError((_err) => { - return of({ nodes: {} }); + catchError((nodesInfoRequestError) => { + return of({ nodes: {}, nodesInfoRequestError }); }) ); }), - map((nodesInfo: NodesInfo) => - mapNodesVersionCompatibility(nodesInfo, kibanaVersion, ignoreVersionMismatch) + map((nodesInfoResponse: NodesInfo & { nodesInfoRequestError?: Error }) => + mapNodesVersionCompatibility(nodesInfoResponse, kibanaVersion, ignoreVersionMismatch) ), - distinctUntilChanged(compareNodes) // Only emit if there are new nodes or versions + distinctUntilChanged(compareNodes) // Only emit if there are new nodes or versions or if we return an error and that error changes ); }; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4c12ca53b9098..f4c70d718bc87 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -976,6 +976,8 @@ export interface ElasticsearchStatusMeta { // (undocumented) incompatibleNodes: NodesVersionCompatibility['incompatibleNodes']; // (undocumented) + nodesInfoRequestError?: NodesVersionCompatibility['nodesInfoRequestError']; + // (undocumented) warningNodes: NodesVersionCompatibility['warningNodes']; } @@ -1727,6 +1729,8 @@ export interface NodesVersionCompatibility { // (undocumented) message?: string; // (undocumented) + nodesInfoRequestError?: Error; + // (undocumented) warningNodes: NodeInfo[]; }