[Upgrade Assistant] Auto upgrade ML job model snapshots (#100066)
This commit is contained in:
parent
16da1a6dbe
commit
5df129aaad
|
@ -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.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<ClusterTestSubjects> & {
|
||||
actions: ReturnType<typeof createActions>;
|
||||
};
|
||||
|
||||
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<string, unknown>): Promise<ClusterTestBed> => {
|
||||
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;
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -33,7 +33,6 @@ export const WithAppDependencies = (Comp: any, overrides: Record<string, unknown
|
|||
|
||||
const contextValue = {
|
||||
http: (mockHttpClient as unknown) as HttpSetup,
|
||||
isCloudEnabled: false,
|
||||
docLinks: docLinksServiceMock.createStartContract(),
|
||||
kibanaVersionInfo: {
|
||||
currentMajor: mockKibanaSemverVersion.major,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { indexSettingDeprecations } from '../../common/constants';
|
||||
import { MIGRATION_DEPRECATION_LEVEL } from '../../common/types';
|
||||
import { UpgradeAssistantStatus } from '../../common/types';
|
||||
|
||||
import { IndicesTestBed, setupIndicesPage, setupEnvironment } from './helpers';
|
||||
|
||||
|
@ -20,16 +20,19 @@ describe('Indices tab', () => {
|
|||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
"ui": true,
|
||||
"configPath": ["xpack", "upgrade_assistant"],
|
||||
"requiredPlugins": ["management", "licensing", "features"],
|
||||
"optionalPlugins": ["cloud", "usageCollection"],
|
||||
"optionalPlugins": ["usageCollection"],
|
||||
"requiredBundles": ["esUiShared", "kibanaReact"]
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ export interface KibanaVersionContext {
|
|||
|
||||
export interface ContextValue {
|
||||
http: HttpSetup;
|
||||
isCloudEnabled: boolean;
|
||||
docLinks: DocLinksStart;
|
||||
kibanaVersionInfo: KibanaVersionContext;
|
||||
notifications: NotificationsStart;
|
||||
|
|
|
@ -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<CellActionProps> = ({ correctiveAction, indexName, items }) => {
|
||||
const { type: correctiveActionType } = correctiveAction!;
|
||||
switch (correctiveActionType) {
|
||||
case 'mlSnapshot':
|
||||
const { jobId, snapshotId } = correctiveAction as MlAction;
|
||||
return (
|
||||
<FixMlSnapshotsButton
|
||||
jobId={jobId}
|
||||
snapshotId={snapshotId}
|
||||
// There will only ever be a single item for the cluster deprecations list, so we can use the index to access the first one
|
||||
description={items[0]?.body}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'reindex':
|
||||
const { blockerForReindexing } = correctiveAction as ReindexAction;
|
||||
|
||||
return (
|
||||
<AppContext.Consumer>
|
||||
{({ http, docLinks }) => (
|
||||
<ReindexButton
|
||||
docLinks={docLinks}
|
||||
reindexBlocker={blockerForReindexing}
|
||||
indexName={indexName!}
|
||||
http={http}
|
||||
/>
|
||||
)}
|
||||
</AppContext.Consumer>
|
||||
);
|
||||
|
||||
case 'indexSetting':
|
||||
const { deprecatedSettings } = correctiveAction as IndexSettingAction;
|
||||
|
||||
return <FixIndexSettingsButton settings={deprecatedSettings} index={indexName!} />;
|
||||
|
||||
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<DeprecationCellProps> = ({
|
||||
headline,
|
||||
healthColor,
|
||||
reindexIndexName,
|
||||
deprecatedIndexSettings,
|
||||
correctiveAction,
|
||||
indexName,
|
||||
docUrl,
|
||||
items = [],
|
||||
children,
|
||||
reindexBlocker,
|
||||
}) => (
|
||||
<div className="upgDeprecationCell">
|
||||
<EuiFlexGroup responsive={false} wrap alignItems="baseline">
|
||||
|
@ -82,24 +132,9 @@ export const DeprecationCell: FunctionComponent<DeprecationCellProps> = ({
|
|||
)}
|
||||
</EuiFlexItem>
|
||||
|
||||
{reindexIndexName && (
|
||||
{correctiveAction && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AppContext.Consumer>
|
||||
{({ http, docLinks }) => (
|
||||
<ReindexButton
|
||||
docLinks={docLinks}
|
||||
reindexBlocker={reindexBlocker}
|
||||
indexName={reindexIndexName}
|
||||
http={http}
|
||||
/>
|
||||
)}
|
||||
</AppContext.Consumer>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{deprecatedIndexSettings?.length && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<FixIndexSettingsButton settings={deprecatedIndexSettings} index={reindexIndexName!} />
|
||||
<CellAction correctiveAction={correctiveAction} indexName={indexName} items={items} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<ReindexButton
|
||||
docLinks={docLinks}
|
||||
reindexBlocker={indexDep.blockerForReindexing}
|
||||
reindexBlocker={
|
||||
(indexDep.correctiveAction as ReindexAction).blockerForReindexing
|
||||
}
|
||||
indexName={indexDep.index!}
|
||||
http={http}
|
||||
/>
|
||||
|
@ -184,7 +188,7 @@ export class IndexDeprecationTable extends React.Component<
|
|||
|
||||
return (
|
||||
<FixIndexSettingsButton
|
||||
settings={indexDep.deprecatedIndexSettings!}
|
||||
settings={(indexDep.correctiveAction as IndexSettingAction).deprecatedSettings}
|
||||
index={indexDep.index}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -32,11 +32,10 @@ const MessageDeprecation: FunctionComponent<{
|
|||
|
||||
return (
|
||||
<DeprecationCell
|
||||
reindexBlocker={deprecation.blockerForReindexing}
|
||||
headline={deprecation.message}
|
||||
healthColor={COLOR_MAP[deprecation.level]}
|
||||
reindexIndexName={deprecation.reindex ? deprecation.index! : undefined}
|
||||
deprecatedIndexSettings={deprecation.deprecatedIndexSettings}
|
||||
correctiveAction={deprecation.correctiveAction}
|
||||
indexName={deprecation.index}
|
||||
docUrl={deprecation.url}
|
||||
items={items}
|
||||
/>
|
||||
|
@ -57,10 +56,10 @@ const SimpleMessageDeprecation: FunctionComponent<{ deprecation: EnrichedDepreca
|
|||
|
||||
return (
|
||||
<DeprecationCell
|
||||
reindexBlocker={deprecation.blockerForReindexing}
|
||||
correctiveAction={deprecation.correctiveAction}
|
||||
indexName={deprecation.index}
|
||||
items={items}
|
||||
docUrl={deprecation.url}
|
||||
deprecatedIndexSettings={deprecation.deprecatedIndexSettings}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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 <IndexDeprecation indices={indices} deprecation={deprecations[0]} />;
|
||||
} else if (currentGroupBy === GroupByOption.index) {
|
||||
|
|
|
@ -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<Props> = ({
|
||||
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 = <EuiButton {...commonButtonProps}>{i18nTexts.fixButtonLabel}</EuiButton>;
|
||||
|
||||
switch (snapshotState.status) {
|
||||
case 'in_progress':
|
||||
button = (
|
||||
<EuiButton color="secondary" {...commonButtonProps} isLoading>
|
||||
{snapshotState.action === 'delete'
|
||||
? i18nTexts.deletingButtonLabel
|
||||
: i18nTexts.upgradingButtonLabel}
|
||||
</EuiButton>
|
||||
);
|
||||
break;
|
||||
case 'complete':
|
||||
button = (
|
||||
<EuiButton color="secondary" iconType="check" {...commonButtonProps} disabled>
|
||||
{i18nTexts.doneButtonLabel}
|
||||
</EuiButton>
|
||||
);
|
||||
break;
|
||||
case 'error':
|
||||
button = (
|
||||
<EuiButton color="danger" iconType="cross" {...commonButtonProps}>
|
||||
{i18nTexts.failedButtonLabel}
|
||||
</EuiButton>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{button}
|
||||
|
||||
{showFlyout && (
|
||||
<FixSnapshotsFlyout
|
||||
snapshotState={snapshotState}
|
||||
upgradeSnapshot={upgradeSnapshot}
|
||||
deleteSnapshot={deleteSnapshot}
|
||||
description={description}
|
||||
closeFlyout={() => setShowFlyout(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<void>;
|
||||
deleteSnapshot: () => Promise<void>;
|
||||
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 (
|
||||
<EuiPortal>
|
||||
<EuiFlyout
|
||||
onClose={closeFlyout}
|
||||
ownFocus
|
||||
size="m"
|
||||
maxWidth
|
||||
data-test-subj="fixSnapshotsFlyout"
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2>{i18nTexts.flyoutTitle}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
{snapshotState.error && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={
|
||||
snapshotState.action === 'delete'
|
||||
? i18nTexts.deleteSnapshotErrorTitle
|
||||
: i18nTexts.upgradeSnapshotErrorTitle
|
||||
}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
data-test-subj="upgradeSnapshotError"
|
||||
>
|
||||
{snapshotState.error.message}
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
<EuiText>
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
|
||||
{i18nTexts.closeButtonLabel}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="deleteSnapshotButton"
|
||||
color="danger"
|
||||
onClick={onDeleteSnapshot}
|
||||
isLoading={false}
|
||||
>
|
||||
{snapshotState.action === 'delete' && snapshotState.error
|
||||
? i18nTexts.retryDeleteButtonLabel
|
||||
: i18nTexts.deleteButtonLabel}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={onUpgradeSnapshot}
|
||||
isLoading={false}
|
||||
data-test-subj="upgradeSnapshotButton"
|
||||
>
|
||||
{snapshotState.action === 'upgrade' && snapshotState.error
|
||||
? i18nTexts.retryUpgradeButtonLabel
|
||||
: i18nTexts.upgradeButtonLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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<ResponseError | undefined>(undefined);
|
||||
const [snapshotState, setSnapshotState] = useState<SnapshotStatus>({
|
||||
status: 'idle',
|
||||
jobId,
|
||||
snapshotId,
|
||||
});
|
||||
|
||||
const pollIntervalIdRef = useRef<ReturnType<typeof setTimeout> | 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,
|
||||
};
|
||||
};
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Config>();
|
||||
|
||||
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
|
||||
|
|
|
@ -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 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
|
|
|
@ -22,7 +22,13 @@ const asApiResponse = <T>(body: T): RequestEvent<T> =>
|
|||
|
||||
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', []);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,26 +16,12 @@ import {
|
|||
import { esIndicesStateCheck } from './es_indices_state_check';
|
||||
|
||||
export async function getUpgradeAssistantStatus(
|
||||
dataClient: IScopedClusterClient,
|
||||
isCloudEnabled: boolean
|
||||
dataClient: IScopedClusterClient
|
||||
): Promise<UpgradeAssistantStatus> {
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -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 }]) => {
|
||||
|
|
|
@ -49,6 +49,7 @@ export const createMockRouter = () => {
|
|||
post: assign('post'),
|
||||
put: assign('put'),
|
||||
patch: assign('patch'),
|
||||
delete: assign('delete'),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: <P, Q, B>(handler: RequestHandler<P, Q, B>) => 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
348
x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts
Normal file
348
x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts
Normal file
|
@ -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<MlOperation>({
|
||||
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<MlOperation>(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-<job_id>-<snapshot_id>"
|
||||
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 });
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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" },
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue