/**
 * OpenSearch and OpenSearch Dashboards versions are locked, so OpenSearch Dashboards should require that OpenSearch has the same version as
 * that defined in OpenSearch Dashboards's package.json.
 */ import { timer , of , from , Observable } from 'rxjs' ; import { map , distinctUntilChanged , catchError , exhaustMap , mergeMap } from 'rxjs/operators' ; import { get } from 'lodash' ; import { opensearchVersionCompatibleWithOpenSearchDashboards , opensearchVersionEqualsOpenSearchDashboards , } from './opensearch_opensearch_dashboards_version_compatability' ; import { Logger } from '../../logging' ; import type { OpenSearchClient } from '../client' ; param { OpenSearchClient } internalClient param { string } healthcheckAttributeName returns { string | null } export const getNodeId = async ( internalClient , healthcheckAttributeName ) : Promise < string | null > => { try { const state = await internalClient . cluster . state ( { filter_path : [ `nodes.*.attributes.${ healthcheckAttributeName }` ] , } ) ; const sharedClusterId = state . body . nodes . length > 0 ? get ( state . body . nodes [ 0 ] , `attributes.${ healthcheckAttributeName }` , null ) : null ; return sharedClusterId === null || state . body . nodes . find ( ( node ) => sharedClusterId !== get ( node , `attributes.${ healthcheckAttributeName }` , null ) ) ? null : '_local' ; } catch ( e ) { return null ; } } ; export interface PollOpenSearchNodesVersionOptions { internalClient : OpenSearchClient ; optimizedHealthcheckId ? : string ; log : Logger ; opensearchDashboardsVersion : string ; ignoreVersionMismatch : boolean ; opensearchVersionCheckInterval : number ; } interface NodeInfo { version : string ; ip : string ; http : { publish_address : string ; } ; name : string ; } export interface NodesInfo { nodes : { [ key ] : NodeInfo ; } ; } export interface NodesVersionCompatibility { isCompatible : boolean ; message ? : string ; incompatibleNodes : NodeInfo [ ] ; warningNodes : NodeInfo [ ] ; opensearchDashboardsVersion : string ; } function getHumanizedNodeName ( node ) { const publishAddress = node ?. http ?. publish_address + ' ' || '' ; return 'v' + node . version + ' @ ' + publishAddress + '(' + node . ip + ')' ; } export function mapNodesVersionCompatibility ( nodesInfo , opensearchDashboardsVersion , ignoreVersionMismatch ) { if ( Object . keys ( nodesInfo . nodes ?? { } ) . length === 0 ) { return { isCompatible : false , message : 'Unable to retrieve version information from OpenSearch nodes.' , incompatibleNodes : [ ] , warningNodes : [ ] , opensearchDashboardsVersion , } ; } const nodes = Object . keys ( nodesInfo . nodes ) . sort ( ) . map ( ( key ) => nodesInfo . nodes [ key ] ) . map ( ( node ) => Object . assign ( { } , node , { name : getHumanizedNodeName ( node ) } ) ) ; const incompatibleNodes = nodes . filter ( ( node ) => ! opensearchVersionCompatibleWithOpenSearchDashboards ( node . version , opensearchDashboardsVersion ) ) ; const warningNodes = nodes . filter ( ( node ) => ! opensearchVersionEqualsOpenSearchDashboards ( node . version , opensearchDashboardsVersion ) ) ; let message ; if ( incompatibleNodes . length > 0 ) { const incompatibleNodeNames = incompatibleNodes . map ( ( node ) => node . name ) . join ( ', ' ) ; if ( ignoreVersionMismatch ) { message = `Ignoring version incompatibility between OpenSearch Dashboards v${ opensearchDashboardsVersion } and the following OpenSearch nodes: ${ incompatibleNodeNames }` ; } else { message = `This version of OpenSearch Dashboards (v${ opensearchDashboardsVersion }) is incompatible with the following OpenSearch nodes in your cluster: ${ incompatibleNodeNames }` ; } } else if ( warningNodes . length > 0 ) { const warningNodeNames = warningNodes . map ( ( node ) => node . name ) . join ( ', ' ) ; message = `You're running OpenSearch Dashboards ${ opensearchDashboardsVersion } with some different versions of ` + 'OpenSearch. Update OpenSearch Dashboards or OpenSearch to the same ' + `version to prevent compatibility issues: ${ warningNodeNames }` ; } return { isCompatible : ignoreVersionMismatch || incompatibleNodes . length === 0 , message , incompatibleNodes , warningNodes , opensearchDashboardsVersion , } ; } function compareNodes ( prev , curr ) { const nodesEqual = ( n , m ) => n . ip === m . ip && n . version === m . version ; return ( curr . isCompatible === prev . isCompatible && 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 ] ) ) ) ; } export const pollOpenSearchNodesVersion = ( { internalClient , optimizedHealthcheckId , log , opensearchDashboardsVersion , ignoreVersionMismatch , opensearchVersionCheckInterval : healthCheckInterval , } ) : Observable < NodesVersionCompatibility > => { log . debug ( 'Checking OpenSearch version' ) ; return timer ( 0 , healthCheckInterval ) . pipe ( exhaustMap ( ( ) => { if ( optimizedHealthcheckId ) { return from ( getNodeId ( internalClient , optimizedHealthcheckId ) ) . pipe ( mergeMap ( ( nodeId ) => from ( internalClient . nodes . info < NodesInfo > ( { node_id : nodeId , filter_path : [ 'nodes.*.version' , 'nodes.*.http.publish_address' , 'nodes.*.ip' ] , } ) ) . pipe ( map ( ( { body } ) => body ) , catchError ( ( _err ) => { return of ( { nodes : { } } ) ; } ) ) ) ) ; } else { return from ( internalClient . nodes . info < NodesInfo > ( { filter_path : [ 'nodes.*.version' , 'nodes.*.http.publish_address' , 'nodes.*.ip' ] , } ) ) . pipe ( map ( ( { body } ) => body ) , catchError ( ( _err ) => { return of ( { nodes : { } } ) ; } ) ) ; } } ) , map ( ( nodesInfo ) => mapNodesVersionCompatibility ( nodesInfo , opensearchDashboardsVersion , ignoreVersionMismatch ) ) , distinctUntilChanged ( compareNodes ) ) ; } ;