diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/cluster.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/cluster.test.ts new file mode 100644 index 000000000000..412ce348d56e --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/cluster.test.ts @@ -0,0 +1,360 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { MlAction, UpgradeAssistantStatus } from '../../common/types'; + +import { ClusterTestBed, setupClusterPage, setupEnvironment } from './helpers'; + +describe('Cluster tab', () => { + let testBed: ClusterTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + describe('with deprecations', () => { + const snapshotId = '1'; + const jobId = 'deprecation_check_job'; + const upgradeStatusMockResponse: UpgradeAssistantStatus = { + readyForUpgrade: false, + cluster: [ + { + level: 'critical', + message: + 'model snapshot [1] for job [deprecation_check_job] needs to be deleted or upgraded', + details: + 'model snapshot [%s] for job [%s] supports minimum version [%s] and needs to be at least [%s]', + url: 'doc_url', + correctiveAction: { + type: 'mlSnapshot', + snapshotId, + jobId, + }, + }, + ], + indices: [], + }; + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(upgradeStatusMockResponse); + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ isEnabled: true }); + + await act(async () => { + testBed = await setupClusterPage({ isReadOnlyMode: false }); + }); + + const { actions, component } = testBed; + + component.update(); + + // Navigate to the cluster tab + await act(async () => { + actions.clickTab('cluster'); + }); + + component.update(); + }); + + test('renders deprecations', () => { + const { exists } = testBed; + expect(exists('clusterTabContent')).toBe(true); + expect(exists('deprecationsContainer')).toBe(true); + }); + + describe('fix ml snapshots button', () => { + let flyout: Element | null; + + beforeEach(async () => { + const { component, actions, exists, find } = testBed; + + expect(exists('deprecationsContainer')).toBe(true); + + // Open all deprecations + actions.clickExpandAll(); + + // The data-test-subj is derived from the deprecation message + const accordionTestSubj = `depgroup_${upgradeStatusMockResponse.cluster[0].message + .split(' ') + .join('_')}`; + + await act(async () => { + find(`${accordionTestSubj}.fixMlSnapshotsButton`).simulate('click'); + }); + + component.update(); + + // We need to read the document "body" as the flyout is added there and not inside + // the component DOM tree. + flyout = document.body.querySelector('[data-test-subj="fixSnapshotsFlyout"]'); + + expect(flyout).not.toBe(null); + expect(flyout!.textContent).toContain('Upgrade or delete model snapshot'); + }); + + test('upgrades snapshots', async () => { + const { component } = testBed; + + const upgradeButton: HTMLButtonElement | null = flyout!.querySelector( + '[data-test-subj="upgradeSnapshotButton"]' + ); + + httpRequestsMockHelpers.setUpgradeMlSnapshotResponse({ + nodeId: 'my_node', + snapshotId, + jobId, + status: 'in_progress', + }); + + await act(async () => { + upgradeButton!.click(); + }); + + component.update(); + + // First, we expect a POST request to upgrade the snapshot + const upgradeRequest = server.requests[server.requests.length - 2]; + expect(upgradeRequest.method).toBe('POST'); + expect(upgradeRequest.url).toBe('/api/upgrade_assistant/ml_snapshots'); + + // Next, we expect a GET request to check the status of the upgrade + const statusRequest = server.requests[server.requests.length - 1]; + expect(statusRequest.method).toBe('GET'); + expect(statusRequest.url).toBe( + `/api/upgrade_assistant/ml_snapshots/${jobId}/${snapshotId}` + ); + }); + + test('handles upgrade failure', async () => { + const { component, find } = testBed; + + const upgradeButton: HTMLButtonElement | null = flyout!.querySelector( + '[data-test-subj="upgradeSnapshotButton"]' + ); + + const error = { + statusCode: 500, + error: 'Upgrade snapshot error', + message: 'Upgrade snapshot error', + }; + + httpRequestsMockHelpers.setUpgradeMlSnapshotResponse(undefined, error); + + await act(async () => { + upgradeButton!.click(); + }); + + component.update(); + + const upgradeRequest = server.requests[server.requests.length - 1]; + expect(upgradeRequest.method).toBe('POST'); + expect(upgradeRequest.url).toBe('/api/upgrade_assistant/ml_snapshots'); + + const accordionTestSubj = `depgroup_${upgradeStatusMockResponse.cluster[0].message + .split(' ') + .join('_')}`; + + expect(find(`${accordionTestSubj}.fixMlSnapshotsButton`).text()).toEqual('Failed'); + }); + + test('deletes snapshots', async () => { + const { component } = testBed; + + const deleteButton: HTMLButtonElement | null = flyout!.querySelector( + '[data-test-subj="deleteSnapshotButton"]' + ); + + httpRequestsMockHelpers.setDeleteMlSnapshotResponse({ + acknowledged: true, + }); + + await act(async () => { + deleteButton!.click(); + }); + + component.update(); + + const request = server.requests[server.requests.length - 1]; + const mlDeprecation = upgradeStatusMockResponse.cluster[0]; + + expect(request.method).toBe('DELETE'); + expect(request.url).toBe( + `/api/upgrade_assistant/ml_snapshots/${ + (mlDeprecation.correctiveAction! as MlAction).jobId + }/${(mlDeprecation.correctiveAction! as MlAction).snapshotId}` + ); + }); + + test('handles delete failure', async () => { + const { component, find } = testBed; + + const deleteButton: HTMLButtonElement | null = flyout!.querySelector( + '[data-test-subj="deleteSnapshotButton"]' + ); + + const error = { + statusCode: 500, + error: 'Upgrade snapshot error', + message: 'Upgrade snapshot error', + }; + + httpRequestsMockHelpers.setDeleteMlSnapshotResponse(undefined, error); + + await act(async () => { + deleteButton!.click(); + }); + + component.update(); + + const request = server.requests[server.requests.length - 1]; + const mlDeprecation = upgradeStatusMockResponse.cluster[0]; + + expect(request.method).toBe('DELETE'); + expect(request.url).toBe( + `/api/upgrade_assistant/ml_snapshots/${ + (mlDeprecation.correctiveAction! as MlAction).jobId + }/${(mlDeprecation.correctiveAction! as MlAction).snapshotId}` + ); + + const accordionTestSubj = `depgroup_${upgradeStatusMockResponse.cluster[0].message + .split(' ') + .join('_')}`; + + expect(find(`${accordionTestSubj}.fixMlSnapshotsButton`).text()).toEqual('Failed'); + }); + }); + }); + + describe('no deprecations', () => { + beforeEach(async () => { + const noDeprecationsResponse = { + readyForUpgrade: false, + cluster: [], + indices: [], + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(noDeprecationsResponse); + + await act(async () => { + testBed = await setupClusterPage({ isReadOnlyMode: false }); + }); + + const { component } = testBed; + + component.update(); + }); + + test('renders prompt', () => { + const { exists, find } = testBed; + expect(exists('noDeprecationsPrompt')).toBe(true); + expect(find('noDeprecationsPrompt').text()).toContain('Ready to upgrade!'); + }); + }); + + describe('error handling', () => { + test('handles 403', async () => { + const error = { + statusCode: 403, + error: 'Forbidden', + message: 'Forbidden', + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); + + await act(async () => { + testBed = await setupClusterPage({ isReadOnlyMode: false }); + }); + + const { component, exists, find } = testBed; + + component.update(); + + expect(exists('permissionsError')).toBe(true); + expect(find('permissionsError').text()).toContain( + 'You are not authorized to view Elasticsearch deprecations.' + ); + }); + + test('shows upgraded message when all nodes have been upgraded', async () => { + const error = { + statusCode: 426, + error: 'Upgrade required', + message: 'There are some nodes running a different version of Elasticsearch', + attributes: { + // This is marked true in the scenario where none of the nodes have the same major version of Kibana, + // and therefore we assume all have been upgraded + allNodesUpgraded: true, + }, + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); + + await act(async () => { + testBed = await setupClusterPage({ isReadOnlyMode: false }); + }); + + const { component, exists, find } = testBed; + + component.update(); + + expect(exists('upgradedCallout')).toBe(true); + expect(find('upgradedCallout').text()).toContain( + 'Your configuration is up to date. Kibana and all Elasticsearch nodes are running the same version.' + ); + }); + + test('shows partially upgrade error when nodes are running different versions', async () => { + const error = { + statusCode: 426, + error: 'Upgrade required', + message: 'There are some nodes running a different version of Elasticsearch', + attributes: { + allNodesUpgraded: false, + }, + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); + + await act(async () => { + testBed = await setupClusterPage({ isReadOnlyMode: false }); + }); + + const { component, exists, find } = testBed; + + component.update(); + + expect(exists('partiallyUpgradedWarning')).toBe(true); + expect(find('partiallyUpgradedWarning').text()).toContain( + 'Upgrade Kibana to the same version as your Elasticsearch cluster. One or more nodes in the cluster is running a different version than Kibana.' + ); + }); + + test('handles generic error', async () => { + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); + + await act(async () => { + testBed = await setupClusterPage({ isReadOnlyMode: false }); + }); + + const { component, exists, find } = testBed; + + component.update(); + + expect(exists('requestError')).toBe(true); + expect(find('requestError').text()).toContain( + 'Could not retrieve Elasticsearch deprecations.' + ); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/cluster.helpers.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/cluster.helpers.ts new file mode 100644 index 000000000000..2aedface1e32 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/cluster.helpers.ts @@ -0,0 +1,67 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; +import { EsDeprecationsContent } from '../../../public/application/components/es_deprecations'; +import { WithAppDependencies } from './setup_environment'; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: ['/es_deprecations/cluster'], + componentRoutePath: '/es_deprecations/:tabName', + }, + doMountAsync: true, +}; + +export type ClusterTestBed = TestBed & { + actions: ReturnType; +}; + +const createActions = (testBed: TestBed) => { + /** + * User Actions + */ + const clickTab = (tabName: string) => { + const { find } = testBed; + const camelcaseTabName = tabName.charAt(0).toUpperCase() + tabName.slice(1); + + find(`upgradeAssistant${camelcaseTabName}Tab`).simulate('click'); + }; + + const clickExpandAll = () => { + const { find } = testBed; + find('expandAll').simulate('click'); + }; + + return { + clickTab, + clickExpandAll, + }; +}; + +export const setup = async (overrides?: Record): Promise => { + const initTestBed = registerTestBed( + WithAppDependencies(EsDeprecationsContent, overrides), + testBedConfig + ); + const testBed = await initTestBed(); + + return { + ...testBed, + actions: createActions(testBed), + }; +}; + +export type ClusterTestSubjects = + | 'expandAll' + | 'deprecationsContainer' + | 'permissionsError' + | 'requestError' + | 'upgradedCallout' + | 'partiallyUpgradedWarning' + | 'noDeprecationsPrompt' + | string; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts index e3f6df54db60..3fd8b7279c07 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts @@ -62,11 +62,35 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setUpgradeMlSnapshotResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('POST', `${API_BASE_PATH}/ml_snapshots`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + + const setDeleteMlSnapshotResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('DELETE', `${API_BASE_PATH}/ml_snapshots/:jobId/:snapshotId`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadEsDeprecationsResponse, setLoadDeprecationLoggingResponse, setUpdateDeprecationLoggingResponse, setUpdateIndexSettingsResponse, + setUpgradeMlSnapshotResponse, + setDeleteMlSnapshotResponse, }; }; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts index ddf5787af103..8e256680253b 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts @@ -7,6 +7,7 @@ export { setup as setupOverviewPage, OverviewTestBed } from './overview.helpers'; export { setup as setupIndicesPage, IndicesTestBed } from './indices.helpers'; +export { setup as setupClusterPage, ClusterTestBed } from './cluster.helpers'; export { setup as setupKibanaPage, KibanaTestBed } from './kibana.helpers'; export { setupEnvironment } from './setup_environment'; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx index faeb0e4a40ab..aae550040332 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx @@ -33,7 +33,6 @@ export const WithAppDependencies = (Comp: any, overrides: Record { }); describe('with deprecations', () => { - const upgradeStatusMockResponse = { + const upgradeStatusMockResponse: UpgradeAssistantStatus = { readyForUpgrade: false, cluster: [], indices: [ { - level: 'warning' as MIGRATION_DEPRECATION_LEVEL, + level: 'warning', message: indexSettingDeprecations.translog.deprecationMessage, url: 'doc_url', index: 'my_index', - deprecatedIndexSettings: indexSettingDeprecations.translog.settings, + correctiveAction: { + type: 'indexSetting', + deprecatedSettings: indexSettingDeprecations.translog.settings, + }, }, ], }; @@ -56,6 +59,7 @@ describe('Indices tab', () => { test('renders deprecations', () => { const { exists, find } = testBed; + expect(exists('indexTabContent')).toBe(true); expect(exists('deprecationsContainer')).toBe(true); expect(find('indexCount').text()).toEqual('1'); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview.test.ts index 85efaf38f32a..9b65b493a74c 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview.test.ts @@ -38,7 +38,6 @@ describe('Overview page', () => { details: 'translog retention settings [index.translog.retention.size] and [index.translog.retention.age] are ignored because translog is no longer used in peer recoveries with soft-deletes enabled (default in 7.0 or later)', index: 'settings', - reindex: false, }, ], }; diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index 0471fc30f28e..88fa103bace8 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -28,7 +28,6 @@ export enum ReindexStatus { } export const REINDEX_OP_TYPE = 'upgrade-assistant-reindex-operation'; - export interface QueueSettings extends SavedObjectAttributes { /** * A Unix timestamp of when the reindex operation was enqueued. @@ -190,11 +189,9 @@ export interface DeprecationAPIResponse { node_settings: DeprecationInfo[]; index_settings: IndexSettingsDeprecationInfo; } -export interface EnrichedDeprecationInfo extends DeprecationInfo { - index?: string; - node?: string; - reindex?: boolean; - deprecatedIndexSettings?: string[]; + +export interface ReindexAction { + type: 'reindex'; /** * Indicate what blockers have been detected for calling reindex * against this index. @@ -205,6 +202,21 @@ export interface EnrichedDeprecationInfo extends DeprecationInfo { blockerForReindexing?: 'index-closed'; // 'index-closed' can be handled automatically, but requires more resources, user should be warned } +export interface MlAction { + type: 'mlSnapshot'; + snapshotId: string; + jobId: string; +} + +export interface IndexSettingAction { + type: 'indexSetting'; + deprecatedSettings: string[]; +} +export interface EnrichedDeprecationInfo extends DeprecationInfo { + index?: string; + correctiveAction?: ReindexAction | MlAction | IndexSettingAction; +} + export interface UpgradeAssistantStatus { readyForUpgrade: boolean; cluster: EnrichedDeprecationInfo[]; @@ -225,3 +237,11 @@ export interface ResolveIndexResponseFromES { }>; data_streams: Array<{ name: string; backing_indices: string[]; timestamp_field: string }>; } + +export const ML_UPGRADE_OP_TYPE = 'upgrade-assistant-ml-upgrade-operation'; + +export interface MlOperation extends SavedObjectAttributes { + nodeId: string; + snapshotId: string; + jobId: string; +} diff --git a/x-pack/plugins/upgrade_assistant/kibana.json b/x-pack/plugins/upgrade_assistant/kibana.json index d9f4917fa0a6..d013c16837b7 100644 --- a/x-pack/plugins/upgrade_assistant/kibana.json +++ b/x-pack/plugins/upgrade_assistant/kibana.json @@ -5,6 +5,6 @@ "ui": true, "configPath": ["xpack", "upgrade_assistant"], "requiredPlugins": ["management", "licensing", "features"], - "optionalPlugins": ["cloud", "usageCollection"], + "optionalPlugins": ["usageCollection"], "requiredBundles": ["esUiShared", "kibanaReact"] } diff --git a/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx b/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx index 049318f5b78d..88b5bd4721c3 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx @@ -24,7 +24,6 @@ export interface KibanaVersionContext { export interface ContextValue { http: HttpSetup; - isCloudEnabled: boolean; docLinks: DocLinksStart; kibanaVersionInfo: KibanaVersionContext; notifications: NotificationsStart; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/cell.tsx index b7d3247ffbf2..4324379f456e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/cell.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/cell.tsx @@ -17,34 +17,84 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EnrichedDeprecationInfo } from '../../../../../common/types'; +import { + EnrichedDeprecationInfo, + MlAction, + ReindexAction, + IndexSettingAction, +} from '../../../../../common/types'; import { AppContext } from '../../../app_context'; import { ReindexButton } from './reindex'; import { FixIndexSettingsButton } from './index_settings'; +import { FixMlSnapshotsButton } from './ml_snapshots'; interface DeprecationCellProps { items?: Array<{ title?: string; body: string }>; - reindexIndexName?: string; - deprecatedIndexSettings?: string[]; docUrl?: string; headline?: string; healthColor?: string; children?: ReactNode; - reindexBlocker?: EnrichedDeprecationInfo['blockerForReindexing']; + correctiveAction?: EnrichedDeprecationInfo['correctiveAction']; + indexName?: string; } +interface CellActionProps { + correctiveAction: EnrichedDeprecationInfo['correctiveAction']; + indexName?: string; + items: Array<{ title?: string; body: string }>; +} + +const CellAction: FunctionComponent = ({ correctiveAction, indexName, items }) => { + const { type: correctiveActionType } = correctiveAction!; + switch (correctiveActionType) { + case 'mlSnapshot': + const { jobId, snapshotId } = correctiveAction as MlAction; + return ( + + ); + + case 'reindex': + const { blockerForReindexing } = correctiveAction as ReindexAction; + + return ( + + {({ http, docLinks }) => ( + + )} + + ); + + case 'indexSetting': + const { deprecatedSettings } = correctiveAction as IndexSettingAction; + + return ; + + default: + throw new Error(`No UI defined for corrective action: ${correctiveActionType}`); + } +}; + /** * Used to display a deprecation with links to docs, a health indicator, and other descriptive information. */ export const DeprecationCell: FunctionComponent = ({ headline, healthColor, - reindexIndexName, - deprecatedIndexSettings, + correctiveAction, + indexName, docUrl, items = [], children, - reindexBlocker, }) => (
@@ -82,24 +132,9 @@ export const DeprecationCell: FunctionComponent = ({ )} - {reindexIndexName && ( + {correctiveAction && ( - - {({ http, docLinks }) => ( - - )} - - - )} - - {deprecatedIndexSettings?.length && ( - - + )} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.test.tsx index 188e70b64ce6..f4ac573d86b1 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.test.tsx @@ -13,9 +13,9 @@ import { IndexDeprecationTableProps, IndexDeprecationTable } from './index_table describe('IndexDeprecationTable', () => { const defaultProps = { indices: [ - { index: 'index1', details: 'Index 1 deets', reindex: true }, - { index: 'index2', details: 'Index 2 deets', reindex: true }, - { index: 'index3', details: 'Index 3 deets', reindex: true }, + { index: 'index1', details: 'Index 1 deets', correctiveAction: { type: 'reindex' } }, + { index: 'index2', details: 'Index 2 deets', correctiveAction: { type: 'reindex' } }, + { index: 'index3', details: 'Index 3 deets', correctiveAction: { type: 'reindex' } }, ], } as IndexDeprecationTableProps; @@ -49,19 +49,25 @@ describe('IndexDeprecationTable', () => { items={ Array [ Object { + "correctiveAction": Object { + "type": "reindex", + }, "details": "Index 1 deets", "index": "index1", - "reindex": true, }, Object { + "correctiveAction": Object { + "type": "reindex", + }, "details": "Index 2 deets", "index": "index2", - "reindex": true, }, Object { + "correctiveAction": Object { + "type": "reindex", + }, "details": "Index 3 deets", "index": "index3", - "reindex": true, }, ] } diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.tsx index 216884d547ee..6b0f94ea24bc 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.tsx @@ -10,7 +10,11 @@ import React from 'react'; import { EuiBasicTable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EnrichedDeprecationInfo } from '../../../../../common/types'; +import { + EnrichedDeprecationInfo, + IndexSettingAction, + ReindexAction, +} from '../../../../../common/types'; import { AppContext } from '../../../app_context'; import { ReindexButton } from './reindex'; import { FixIndexSettingsButton } from './index_settings'; @@ -19,9 +23,7 @@ const PAGE_SIZES = [10, 25, 50, 100, 250, 500, 1000]; export interface IndexDeprecationDetails { index: string; - reindex: boolean; - deprecatedIndexSettings?: string[]; - blockerForReindexing?: EnrichedDeprecationInfo['blockerForReindexing']; + correctiveAction?: EnrichedDeprecationInfo['correctiveAction']; details?: string; } @@ -152,9 +154,9 @@ export class IndexDeprecationTable extends React.Component< // NOTE: this naive implementation assumes all indices in the table // should show the reindex button or fix indices button. This should work for known use cases. const { indices } = this.props; - const showReindexButton = Boolean(indices.find((i) => i.reindex === true)); + const showReindexButton = Boolean(indices.find((i) => i.correctiveAction?.type === 'reindex')); const showFixSettingsButton = Boolean( - indices.find((i) => i.deprecatedIndexSettings && i.deprecatedIndexSettings.length > 0) + indices.find((i) => i.correctiveAction?.type === 'indexSetting') ); if (showReindexButton === false && showFixSettingsButton === false) { @@ -172,7 +174,9 @@ export class IndexDeprecationTable extends React.Component< return ( @@ -184,7 +188,7 @@ export class IndexDeprecationTable extends React.Component< return ( ); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.test.tsx index 579cf1f4a55b..2bfa8119e41b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.test.tsx @@ -72,18 +72,14 @@ describe('EsDeprecationList', () => { indices={ Array [ Object { - "blockerForReindexing": undefined, - "deprecatedIndexSettings": undefined, + "correctiveAction": undefined, "details": undefined, "index": "0", - "reindex": false, }, Object { - "blockerForReindexing": undefined, - "deprecatedIndexSettings": undefined, + "correctiveAction": undefined, "details": undefined, "index": "1", - "reindex": false, }, ] } diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.tsx index cb9f238d0e4d..7b543a7e94b3 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.tsx @@ -32,11 +32,10 @@ const MessageDeprecation: FunctionComponent<{ return ( @@ -57,10 +56,10 @@ const SimpleMessageDeprecation: FunctionComponent<{ deprecation: EnrichedDepreca return ( ); }; @@ -94,12 +93,11 @@ export const EsDeprecationList: FunctionComponent<{ if (currentGroupBy === GroupByOption.message && deprecations[0].index !== undefined) { // We assume that every deprecation message is the same issue (since they have the same // message) and that each deprecation will have an index associated with it. + const indices = deprecations.map((dep) => ({ index: dep.index!, details: dep.details, - reindex: dep.reindex === true, - deprecatedIndexSettings: dep.deprecatedIndexSettings, - blockerForReindexing: dep.blockerForReindexing, + correctiveAction: dep.correctiveAction, })); return ; } else if (currentGroupBy === GroupByOption.index) { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/ml_snapshots/button.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/ml_snapshots/button.tsx new file mode 100644 index 000000000000..13b7dacc3b59 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/ml_snapshots/button.tsx @@ -0,0 +1,125 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; + +import { ButtonSize, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FixSnapshotsFlyout } from './fix_snapshots_flyout'; +import { useAppContext } from '../../../../app_context'; +import { useSnapshotState } from './use_snapshot_state'; + +const i18nTexts = { + fixButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.fixButtonLabel', + { + defaultMessage: 'Fix', + } + ), + upgradingButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.upgradingButtonLabel', + { + defaultMessage: 'Upgradingā€¦', + } + ), + deletingButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.deletingButtonLabel', + { + defaultMessage: 'Deletingā€¦', + } + ), + doneButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.doneButtonLabel', + { + defaultMessage: 'Done', + } + ), + failedButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.failedButtonLabel', + { + defaultMessage: 'Failed', + } + ), +}; + +interface Props { + snapshotId: string; + jobId: string; + description: string; +} + +export const FixMlSnapshotsButton: React.FunctionComponent = ({ + snapshotId, + jobId, + description, +}) => { + const { api } = useAppContext(); + const { snapshotState, upgradeSnapshot, deleteSnapshot, updateSnapshotStatus } = useSnapshotState( + { + jobId, + snapshotId, + api, + } + ); + + const [showFlyout, setShowFlyout] = useState(false); + + useEffect(() => { + updateSnapshotStatus(); + }, [updateSnapshotStatus]); + + const commonButtonProps = { + size: 's' as ButtonSize, + onClick: () => setShowFlyout(true), + 'data-test-subj': 'fixMlSnapshotsButton', + }; + + let button = {i18nTexts.fixButtonLabel}; + + switch (snapshotState.status) { + case 'in_progress': + button = ( + + {snapshotState.action === 'delete' + ? i18nTexts.deletingButtonLabel + : i18nTexts.upgradingButtonLabel} + + ); + break; + case 'complete': + button = ( + + {i18nTexts.doneButtonLabel} + + ); + break; + case 'error': + button = ( + + {i18nTexts.failedButtonLabel} + + ); + break; + } + + return ( + <> + {button} + + {showFlyout && ( + setShowFlyout(false)} + /> + )} + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/ml_snapshots/fix_snapshots_flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/ml_snapshots/fix_snapshots_flyout.tsx new file mode 100644 index 000000000000..7dafab011a69 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/ml_snapshots/fix_snapshots_flyout.tsx @@ -0,0 +1,181 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiPortal, + EuiTitle, + EuiText, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; +import { SnapshotStatus } from './use_snapshot_state'; +import { ResponseError } from '../../../../lib/api'; + +interface SnapshotState extends SnapshotStatus { + error?: ResponseError; +} +interface Props { + upgradeSnapshot: () => Promise; + deleteSnapshot: () => Promise; + description: string; + closeFlyout: () => void; + snapshotState: SnapshotState; +} + +const i18nTexts = { + upgradeButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.upgradeButtonLabel', + { + defaultMessage: 'Upgrade', + } + ), + retryUpgradeButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.retryUpgradeButtonLabel', + { + defaultMessage: 'Retry upgrade', + } + ), + closeButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.cancelButtonLabel', + { + defaultMessage: 'Close', + } + ), + deleteButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.deleteButtonLabel', + { + defaultMessage: 'Delete', + } + ), + retryDeleteButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.retryDeleteButtonLabel', + { + defaultMessage: 'Retry delete', + } + ), + flyoutTitle: i18n.translate('xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.title', { + defaultMessage: 'Upgrade or delete model snapshot', + }), + deleteSnapshotErrorTitle: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.deleteSnapshotErrorTitle', + { + defaultMessage: 'Error deleting snapshot', + } + ), + upgradeSnapshotErrorTitle: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.upgradeSnapshotErrorTitle', + { + defaultMessage: 'Error upgrading snapshot', + } + ), +}; + +export const FixSnapshotsFlyout = ({ + upgradeSnapshot, + deleteSnapshot, + description, + closeFlyout, + snapshotState, +}: Props) => { + const onUpgradeSnapshot = () => { + upgradeSnapshot(); + closeFlyout(); + }; + + const onDeleteSnapshot = () => { + deleteSnapshot(); + closeFlyout(); + }; + + return ( + + + + +

{i18nTexts.flyoutTitle}

+
+
+ + {snapshotState.error && ( + <> + + {snapshotState.error.message} + + + + )} + +

{description}

+
+
+ + + + + {i18nTexts.closeButtonLabel} + + + + + + + {snapshotState.action === 'delete' && snapshotState.error + ? i18nTexts.retryDeleteButtonLabel + : i18nTexts.deleteButtonLabel} + + + + + {snapshotState.action === 'upgrade' && snapshotState.error + ? i18nTexts.retryUpgradeButtonLabel + : i18nTexts.upgradeButtonLabel} + + + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/ml_snapshots/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/ml_snapshots/index.ts new file mode 100644 index 000000000000..d537c94cf67a --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/ml_snapshots/index.ts @@ -0,0 +1,8 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { FixMlSnapshotsButton } from './button'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/ml_snapshots/use_snapshot_state.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/ml_snapshots/use_snapshot_state.tsx new file mode 100644 index 000000000000..2dd4638c772b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/ml_snapshots/use_snapshot_state.tsx @@ -0,0 +1,151 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRef, useCallback, useState, useEffect } from 'react'; + +import { ApiService, ResponseError } from '../../../../lib/api'; + +const POLL_INTERVAL_MS = 1000; + +export interface SnapshotStatus { + snapshotId: string; + jobId: string; + status: 'complete' | 'in_progress' | 'error' | 'idle'; + action?: 'upgrade' | 'delete'; +} + +export const useSnapshotState = ({ + jobId, + snapshotId, + api, +}: { + jobId: string; + snapshotId: string; + api: ApiService; +}) => { + const [requestError, setRequestError] = useState(undefined); + const [snapshotState, setSnapshotState] = useState({ + status: 'idle', + jobId, + snapshotId, + }); + + const pollIntervalIdRef = useRef | null>(null); + const isMounted = useRef(false); + + const clearPollInterval = useCallback(() => { + if (pollIntervalIdRef.current) { + clearTimeout(pollIntervalIdRef.current); + pollIntervalIdRef.current = null; + } + }, []); + + const updateSnapshotStatus = useCallback(async () => { + clearPollInterval(); + + const { data, error: updateStatusError } = await api.getMlSnapshotUpgradeStatus({ + jobId, + snapshotId, + }); + + if (updateStatusError) { + setSnapshotState({ + snapshotId, + jobId, + action: 'upgrade', + status: 'error', + }); + setRequestError(updateStatusError); + return; + } + + setSnapshotState(data); + + // Only keep polling if it exists and is in progress. + if (data?.status === 'in_progress') { + pollIntervalIdRef.current = setTimeout(updateSnapshotStatus, POLL_INTERVAL_MS); + } + }, [api, clearPollInterval, jobId, snapshotId]); + + const upgradeSnapshot = useCallback(async () => { + setSnapshotState({ + snapshotId, + jobId, + action: 'upgrade', + status: 'in_progress', + }); + + const { data, error: upgradeError } = await api.upgradeMlSnapshot({ jobId, snapshotId }); + + if (upgradeError) { + setRequestError(upgradeError); + setSnapshotState({ + snapshotId, + jobId, + action: 'upgrade', + status: 'error', + }); + return; + } + + setSnapshotState(data); + updateSnapshotStatus(); + }, [api, jobId, snapshotId, updateSnapshotStatus]); + + const deleteSnapshot = useCallback(async () => { + setSnapshotState({ + snapshotId, + jobId, + action: 'delete', + status: 'in_progress', + }); + + const { error: deleteError } = await api.deleteMlSnapshot({ + snapshotId, + jobId, + }); + + if (deleteError) { + setRequestError(deleteError); + setSnapshotState({ + snapshotId, + jobId, + action: 'delete', + status: 'error', + }); + return; + } + + setSnapshotState({ + snapshotId, + jobId, + action: 'delete', + status: 'complete', + }); + }, [api, jobId, snapshotId]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + + // Clean up on unmount. + clearPollInterval(); + }; + }, [clearPollInterval]); + + return { + snapshotState: { + ...snapshotState, + error: requestError, + }, + upgradeSnapshot, + updateSnapshotStatus, + deleteSnapshot, + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/button.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/button.tsx index 34c1328459cd..646f25393166 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/button.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/button.tsx @@ -14,11 +14,7 @@ import { EuiButton, EuiLoadingSpinner, EuiText, EuiToolTip } from '@elastic/eui' import { FormattedMessage } from '@kbn/i18n/react'; import { DocLinksStart, HttpSetup } from 'src/core/public'; import { API_BASE_PATH } from '../../../../../../common/constants'; -import { - EnrichedDeprecationInfo, - ReindexStatus, - UIReindexOption, -} from '../../../../../../common/types'; +import { ReindexAction, ReindexStatus, UIReindexOption } from '../../../../../../common/types'; import { LoadingState } from '../../../types'; import { ReindexFlyout } from './flyout'; import { ReindexPollingService, ReindexState } from './polling_service'; @@ -27,7 +23,7 @@ interface ReindexButtonProps { indexName: string; http: HttpSetup; docLinks: DocLinksStart; - reindexBlocker?: EnrichedDeprecationInfo['blockerForReindexing']; + reindexBlocker?: ReindexAction['blockerForReindexing']; } interface ReindexButtonState { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/container.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/container.tsx index 3e7b93145256..97031dd08ee2 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/container.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/container.tsx @@ -19,7 +19,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { EnrichedDeprecationInfo, ReindexStatus } from '../../../../../../../common/types'; +import { ReindexAction, ReindexStatus } from '../../../../../../../common/types'; import { ReindexState } from '../polling_service'; import { ChecklistFlyoutStep } from './checklist_step'; @@ -37,7 +37,7 @@ interface ReindexFlyoutProps { startReindex: () => void; cancelReindex: () => void; docLinks: DocLinksStart; - reindexBlocker?: EnrichedDeprecationInfo['blockerForReindexing']; + reindexBlocker?: ReindexAction['blockerForReindexing']; } interface ReindexFlyoutState { diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts index 1c42c249e9d5..c4d9128baa56 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts @@ -90,6 +90,38 @@ export class ApiService { return result; } + + public async upgradeMlSnapshot(body: { jobId: string; snapshotId: string }) { + const result = await this.sendRequest({ + path: `${API_BASE_PATH}/ml_snapshots`, + method: 'post', + body, + }); + + return result; + } + + public async deleteMlSnapshot({ jobId, snapshotId }: { jobId: string; snapshotId: string }) { + const result = await this.sendRequest({ + path: `${API_BASE_PATH}/ml_snapshots/${jobId}/${snapshotId}`, + method: 'delete', + }); + + return result; + } + + public async getMlSnapshotUpgradeStatus({ + jobId, + snapshotId, + }: { + jobId: string; + snapshotId: string; + }) { + return await this.sendRequest({ + path: `${API_BASE_PATH}/ml_snapshots/${jobId}/${snapshotId}`, + method: 'get', + }); + } } export const apiService = new ApiService(); diff --git a/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts b/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts index 73e5d33e6c96..8cd9f8b6591e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts @@ -14,7 +14,6 @@ import { breadcrumbService } from './lib/breadcrumbs'; export async function mountManagementSection( coreSetup: CoreSetup, - isCloudEnabled: boolean, params: ManagementAppMountParams, kibanaVersionInfo: KibanaVersionContext, readonly: boolean @@ -31,7 +30,6 @@ export async function mountManagementSection( return renderApp({ element, - isCloudEnabled, http, i18n, docLinks, diff --git a/x-pack/plugins/upgrade_assistant/public/plugin.ts b/x-pack/plugins/upgrade_assistant/public/plugin.ts index 4f5429201f30..4cffd40faf38 100644 --- a/x-pack/plugins/upgrade_assistant/public/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/public/plugin.ts @@ -9,19 +9,17 @@ import SemVer from 'semver/classes/semver'; import { i18n } from '@kbn/i18n'; import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/public'; -import { CloudSetup } from '../../cloud/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; import { Config } from '../common/config'; interface Dependencies { - cloud: CloudSetup; management: ManagementSetup; } export class UpgradeAssistantUIPlugin implements Plugin { constructor(private ctx: PluginInitializerContext) {} - setup(coreSetup: CoreSetup, { cloud, management }: Dependencies) { + setup(coreSetup: CoreSetup, { management }: Dependencies) { const { enabled, readonly } = this.ctx.config.get(); if (!enabled) { @@ -29,7 +27,6 @@ export class UpgradeAssistantUIPlugin implements Plugin { } const appRegistrar = management.sections.section.stack; - const isCloudEnabled = Boolean(cloud?.isCloudEnabled); const kibanaVersion = new SemVer(this.ctx.env.packageInfo.version); const kibanaVersionInfo = { @@ -59,7 +56,6 @@ export class UpgradeAssistantUIPlugin implements Plugin { const { mountManagementSection } = await import('./application/mount_management_section'); const unmountAppCallback = await mountManagementSection( coreSetup, - isCloudEnabled, params, kibanaVersionInfo, readonly diff --git a/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json index 10a5d39f5cec..2b8519d75cb2 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json +++ b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json @@ -19,6 +19,12 @@ "message": "Datafeed [deprecation-datafeed] uses deprecated query options", "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-7.0.html#breaking_70_search_changes", "details": "[Deprecated field [use_dis_max] used, replaced by [Set [tie_breaker] to 1 instead]]" + }, + { + "level": "critical", + "message": "model snapshot [1] for job [deprecation_check_job] needs to be deleted or upgraded", + "url": "", + "details": "details" } ], "node_settings": [ @@ -46,6 +52,33 @@ "details": "[[type: tweet, field: liked]]" } ], + "old_index": [ + { + "level": "critical", + "message": "Index created before 7.0", + "url": + "https: //www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html", + "details": "This index was created using version: 6.8.13" + } + ], + "closed_index": [ + { + "level": "critical", + "message": "Index created before 7.0", + "url": "https: //www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html", + "details": "This index was created using version: 6.8.13" + } + ], + "deprecated_settings": [ + { + "level": "warning", + "message": "translog retention settings are ignored", + "url": + "https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-translog.html", + "details": + "translog retention settings [index.translog.retention.size] and [index.translog.retention.age] are ignored because translog is no longer used in peer recoveries with soft-deletes enabled (default in 7.0 or later)" + } + ], ".kibana": [ { "level": "warning", @@ -79,4 +112,4 @@ } ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/__snapshots__/es_migration_apis.test.ts.snap b/x-pack/plugins/upgrade_assistant/server/lib/__snapshots__/es_migration_apis.test.ts.snap index aefac2b4c63f..a7890adf1f0e 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/__snapshots__/es_migration_apis.test.ts.snap +++ b/x-pack/plugins/upgrade_assistant/server/lib/__snapshots__/es_migration_apis.test.ts.snap @@ -4,24 +4,39 @@ exports[`getUpgradeAssistantStatus returns the correct shape of data 1`] = ` Object { "cluster": Array [ Object { + "correctiveAction": undefined, "details": "templates using \`template\` field: security_audit_log,watches,.monitoring-alerts,triggered_watches,.ml-anomalies-,.ml-notifications,.ml-meta,.monitoring-kibana,.monitoring-es,.monitoring-logstash,.watch-history-6,.ml-state,security-index-template", "level": "warning", "message": "Template patterns are no longer using \`template\` field, but \`index_patterns\` instead", "url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_indices_changes.html#_index_templates_use_literal_index_patterns_literal_instead_of_literal_template_literal", }, Object { + "correctiveAction": undefined, "details": "{.monitoring-logstash=[Coercion of boolean fields], .monitoring-es=[Coercion of boolean fields], .ml-anomalies-=[Coercion of boolean fields], .watch-history-6=[Coercion of boolean fields], .monitoring-kibana=[Coercion of boolean fields], security-index-template=[Coercion of boolean fields]}", "level": "warning", "message": "one or more templates use deprecated mapping settings", "url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_indices_changes.html", }, Object { + "correctiveAction": undefined, "details": "[Deprecated field [use_dis_max] used, replaced by [Set [tie_breaker] to 1 instead]]", "level": "warning", "message": "Datafeed [deprecation-datafeed] uses deprecated query options", "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-7.0.html#breaking_70_search_changes", }, Object { + "correctiveAction": Object { + "jobId": "deprecation_check_job", + "snapshotId": "1", + "type": "mlSnapshot", + }, + "details": "details", + "level": "critical", + "message": "model snapshot [1] for job [deprecation_check_job] needs to be deleted or upgraded", + "url": "", + }, + Object { + "correctiveAction": undefined, "details": "This node thing is wrong", "level": "critical", "message": "A node-level issue", @@ -30,63 +45,87 @@ Object { ], "indices": Array [ Object { - "blockerForReindexing": undefined, - "deprecatedIndexSettings": Array [], + "correctiveAction": undefined, "details": "[[type: doc, field: spins], [type: doc, field: mlockall], [type: doc, field: node_master], [type: doc, field: primary]]", "index": ".monitoring-es-6-2018.11.07", "level": "warning", "message": "Coercion of boolean fields", - "reindex": false, "url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields", }, Object { - "blockerForReindexing": undefined, - "deprecatedIndexSettings": Array [], + "correctiveAction": undefined, "details": "[[type: tweet, field: liked]]", "index": "twitter", "level": "warning", "message": "Coercion of boolean fields", - "reindex": false, "url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields", }, Object { - "blockerForReindexing": undefined, - "deprecatedIndexSettings": Array [], + "correctiveAction": Object { + "blockerForReindexing": undefined, + "type": "reindex", + }, + "details": "This index was created using version: 6.8.13", + "index": "old_index", + "level": "critical", + "message": "Index created before 7.0", + "url": "https: //www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html", + }, + Object { + "correctiveAction": Object { + "blockerForReindexing": "index-closed", + "type": "reindex", + }, + "details": "This index was created using version: 6.8.13", + "index": "closed_index", + "level": "critical", + "message": "Index created before 7.0", + "url": "https: //www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html", + }, + Object { + "correctiveAction": Object { + "deprecatedSettings": Array [ + "translog.retention.size", + "translog.retention.age", + ], + "type": "indexSetting", + }, + "details": "translog retention settings [index.translog.retention.size] and [index.translog.retention.age] are ignored because translog is no longer used in peer recoveries with soft-deletes enabled (default in 7.0 or later)", + "index": "deprecated_settings", + "level": "warning", + "message": "translog retention settings are ignored", + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-translog.html", + }, + Object { + "correctiveAction": undefined, "details": "[[type: index-pattern, field: notExpandable], [type: config, field: xPackMonitoring:allowReport], [type: config, field: xPackMonitoring:showBanner], [type: dashboard, field: pause], [type: dashboard, field: timeRestore]]", "index": ".kibana", "level": "warning", "message": "Coercion of boolean fields", - "reindex": false, "url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields", }, Object { - "blockerForReindexing": undefined, - "deprecatedIndexSettings": Array [], + "correctiveAction": undefined, "details": "[[type: doc, field: notify], [type: doc, field: created], [type: doc, field: attach_payload], [type: doc, field: met]]", "index": ".watcher-history-6-2018.11.07", "level": "warning", "message": "Coercion of boolean fields", - "reindex": false, "url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields", }, Object { - "blockerForReindexing": undefined, - "deprecatedIndexSettings": Array [], + "correctiveAction": undefined, "details": "[[type: doc, field: snapshot]]", "index": ".monitoring-kibana-6-2018.11.07", "level": "warning", "message": "Coercion of boolean fields", - "reindex": false, "url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields", }, Object { - "blockerForReindexing": undefined, - "deprecatedIndexSettings": Array [], + "correctiveAction": undefined, "details": "[[type: tweet, field: liked]]", "index": "twitter2", "level": "warning", "message": "Coercion of boolean fields", - "reindex": false, "url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields", }, ], diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts index d78af9162e92..6477ce738c08 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts @@ -22,7 +22,13 @@ const asApiResponse = (body: T): RequestEvent => describe('getUpgradeAssistantStatus', () => { const resolvedIndices = { - indices: fakeIndexNames.map((f) => ({ name: f, attributes: ['open'] })), + indices: fakeIndexNames.map((indexName) => { + // mark one index as closed to test blockerForReindexing flag + if (indexName === 'closed_index') { + return { name: indexName, attributes: ['closed'] }; + } + return { name: indexName, attributes: ['open'] }; + }), }; // @ts-expect-error mock data is too loosely typed @@ -39,12 +45,12 @@ describe('getUpgradeAssistantStatus', () => { esClient.asCurrentUser.indices.resolveIndex.mockResolvedValue(asApiResponse(resolvedIndices)); it('calls /_migration/deprecations', async () => { - await getUpgradeAssistantStatus(esClient, false); + await getUpgradeAssistantStatus(esClient); expect(esClient.asCurrentUser.migration.deprecations).toHaveBeenCalled(); }); it('returns the correct shape of data', async () => { - const resp = await getUpgradeAssistantStatus(esClient, false); + const resp = await getUpgradeAssistantStatus(esClient); expect(resp).toMatchSnapshot(); }); @@ -59,7 +65,7 @@ describe('getUpgradeAssistantStatus', () => { }) ); - await expect(getUpgradeAssistantStatus(esClient, false)).resolves.toHaveProperty( + await expect(getUpgradeAssistantStatus(esClient)).resolves.toHaveProperty( 'readyForUpgrade', false ); @@ -76,32 +82,9 @@ describe('getUpgradeAssistantStatus', () => { }) ); - await expect(getUpgradeAssistantStatus(esClient, false)).resolves.toHaveProperty( + await expect(getUpgradeAssistantStatus(esClient)).resolves.toHaveProperty( 'readyForUpgrade', true ); }); - - it('filters out security realm deprecation on Cloud', async () => { - esClient.asCurrentUser.migration.deprecations.mockResolvedValue( - // @ts-expect-error not full interface - asApiResponse({ - cluster_settings: [ - { - level: 'critical', - message: 'Security realm settings structure changed', - url: 'https://...', - }, - ], - node_settings: [], - ml_settings: [], - index_settings: {}, - }) - ); - - const result = await getUpgradeAssistantStatus(esClient, true); - - expect(result).toHaveProperty('readyForUpgrade', true); - expect(result).toHaveProperty('cluster', []); - }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts index e775190d426d..85cde9069d60 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts @@ -16,26 +16,12 @@ import { import { esIndicesStateCheck } from './es_indices_state_check'; export async function getUpgradeAssistantStatus( - dataClient: IScopedClusterClient, - isCloudEnabled: boolean + dataClient: IScopedClusterClient ): Promise { const { body: deprecations } = await dataClient.asCurrentUser.migration.deprecations(); - const cluster = getClusterDeprecations(deprecations, isCloudEnabled); - const indices = getCombinedIndexInfos(deprecations); - - const indexNames = indices.map(({ index }) => index!); - - // If we have found deprecation information for index/indices check whether the index is - // open or closed. - if (indexNames.length) { - const indexStates = await esIndicesStateCheck(dataClient.asCurrentUser, indexNames); - - indices.forEach((indexData) => { - indexData.blockerForReindexing = - indexStates[indexData.index!] === 'closed' ? 'index-closed' : undefined; - }); - } + const cluster = getClusterDeprecations(deprecations); + const indices = await getCombinedIndexInfos(deprecations, dataClient); const criticalWarnings = cluster.concat(indices).filter((d) => d.level === 'critical'); @@ -47,38 +33,91 @@ export async function getUpgradeAssistantStatus( } // Reformats the index deprecations to an array of deprecation warnings extended with an index field. -const getCombinedIndexInfos = (deprecations: DeprecationAPIResponse) => - Object.keys(deprecations.index_settings).reduce((indexDeprecations, indexName) => { - return indexDeprecations.concat( - deprecations.index_settings[indexName].map( - (d) => - ({ - ...d, - index: indexName, - reindex: /Index created before/.test(d.message), - deprecatedIndexSettings: getIndexSettingDeprecations(d.message), - } as EnrichedDeprecationInfo) - ) - ); - }, [] as EnrichedDeprecationInfo[]); +const getCombinedIndexInfos = async ( + deprecations: DeprecationAPIResponse, + dataClient: IScopedClusterClient +) => { + const indices = Object.keys(deprecations.index_settings).reduce( + (indexDeprecations, indexName) => { + return indexDeprecations.concat( + deprecations.index_settings[indexName].map( + (d) => + ({ + ...d, + index: indexName, + correctiveAction: getCorrectiveAction(d.message), + } as EnrichedDeprecationInfo) + ) + ); + }, + [] as EnrichedDeprecationInfo[] + ); -const getClusterDeprecations = (deprecations: DeprecationAPIResponse, isCloudEnabled: boolean) => { - const combined = deprecations.cluster_settings + const indexNames = indices.map(({ index }) => index!); + + // If we have found deprecation information for index/indices + // check whether the index is open or closed. + if (indexNames.length) { + const indexStates = await esIndicesStateCheck(dataClient.asCurrentUser, indexNames); + + indices.forEach((indexData) => { + if (indexData.correctiveAction?.type === 'reindex') { + indexData.correctiveAction.blockerForReindexing = + indexStates[indexData.index!] === 'closed' ? 'index-closed' : undefined; + } + }); + } + return indices as EnrichedDeprecationInfo[]; +}; + +const getClusterDeprecations = (deprecations: DeprecationAPIResponse) => { + const combinedDeprecations = deprecations.cluster_settings .concat(deprecations.ml_settings) .concat(deprecations.node_settings); - if (isCloudEnabled) { - // In Cloud, this is changed at upgrade time. Filter it out to improve upgrade UX. - return combined.filter((d) => d.message !== 'Security realm settings structure changed'); - } else { - return combined; - } + return combinedDeprecations.map((deprecation) => { + return { + ...deprecation, + correctiveAction: getCorrectiveAction(deprecation.message), + }; + }) as EnrichedDeprecationInfo[]; }; -const getIndexSettingDeprecations = (message: string) => { - const indexDeprecation = Object.values(indexSettingDeprecations).find( +const getCorrectiveAction = (message: string) => { + const indexSettingDeprecation = Object.values(indexSettingDeprecations).find( ({ deprecationMessage }) => deprecationMessage === message ); + const requiresReindexAction = /Index created before/.test(message); + const requiresIndexSettingsAction = Boolean(indexSettingDeprecation); + const requiresMlAction = /model snapshot/.test(message); - return indexDeprecation?.settings || []; + if (requiresReindexAction) { + return { + type: 'reindex', + }; + } + + if (requiresIndexSettingsAction) { + return { + type: 'indexSetting', + deprecatedSettings: indexSettingDeprecation!.settings, + }; + } + + if (requiresMlAction) { + // This logic is brittle, as we are expecting the message to be in a particular format to extract the snapshot ID and job ID + // Implementing https://github.com/elastic/elasticsearch/issues/73089 in ES should address this concern + const regex = /(?<=\[).*?(?=\])/g; + const matches = message.match(regex); + + if (matches?.length === 2) { + return { + type: 'mlSnapshot', + snapshotId: matches[0], + jobId: matches[1], + }; + } + } + + return undefined; }; diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index ae5975c2bc8a..50b7330b4d46 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -17,7 +17,6 @@ import { SavedObjectsServiceStart, } from '../../../../src/core/server'; -import { CloudSetup } from '../../cloud/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; @@ -25,12 +24,13 @@ import { CredentialStore, credentialStoreFactory } from './lib/reindexing/creden import { ReindexWorker } from './lib/reindexing'; import { registerUpgradeAssistantUsageCollector } from './lib/telemetry'; import { versionService } from './lib/version'; -import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; -import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; -import { registerReindexIndicesRoutes, createReindexWorker } from './routes/reindex_indices'; -import { registerTelemetryRoutes } from './routes/telemetry'; -import { registerUpdateSettingsRoute } from './routes/update_index_settings'; -import { telemetrySavedObjectType, reindexOperationSavedObjectType } from './saved_object_types'; +import { createReindexWorker } from './routes/reindex_indices'; +import { registerRoutes } from './routes/register_routes'; +import { + telemetrySavedObjectType, + reindexOperationSavedObjectType, + mlSavedObjectType, +} from './saved_object_types'; import { RouteDependencies } from './types'; @@ -38,7 +38,6 @@ interface PluginsSetup { usageCollection: UsageCollectionSetup; licensing: LicensingPluginSetup; features: FeaturesPluginSetup; - cloud?: CloudSetup; } export class UpgradeAssistantServerPlugin implements Plugin { @@ -68,12 +67,13 @@ export class UpgradeAssistantServerPlugin implements Plugin { setup( { http, getStartServices, capabilities, savedObjects }: CoreSetup, - { usageCollection, cloud, features, licensing }: PluginsSetup + { usageCollection, features, licensing }: PluginsSetup ) { this.licensing = licensing; savedObjects.registerType(reindexOperationSavedObjectType); savedObjects.registerType(telemetrySavedObjectType); + savedObjects.registerType(mlSavedObjectType); features.registerElasticsearchFeature({ id: 'upgrade_assistant', @@ -91,7 +91,6 @@ export class UpgradeAssistantServerPlugin implements Plugin { const router = http.createRouter(); const dependencies: RouteDependencies = { - cloud, router, credentialStore: this.credentialStore, log: this.logger, @@ -107,12 +106,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { // Initialize version service with current kibana version versionService.setup(this.kibanaVersion); - registerClusterCheckupRoutes(dependencies); - registerDeprecationLoggingRoutes(dependencies); - registerReindexIndicesRoutes(dependencies, this.getWorker.bind(this)); - // Bootstrap the needed routes and the collector for the telemetry - registerTelemetryRoutes(dependencies); - registerUpdateSettingsRoute(dependencies); + registerRoutes(dependencies, this.getWorker.bind(this)); if (usageCollection) { getStartServices().then(([{ savedObjects: savedObjectsService, elasticsearch }]) => { diff --git a/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts index 3aabae87c06b..09da52e4b6ff 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts @@ -49,6 +49,7 @@ export const createMockRouter = () => { post: assign('post'), put: assign('put'), patch: assign('patch'), + delete: assign('delete'), }; }; diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts index a5da4741b10e..934fdb1c4eb3 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts @@ -32,9 +32,6 @@ describe('cluster checkup API', () => { beforeEach(() => { mockRouter = createMockRouter(); routeDependencies = { - cloud: { - isCloudEnabled: true, - }, router: mockRouter, }; registerClusterCheckupRoutes(routeDependencies); @@ -44,24 +41,6 @@ describe('cluster checkup API', () => { jest.resetAllMocks(); }); - describe('with cloud enabled', () => { - it('is provided to getUpgradeAssistantStatus', async () => { - const spy = jest.spyOn(MigrationApis, 'getUpgradeAssistantStatus'); - - MigrationApis.getUpgradeAssistantStatus.mockResolvedValue({ - cluster: [], - indices: [], - nodes: [], - }); - - await routeDependencies.router.getHandler({ - method: 'get', - pathPattern: '/api/upgrade_assistant/status', - })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); - expect(spy.mock.calls[0][1]).toBe(true); - }); - }); - describe('GET /api/upgrade_assistant/reindex/{indexName}.json', () => { it('returns state', async () => { MigrationApis.getUpgradeAssistantStatus.mockResolvedValue({ diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts index fe5b9baef6c8..31026be55fa3 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts @@ -12,9 +12,7 @@ import { RouteDependencies } from '../types'; import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; import { reindexServiceFactory } from '../lib/reindexing'; -export function registerClusterCheckupRoutes({ cloud, router, licensing, log }: RouteDependencies) { - const isCloudEnabled = Boolean(cloud?.isCloudEnabled); - +export function registerClusterCheckupRoutes({ router, licensing, log }: RouteDependencies) { router.get( { path: `${API_BASE_PATH}/status`, @@ -32,7 +30,7 @@ export function registerClusterCheckupRoutes({ cloud, router, licensing, log }: response ) => { try { - const status = await getUpgradeAssistantStatus(client, isCloudEnabled); + const status = await getUpgradeAssistantStatus(client); const asCurrentUser = client.asCurrentUser; const reindexActions = reindexActionsFactory(savedObjectsClient, asCurrentUser); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts new file mode 100644 index 000000000000..741f704adac9 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts @@ -0,0 +1,365 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; +import { createRequestMock } from './__mocks__/request.mock'; +import { registerMlSnapshotRoutes } from './ml_snapshots'; + +jest.mock('../lib/es_version_precheck', () => ({ + versionCheckHandlerWrapper: (handler: RequestHandler) => handler, +})); + +const JOB_ID = 'job_id'; +const SNAPSHOT_ID = 'snapshot_id'; +const NODE_ID = 'node_id'; + +describe('ML snapshots APIs', () => { + let mockRouter: MockRouter; + let routeDependencies: any; + + beforeEach(() => { + mockRouter = createMockRouter(); + routeDependencies = { + router: mockRouter, + }; + registerMlSnapshotRoutes(routeDependencies); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('POST /api/upgrade_assistant/ml_snapshots', () => { + it('returns 200 status and in_progress status', async () => { + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.ml + .upgradeJobSnapshot as jest.Mock).mockResolvedValue({ + body: { + node: NODE_ID, + completed: false, + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/ml_snapshots', + })( + routeHandlerContextMock, + createRequestMock({ + body: { + snapshotId: SNAPSHOT_ID, + jobId: JOB_ID, + }, + }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ + jobId: JOB_ID, + nodeId: NODE_ID, + snapshotId: SNAPSHOT_ID, + status: 'in_progress', + }); + }); + + it('returns 200 status and complete status', async () => { + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.ml + .upgradeJobSnapshot as jest.Mock).mockResolvedValue({ + body: { + node: NODE_ID, + completed: true, + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/ml_snapshots', + })( + routeHandlerContextMock, + createRequestMock({ + body: { + snapshotId: SNAPSHOT_ID, + jobId: JOB_ID, + }, + }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ + jobId: JOB_ID, + nodeId: NODE_ID, + snapshotId: SNAPSHOT_ID, + status: 'complete', + }); + }); + + it('returns an error if it throws', async () => { + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.ml + .upgradeJobSnapshot as jest.Mock).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/ml_snapshots', + })( + routeHandlerContextMock, + createRequestMock({ + body: { + snapshotId: SNAPSHOT_ID, + jobId: JOB_ID, + }, + }), + kibanaResponseFactory + ) + ).rejects.toThrow('scary error!'); + }); + }); + + describe('DELETE /api/upgrade_assistant/ml_snapshots/:jobId/:snapshotId', () => { + it('returns 200 status', async () => { + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.ml + .deleteModelSnapshot as jest.Mock).mockResolvedValue({ + body: { acknowledged: true }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'delete', + pathPattern: '/api/upgrade_assistant/ml_snapshots/{jobId}/{snapshotId}', + })( + routeHandlerContextMock, + createRequestMock({ + params: { snapshotId: 'snapshot_id1', jobId: 'job_id1' }, + }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ + acknowledged: true, + }); + }); + + it('returns an error if it throws', async () => { + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.ml + .deleteModelSnapshot as jest.Mock).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'delete', + pathPattern: '/api/upgrade_assistant/ml_snapshots/{jobId}/{snapshotId}', + })( + routeHandlerContextMock, + createRequestMock({ + params: { snapshotId: 'snapshot_id1', jobId: 'job_id1' }, + }), + kibanaResponseFactory + ) + ).rejects.toThrow('scary error!'); + }); + }); + + describe('GET /api/upgrade_assistant/ml_snapshots/:jobId/:snapshotId', () => { + it('returns "idle" status if saved object does not exist', async () => { + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.ml + .getModelSnapshots as jest.Mock).mockResolvedValue({ + body: { + count: 1, + model_snapshots: [ + { + job_id: JOB_ID, + min_version: '6.4.0', + timestamp: 1575402237000, + description: 'State persisted due to job close at 2019-12-03T19:43:57+0000', + snapshot_id: SNAPSHOT_ID, + snapshot_doc_count: 1, + model_size_stats: {}, + latest_record_time_stamp: 1576971072000, + latest_result_time_stamp: 1576965600000, + retain: false, + }, + ], + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/ml_snapshots/{jobId}/{snapshotId}', + })( + routeHandlerContextMock, + createRequestMock({ + params: { + snapshotId: SNAPSHOT_ID, + jobId: JOB_ID, + }, + }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ + jobId: JOB_ID, + nodeId: undefined, + snapshotId: SNAPSHOT_ID, + status: 'idle', + }); + }); + + it('returns "in_progress" status if snapshot upgrade is in progress', async () => { + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.ml + .getModelSnapshots as jest.Mock).mockResolvedValue({ + body: { + count: 1, + model_snapshots: [ + { + job_id: JOB_ID, + min_version: '6.4.0', + timestamp: 1575402237000, + description: 'State persisted due to job close at 2019-12-03T19:43:57+0000', + snapshot_id: SNAPSHOT_ID, + snapshot_doc_count: 1, + model_size_stats: {}, + latest_record_time_stamp: 1576971072000, + latest_result_time_stamp: 1576965600000, + retain: false, + }, + ], + }, + }); + + (routeHandlerContextMock.core.savedObjects.client.find as jest.Mock).mockResolvedValue({ + total: 1, + saved_objects: [ + { + attributes: { + nodeId: NODE_ID, + jobId: JOB_ID, + snapshotId: SNAPSHOT_ID, + }, + }, + ], + }); + + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.tasks + .list as jest.Mock).mockResolvedValue({ + body: { + nodes: { + [NODE_ID]: { + tasks: { + [`${NODE_ID}:12345`]: { + description: `job-snapshot-upgrade-${JOB_ID}-${SNAPSHOT_ID}`, + }, + }, + }, + }, + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/ml_snapshots/{jobId}/{snapshotId}', + })( + routeHandlerContextMock, + createRequestMock({ + params: { + snapshotId: SNAPSHOT_ID, + jobId: JOB_ID, + }, + }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ + jobId: JOB_ID, + nodeId: NODE_ID, + snapshotId: SNAPSHOT_ID, + status: 'in_progress', + }); + }); + + it('returns "complete" status if snapshot upgrade has completed', async () => { + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.ml + .getModelSnapshots as jest.Mock).mockResolvedValue({ + body: { + count: 1, + model_snapshots: [ + { + job_id: JOB_ID, + min_version: '6.4.0', + timestamp: 1575402237000, + description: 'State persisted due to job close at 2019-12-03T19:43:57+0000', + snapshot_id: SNAPSHOT_ID, + snapshot_doc_count: 1, + model_size_stats: {}, + latest_record_time_stamp: 1576971072000, + latest_result_time_stamp: 1576965600000, + retain: false, + }, + ], + }, + }); + + (routeHandlerContextMock.core.savedObjects.client.find as jest.Mock).mockResolvedValue({ + total: 1, + saved_objects: [ + { + attributes: { + nodeId: NODE_ID, + jobId: JOB_ID, + snapshotId: SNAPSHOT_ID, + }, + }, + ], + }); + + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.tasks + .list as jest.Mock).mockResolvedValue({ + body: { + nodes: { + [NODE_ID]: { + tasks: {}, + }, + }, + }, + }); + + (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.migration + .deprecations as jest.Mock).mockResolvedValue({ + body: { + cluster_settings: [], + ml_settings: [], + node_settings: [], + index_settings: {}, + }, + }); + + (routeHandlerContextMock.core.savedObjects.client.delete as jest.Mock).mockResolvedValue({}); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/ml_snapshots/{jobId}/{snapshotId}', + })( + routeHandlerContextMock, + createRequestMock({ + params: { + snapshotId: SNAPSHOT_ID, + jobId: JOB_ID, + }, + }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ + jobId: JOB_ID, + nodeId: NODE_ID, + snapshotId: SNAPSHOT_ID, + status: 'complete', + }); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts new file mode 100644 index 000000000000..80f5f2eb60e0 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts @@ -0,0 +1,348 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { schema } from '@kbn/config-schema'; +import { IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; +import { API_BASE_PATH } from '../../common/constants'; +import { MlOperation, ML_UPGRADE_OP_TYPE } from '../../common/types'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { handleEsError } from '../shared_imports'; +import { RouteDependencies } from '../types'; + +const findMlOperation = async ( + savedObjectsClient: SavedObjectsClientContract, + snapshotId: string +) => { + return savedObjectsClient.find({ + type: ML_UPGRADE_OP_TYPE, + search: `"${snapshotId}"`, + searchFields: ['snapshotId'], + }); +}; + +const createMlOperation = async ( + savedObjectsClient: SavedObjectsClientContract, + attributes: MlOperation +) => { + const foundSnapshots = await findMlOperation(savedObjectsClient, attributes.snapshotId); + + if (foundSnapshots?.total > 0) { + throw new Error(`A ML operation is already in progress for snapshot: ${attributes.snapshotId}`); + } + + return savedObjectsClient.create(ML_UPGRADE_OP_TYPE, attributes); +}; + +const deleteMlOperation = (savedObjectsClient: SavedObjectsClientContract, id: string) => { + return savedObjectsClient.delete(ML_UPGRADE_OP_TYPE, id); +}; + +/* + * The tasks API can only tell us if the snapshot upgrade is in progress. + * We cannot rely on it to determine if a snapshot was upgraded successfully. + * If the task does not exist, it can mean one of two things: + * 1. The snapshot was upgraded successfully. + * 2. There was a failure upgrading the snapshot. + * In order to verify it was successful, we need to recheck the deprecation info API + * and verify the deprecation no longer exists. If it still exists, we assume there was a failure. + */ +const verifySnapshotUpgrade = async ( + esClient: IScopedClusterClient, + snapshot: { snapshotId: string; jobId: string } +): Promise<{ + isSuccessful: boolean; + error?: ResponseError; +}> => { + const { snapshotId, jobId } = snapshot; + + try { + const { body: deprecations } = await esClient.asCurrentUser.migration.deprecations(); + + const mlSnapshotDeprecations = deprecations.ml_settings.filter((deprecation) => { + return /model snapshot/.test(deprecation.message); + }); + + // If there are no ML deprecations, we assume the deprecation was resolved successfully + if (typeof mlSnapshotDeprecations === 'undefined' || mlSnapshotDeprecations.length === 0) { + return { + isSuccessful: true, + }; + } + + const isSuccessful = Boolean( + mlSnapshotDeprecations.find((snapshotDeprecation) => { + const regex = /(?<=\[).*?(?=\])/g; + const matches = snapshotDeprecation.message.match(regex); + + if (matches?.length === 2) { + // If there is no matching snapshot, we assume the deprecation was resolved successfully + return matches[0] === snapshotId && matches[1] === jobId ? false : true; + } + + return false; + }) + ); + + return { + isSuccessful, + }; + } catch (e) { + return { + isSuccessful: false, + error: e, + }; + } +}; + +export function registerMlSnapshotRoutes({ router }: RouteDependencies) { + // Upgrade ML model snapshot + router.post( + { + path: `${API_BASE_PATH}/ml_snapshots`, + validate: { + body: schema.object({ + snapshotId: schema.string(), + jobId: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + savedObjects: { client: savedObjectsClient }, + elasticsearch: { client: esClient }, + }, + }, + request, + response + ) => { + try { + const { snapshotId, jobId } = request.body; + + const { body } = await esClient.asCurrentUser.ml.upgradeJobSnapshot({ + job_id: jobId, + snapshot_id: snapshotId, + }); + + const snapshotInfo: MlOperation = { + nodeId: body.node, + snapshotId, + jobId, + }; + + // Store snapshot in saved object if upgrade not complete + if (body.completed !== true) { + await createMlOperation(savedObjectsClient, snapshotInfo); + } + + return response.ok({ + body: { + ...snapshotInfo, + status: body.completed === true ? 'complete' : 'in_progress', + }, + }); + } catch (e) { + return handleEsError({ error: e, response }); + } + } + ) + ); + + // Get the status of the upgrade snapshot task + router.get( + { + path: `${API_BASE_PATH}/ml_snapshots/{jobId}/{snapshotId}`, + validate: { + params: schema.object({ + snapshotId: schema.string(), + jobId: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + savedObjects: { client: savedObjectsClient }, + elasticsearch: { client: esClient }, + }, + }, + request, + response + ) => { + try { + const { snapshotId, jobId } = request.params; + + // Verify snapshot exists + await esClient.asCurrentUser.ml.getModelSnapshots({ + job_id: jobId, + snapshot_id: snapshotId, + }); + + const foundSnapshots = await findMlOperation(savedObjectsClient, snapshotId); + + // If snapshot is *not* found in SO, assume there has not been an upgrade operation started + if (typeof foundSnapshots === 'undefined' || foundSnapshots.total === 0) { + return response.ok({ + body: { + snapshotId, + jobId, + nodeId: undefined, + status: 'idle', + }, + }); + } + + const snapshotOp = foundSnapshots.saved_objects[0]; + const { nodeId } = snapshotOp.attributes; + + // Now that we have the node ID, check the upgrade snapshot task progress + const { body: taskResponse } = await esClient.asCurrentUser.tasks.list({ + nodes: [nodeId], + actions: 'xpack/ml/job/snapshot/upgrade', + detailed: true, // necessary in order to filter if there are more than 1 snapshot upgrades in progress + }); + + const nodeTaskInfo = taskResponse?.nodes && taskResponse!.nodes[nodeId]; + const snapshotInfo: MlOperation = { + ...snapshotOp.attributes, + }; + + if (nodeTaskInfo) { + // Find the correct snapshot task ID based on the task description + const snapshotTaskId = Object.keys(nodeTaskInfo.tasks).find((task) => { + // The description is in the format of "job-snapshot-upgrade--" + const taskDescription = nodeTaskInfo.tasks[task].description; + const taskSnapshotAndJobIds = taskDescription!.replace('job-snapshot-upgrade-', ''); + const taskSnapshotAndJobIdParts = taskSnapshotAndJobIds.split('-'); + const taskSnapshotId = + taskSnapshotAndJobIdParts[taskSnapshotAndJobIdParts.length - 1]; + const taskJobId = taskSnapshotAndJobIdParts.slice(0, 1).join('-'); + + return taskSnapshotId === snapshotId && taskJobId === jobId; + }); + + // If the snapshot task exists, assume the upgrade is in progress + if (snapshotTaskId && nodeTaskInfo.tasks[snapshotTaskId]) { + return response.ok({ + body: { + ...snapshotInfo, + status: 'in_progress', + }, + }); + } else { + // The task ID was not found; verify the deprecation was resolved + const { + isSuccessful: isSnapshotDeprecationResolved, + error: upgradeSnapshotError, + } = await verifySnapshotUpgrade(esClient, { + snapshotId, + jobId, + }); + + // Delete the SO; if it's complete, no need to store it anymore. If there's an error, this will give the user a chance to retry + await deleteMlOperation(savedObjectsClient, snapshotOp.id); + + if (isSnapshotDeprecationResolved) { + return response.ok({ + body: { + ...snapshotInfo, + status: 'complete', + }, + }); + } + + return response.customError({ + statusCode: upgradeSnapshotError ? upgradeSnapshotError.statusCode : 500, + body: { + message: + upgradeSnapshotError?.body?.error?.reason || + 'There was an error upgrading your snapshot. Check the Elasticsearch logs for more details.', + }, + }); + } + } else { + // No tasks found; verify the deprecation was resolved + const { + isSuccessful: isSnapshotDeprecationResolved, + error: upgradeSnapshotError, + } = await verifySnapshotUpgrade(esClient, { + snapshotId, + jobId, + }); + + // Delete the SO; if it's complete, no need to store it anymore. If there's an error, this will give the user a chance to retry + await deleteMlOperation(savedObjectsClient, snapshotOp.id); + + if (isSnapshotDeprecationResolved) { + return response.ok({ + body: { + ...snapshotInfo, + status: 'complete', + }, + }); + } + + return response.customError({ + statusCode: upgradeSnapshotError ? upgradeSnapshotError.statusCode : 500, + body: { + message: + upgradeSnapshotError?.body?.error?.reason || + 'There was an error upgrading your snapshot. Check the Elasticsearch logs for more details.', + }, + }); + } + } catch (e) { + return handleEsError({ error: e, response }); + } + } + ) + ); + + // Delete ML model snapshot + router.delete( + { + path: `${API_BASE_PATH}/ml_snapshots/{jobId}/{snapshotId}`, + validate: { + params: schema.object({ + snapshotId: schema.string(), + jobId: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + const { snapshotId, jobId } = request.params; + + const { + body: deleteSnapshotResponse, + } = await client.asCurrentUser.ml.deleteModelSnapshot({ + job_id: jobId, + snapshot_id: snapshotId, + }); + + return response.ok({ + body: deleteSnapshotResponse, + }); + } catch (e) { + return handleEsError({ error: e, response }); + } + } + ) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts new file mode 100644 index 000000000000..50cb9257462b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts @@ -0,0 +1,25 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RouteDependencies } from '../types'; + +import { registerClusterCheckupRoutes } from './cluster_checkup'; +import { registerDeprecationLoggingRoutes } from './deprecation_logging'; +import { registerReindexIndicesRoutes } from './reindex_indices'; +import { registerTelemetryRoutes } from './telemetry'; +import { registerUpdateSettingsRoute } from './update_index_settings'; +import { registerMlSnapshotRoutes } from './ml_snapshots'; +import { ReindexWorker } from '../lib/reindexing'; + +export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) { + registerClusterCheckupRoutes(dependencies); + registerDeprecationLoggingRoutes(dependencies); + registerReindexIndicesRoutes(dependencies, getWorker); + registerTelemetryRoutes(dependencies); + registerUpdateSettingsRoute(dependencies); + registerMlSnapshotRoutes(dependencies); +} diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts index 91779bd4224b..e394cac5100f 100644 --- a/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts @@ -7,3 +7,4 @@ export { reindexOperationSavedObjectType } from './reindex_operation_saved_object_type'; export { telemetrySavedObjectType } from './telemetry_saved_object_type'; +export { mlSavedObjectType } from './ml_upgrade_operation_saved_object_type'; diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/ml_upgrade_operation_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/ml_upgrade_operation_saved_object_type.ts new file mode 100644 index 000000000000..6dc70fab1203 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/ml_upgrade_operation_saved_object_type.ts @@ -0,0 +1,56 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsType } from 'src/core/server'; + +import { ML_UPGRADE_OP_TYPE } from '../../common/types'; + +export const mlSavedObjectType: SavedObjectsType = { + name: ML_UPGRADE_OP_TYPE, + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + nodeId: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + snapshotId: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + jobId: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + status: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/upgrade_assistant/server/shared_imports.ts b/x-pack/plugins/upgrade_assistant/server/shared_imports.ts new file mode 100644 index 000000000000..7f55d189457c --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/shared_imports.ts @@ -0,0 +1,8 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/plugins/upgrade_assistant/server/types.ts b/x-pack/plugins/upgrade_assistant/server/types.ts index 80c60e3f310b..b25b73070e4c 100644 --- a/x-pack/plugins/upgrade_assistant/server/types.ts +++ b/x-pack/plugins/upgrade_assistant/server/types.ts @@ -6,7 +6,6 @@ */ import { IRouter, Logger, SavedObjectsServiceStart } from 'src/core/server'; -import { CloudSetup } from '../../cloud/server'; import { CredentialStore } from './lib/reindexing/credential_store'; import { LicensingPluginSetup } from '../../licensing/server'; @@ -16,5 +15,4 @@ export interface RouteDependencies { log: Logger; getSavedObjectsService: () => SavedObjectsServiceStart; licensing: LicensingPluginSetup; - cloud?: CloudSetup; } diff --git a/x-pack/plugins/upgrade_assistant/tsconfig.json b/x-pack/plugins/upgrade_assistant/tsconfig.json index 6303b06c0d89..750bea75c665 100644 --- a/x-pack/plugins/upgrade_assistant/tsconfig.json +++ b/x-pack/plugins/upgrade_assistant/tsconfig.json @@ -20,8 +20,8 @@ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/management/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, - { "path": "../cloud/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, + { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, ] }