diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index f20c6c2d52fdd..c95c837c4959f 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -91,6 +91,12 @@ export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; export const API_LIST_URL = `${API_BASE_URL}/jobs`; export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; +export const API_GET_ILM_POLICY_STATUS = `${API_BASE_URL}/ilm_policy_status`; +export const API_CREATE_ILM_POLICY_URL = `${API_BASE_URL}/ilm_policy`; +export const API_MIGRATE_ILM_POLICY_URL = `${API_BASE_URL}/deprecations/migrate_ilm_policy`; + +export const ILM_POLICY_NAME = 'kibana-reporting'; + // hacky endpoint: download CSV without queueing a report export const API_BASE_URL_V1 = '/api/reporting/v1'; // export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv_searchsource`; diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 8205b4f13a320..6efaf42a5ad14 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -164,3 +164,9 @@ export type DownloadReportFn = (jobId: JobId) => DownloadLink; type ManagementLink = string; export type ManagementLinkFn = () => ManagementLink; + +export type IlmPolicyMigrationStatus = 'policy-not-found' | 'indices-not-managed-by-policy' | 'ok'; + +export interface IlmPolicyStatusResponse { + status: IlmPolicyMigrationStatus; +} diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index ddba61e9a0b8d..6a8f3a3e4e5ec 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -6,6 +6,7 @@ "configPath": ["xpack", "reporting"], "requiredPlugins": [ "data", + "esUiShared", "home", "management", "licensing", diff --git a/x-pack/plugins/reporting/public/lib/ilm_policy_status_context.tsx b/x-pack/plugins/reporting/public/lib/ilm_policy_status_context.tsx new file mode 100644 index 0000000000000..78b2e77d09aee --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/ilm_policy_status_context.tsx @@ -0,0 +1,43 @@ +/* + * 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 type { FunctionComponent } from 'react'; +import React, { createContext, useContext } from 'react'; + +import { IlmPolicyStatusResponse } from '../../common/types'; + +import { useCheckIlmPolicyStatus } from './reporting_api_client'; + +type UseCheckIlmPolicyStatus = ReturnType; + +interface ContextValue { + status: undefined | IlmPolicyStatusResponse['status']; + isLoading: UseCheckIlmPolicyStatus['isLoading']; + recheckStatus: UseCheckIlmPolicyStatus['resendRequest']; +} + +const IlmPolicyStatusContext = createContext(undefined); + +export const IlmPolicyStatusContextProvider: FunctionComponent = ({ children }) => { + const { isLoading, data, resendRequest: recheckStatus } = useCheckIlmPolicyStatus(); + + return ( + + {children} + + ); +}; + +export type UseIlmPolicyStatusReturn = ReturnType; + +export const useIlmPolicyStatus = (): ContextValue => { + const ctx = useContext(IlmPolicyStatusContext); + if (!ctx) { + throw new Error('"useIlmPolicyStatus" can only be used inside of "IlmPolicyStatusContext"'); + } + return ctx; +}; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx b/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx new file mode 100644 index 0000000000000..37857943774d4 --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx @@ -0,0 +1,38 @@ +/* + * 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 type { HttpSetup } from 'src/core/public'; +import type { FunctionComponent } from 'react'; +import React, { createContext, useContext } from 'react'; + +import type { ReportingAPIClient } from './reporting_api_client'; + +interface ContextValue { + http: HttpSetup; + apiClient: ReportingAPIClient; +} + +const InternalApiClientContext = createContext(undefined); + +export const InternalApiClientClientProvider: FunctionComponent<{ + http: HttpSetup; + apiClient: ReportingAPIClient; +}> = ({ http, apiClient, children }) => { + return ( + + {children} + + ); +}; + +export const useInternalApiClient = (): ContextValue => { + const ctx = useContext(InternalApiClientContext); + if (!ctx) { + throw new Error('"useInternalApiClient" can only be used inside of "InternalApiClientContext"'); + } + return ctx; +}; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts new file mode 100644 index 0000000000000..afd8222fd3831 --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts @@ -0,0 +1,18 @@ +/* + * 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 { useRequest, UseRequestResponse } from '../../shared_imports'; +import { IlmPolicyStatusResponse } from '../../../common/types'; + +import { API_GET_ILM_POLICY_STATUS } from '../../../common/constants'; + +import { useInternalApiClient } from './context'; + +export const useCheckIlmPolicyStatus = (): UseRequestResponse => { + const { http } = useInternalApiClient(); + return useRequest(http, { path: API_GET_ILM_POLICY_STATUS, method: 'get' }); +}; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/index.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/index.ts new file mode 100644 index 0000000000000..b32d675a1d209 --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/index.ts @@ -0,0 +1,12 @@ +/* + * 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 * from './reporting_api_client'; + +export * from './hooks'; + +export { InternalApiClientClientProvider, useInternalApiClient } from './context'; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts similarity index 93% rename from x-pack/plugins/reporting/public/lib/reporting_api_client.ts rename to x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts index 4ce9e8760f21f..64caac0e27bdd 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts @@ -12,8 +12,9 @@ import { API_BASE_GENERATE, API_BASE_URL, API_LIST_URL, + API_MIGRATE_ILM_POLICY_URL, REPORTING_MANAGEMENT_HOME, -} from '../../common/constants'; +} from '../../../common/constants'; import { DownloadReportFn, JobId, @@ -21,8 +22,8 @@ import { ReportApiJSON, ReportDocument, ReportSource, -} from '../../common/types'; -import { add } from '../notifier/job_completion_notifications'; +} from '../../../common/types'; +import { add } from '../../notifier/job_completion_notifications'; export interface JobQueueEntry { _id: string; @@ -167,4 +168,8 @@ export class ReportingAPIClient { this.http.post(`${API_BASE_URL}/diagnose/screenshot`, { asSystemRequest: true, }); + + public migrateReportingIndicesIlmPolicy = (): Promise => { + return this.http.put(`${API_MIGRATE_ILM_POLICY_URL}`); + }; } diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap index 93df3c8d99935..9ce249aa32a1d 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap @@ -1,82 +1,116 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ReportListing Report job listing with some items 1`] = ` -Array [ - -
-
- +
+
+ +
+
- +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ +
+ + + + + + + + + + -
-
-
- -
- -
- - - - -
+ +
- - - - - - - - - - - - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - -
+ + - -
+ + -
+ - -
- -
-
- + Report + -
-
- - - - - - Report - - - - - - + + + + + - - + - - - - Created at - - - - - - + Created at + + + + + + + + - - + - - - - Status - - - - - - + Status + + + + + + + + - - - - - - Actions - - - - - -
+ -
- Loading reports + Actions -
-
-
-
-
- , -
-
- -
+ + + + + + + + + - -
+ - -
- - -
- -
- -
- - -
- - -
- -
- -
- - - - - - - - - - - - - + + - - - + canvas workpad + + + + + + + + + - - - + Created at + +
+
+
+ 2020-04-14 @ 05:01 PM +
+ + elastic + +
+
+ + + -
- - - + - - - - - - - - + + + + + + + + - - - - - -
- -
-
- - - - + Report + +
+
+
+ My Canvas Workpad +
+ +
- - Report - - - - - -
- - - - - - Created at - - - - - - - - - - - - Status - - - - - - - - - - + Status + +
+
+ + Pending - waiting for job to be processed + +
+
+ + + +
+
+ + +
- - Actions - - - - - - - + + + + + + + + + +
+
+ + + +
+ +
+
+ + +
+ +
+
+ + +
+
+
+ Report +
+
- + My Canvas Workpad +
+ - Loading reports +
+ + + canvas workpad + + +
+
+
+ +
+
+ Created at +
+
+
+
+ 2020-04-14 @ 05:01 PM +
+ + elastic
-
-
+
+ + + + +
+ Status +
+
+
+ + 2020-04-14 @ 05:01 PM + , + } + } + > + Processing (attempt 1 of 1) at + + 2020-04-14 @ 05:01 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + + canvas workpad + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 04:19 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 04:19 PM + , + } + } + > + Completed at + + 2020-04-14 @ 04:19 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + + canvas workpad + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:20 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:21 PM + , + } + } + > + Completed with warnings at + + 2020-04-14 @ 01:21 PM + + + +
+ + + + Errors occurred: see job info for details. + + + +
+
+
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + + canvas workpad + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:19 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:19 PM + , + } + } + > + Completed at + + 2020-04-14 @ 01:19 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + + canvas workpad + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:19 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:19 PM + , + } + } + > + Completed at + + 2020-04-14 @ 01:19 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + + canvas workpad + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:17 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:18 PM + , + } + } + > + Completed at + + 2020-04-14 @ 01:18 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + + canvas workpad + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:12 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:13 PM + , + } + } + > + Completed at + + 2020-04-14 @ 01:13 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ count +
+ +
+ + + visualization + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-09 @ 03:09 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-09 @ 03:10 PM + , + } + } + > + Completed at + + 2020-04-09 @ 03:10 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + } + onPageChange={[Function]} + onPageSizeChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": true, + "pageIndex": 0, + "pageSize": 10, + "totalItemCount": 18, + } + } + > +
+ +
+ + + +
+ +
+ + +
+ + + +
+
+
+ +
-
, -] + +
`; diff --git a/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx b/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx new file mode 100644 index 0000000000000..3945ec5be9fa7 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx @@ -0,0 +1,47 @@ +/* + * 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 type { FunctionComponent } from 'react'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty } from '@elastic/eui'; +import type { ApplicationStart } from 'src/core/public'; + +import { ILM_POLICY_NAME } from '../../common/constants'; +import { LocatorPublic, SerializableState } from '../shared_imports'; + +interface Props { + navigateToUrl: ApplicationStart['navigateToUrl']; + locator: LocatorPublic; +} + +const i18nTexts = { + buttonLabel: i18n.translate('xpack.reporting.listing.reports.ilmPolicyLinkText', { + defaultMessage: 'Edit ILM policy', + }), +}; + +export const IlmPolicyLink: FunctionComponent = ({ locator, navigateToUrl }) => { + return ( + { + locator + .getUrl({ + page: 'policy_edit', + policyName: ILM_POLICY_NAME, + }) + .then((url) => { + navigateToUrl(url); + }); + }} + > + {i18nTexts.buttonLabel} + + ); +}; diff --git a/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx b/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx new file mode 100644 index 0000000000000..5bb3ac524e130 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx @@ -0,0 +1,94 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { FunctionComponent } from 'react'; +import React, { useState } from 'react'; +import { EuiCallOut, EuiButton, EuiCode } from '@elastic/eui'; + +import type { NotificationsSetup } from 'src/core/public'; + +import { ILM_POLICY_NAME } from '../../../common/constants'; + +import { useInternalApiClient } from '../../lib/reporting_api_client'; + +const i18nTexts = { + title: i18n.translate('xpack.reporting.listing.ilmPolicyCallout.migrationNeededTitle', { + defaultMessage: 'Migrate reporting indices', + }), + description: ( + {ILM_POLICY_NAME}, + }} + /> + ), + buttonLabel: i18n.translate( + 'xpack.reporting.listing.ilmPolicyCallout.migrateIndicesButtonLabel', + { + defaultMessage: 'Migrate indices', + } + ), + migrateErrorTitle: i18n.translate( + 'xpack.reporting.listing.ilmPolicyCallout.migrateIndicesErrorTitle', + { + defaultMessage: 'Could not migrate reporting indices', + } + ), + migrateSuccessTitle: i18n.translate( + 'xpack.reporting.listing.ilmPolicyCallout.migrateIndicesSuccessTitle', + { + defaultMessage: 'Successfully migrated reporting indices', + } + ), +}; + +interface Props { + toasts: NotificationsSetup['toasts']; + onMigrationDone: () => void; +} + +export const IlmPolicyMigrationNeededCallOut: FunctionComponent = ({ + toasts, + onMigrationDone, +}) => { + const [isMigratingIndices, setIsMigratingIndices] = useState(false); + + const { apiClient } = useInternalApiClient(); + + const migrateReportingIndices = async () => { + try { + setIsMigratingIndices(true); + await apiClient.migrateReportingIndicesIlmPolicy(); + onMigrationDone(); + toasts.addSuccess({ title: i18nTexts.migrateSuccessTitle }); + } catch (e) { + toasts.addError(e, { + title: i18nTexts.migrateErrorTitle, + toastMessage: e.body?.message, + }); + } finally { + setIsMigratingIndices(false); + } + }; + + return ( + +

{i18nTexts.description}

+ + {i18nTexts.buttonLabel} + +
+ ); +}; diff --git a/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/index.tsx b/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/index.tsx new file mode 100644 index 0000000000000..892cbcdde5ede --- /dev/null +++ b/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/index.tsx @@ -0,0 +1,37 @@ +/* + * 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 type { FunctionComponent } from 'react'; +import React from 'react'; +import { EuiSpacer, EuiFlexItem } from '@elastic/eui'; + +import { NotificationsSetup } from 'src/core/public'; + +import { useIlmPolicyStatus } from '../../lib/ilm_policy_status_context'; + +import { IlmPolicyMigrationNeededCallOut } from './ilm_policy_migration_needed_callout'; + +interface Props { + toasts: NotificationsSetup['toasts']; +} + +export const MigrateIlmPolicyCallOut: FunctionComponent = ({ toasts }) => { + const { isLoading, recheckStatus, status } = useIlmPolicyStatus(); + + if (isLoading || !status || status === 'ok') { + return null; + } + + return ( + <> + + + + + + ); +}; diff --git a/x-pack/plugins/reporting/public/management/mount_management_section.tsx b/x-pack/plugins/reporting/public/management/mount_management_section.tsx index eb1057a9bdfc7..8d147628c6662 100644 --- a/x-pack/plugins/reporting/public/management/mount_management_section.tsx +++ b/x-pack/plugins/reporting/public/management/mount_management_section.tsx @@ -10,10 +10,11 @@ import * as React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Observable } from 'rxjs'; import { CoreSetup, CoreStart } from 'src/core/public'; -import { ManagementAppMountParams } from '../../../../../src/plugins/management/public'; import { ILicense } from '../../../licensing/public'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ReportingAPIClient, InternalApiClientClientProvider } from '../lib/reporting_api_client'; +import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context'; import { ClientConfigType } from '../plugin'; +import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports'; import { ReportListing } from './report_listing'; export async function mountManagementSection( @@ -22,17 +23,23 @@ export async function mountManagementSection( license$: Observable, pollConfig: ClientConfigType['poll'], apiClient: ReportingAPIClient, + urlService: SharePluginSetup['url'], params: ManagementAppMountParams ) { render( - + + + + + , params.element ); diff --git a/x-pack/plugins/reporting/public/management/report_listing.test.tsx b/x-pack/plugins/reporting/public/management/report_listing.test.tsx index efc1a7dfe3b20..0b278cbaa0449 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.test.tsx @@ -7,9 +7,22 @@ import React from 'react'; import { Observable } from 'rxjs'; -import { mountWithIntl } from '@kbn/test/jest'; -import { ILicense } from '../../../licensing/public'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { UnwrapPromise } from '@kbn/utility-types'; + +import { act } from 'react-dom/test-utils'; + +import { registerTestBed } from '@kbn/test/jest'; + +import type { SharePluginSetup, LocatorPublic } from '../../../../../src/plugins/share/public'; +import type { NotificationsSetup } from '../../../../../src/core/public'; +import { httpServiceMock, notificationServiceMock } from '../../../../../src/core/public/mocks'; + +import type { ILicense } from '../../../licensing/public'; + +import { IlmPolicyMigrationStatus } from '../../common/types'; + +import { ReportingAPIClient, InternalApiClientClientProvider } from '../lib/reporting_api_client'; +import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { @@ -17,7 +30,7 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { }; }); -import { ReportListing } from './report_listing'; +import { ReportListing, Props } from './report_listing'; const reportingAPIClient = { list: () => @@ -33,6 +46,7 @@ const reportingAPIClient = { { _id: 'k8t4ylcb07mi9d006214ifyg', _index: '.reporting-2020.04.05', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-09T19:10:10.049Z', created_at: '2020-04-09T19:09:52.139Z', created_by: 'elastic', jobtype: 'PNG', kibana_id: 'f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'png', objectType: 'visualization', }, output: { content_type: 'image/png', }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-09T19:09:52.137Z', layout: { dimensions: { height: 1575, width: 1423, }, id: 'png', }, objectType: 'visualization', relativeUrl: "/s/hsyjklk/app/visualize#/edit/94d1fe40-7a94-11ea-b373-0749f92ad295?_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!((enabled:!t,id:'1',params:(),schema:metric,type:count)),params:(addLegend:!f,addTooltip:!t,metric:(colorSchema:'Green%20to%20Red',colorsRange:!((from:0,to:10000)),invertColors:!f,labels:(show:!t),metricColorMode:None,percentageMode:!f,style:(bgColor:!f,bgFill:%23000,fontSize:60,labelColor:!f,subText:''),useRanges:!f),type:metric),title:count,type:metric))&_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&indexPattern=d81752b0-7434-11ea-be36-1f978cda44d4&type=metric", title: 'count', }, priority: 10, process_expiration: '2020-04-09T19:14:54.570Z', started_at: '2020-04-09T19:09:54.570Z', status: 'completed', timeout: 300000, }, sort: [1586459392139], }, ]), // prettier-ignore total: () => Promise.resolve(18), + migrateReportingIndicesIlmPolicy: jest.fn(), } as any; const validCheck = { @@ -48,10 +62,6 @@ const license$ = { }, } as Observable; -const toasts = { - addDanger: jest.fn(), -} as any; - const mockPollConfig = { jobCompletionNotifier: { interval: 5000, @@ -64,22 +74,87 @@ const mockPollConfig = { }; describe('ReportListing', () => { - it('Report job listing with some items', () => { - const wrapper = mountWithIntl( - ; + let ilmLocator: undefined | LocatorPublic; + let urlService: SharePluginSetup['url']; + let testBed: UnwrapPromise>; + let toasts: NotificationsSetup['toasts']; + + const createTestBed = registerTestBed( + (props?: Partial) => ( + - ); - wrapper.update(); - const input = wrapper.find('[data-test-subj="reportJobListing"]'); - expect(input).toMatchSnapshot(); + http={httpService} + > + + + + + ), + { memoryRouter: { wrapComponent: false } } + ); + + const setup = async (props?: Partial) => { + const tb = await createTestBed(props); + const { find, exists, component } = tb; + + return { + ...tb, + actions: { + findListTable: () => find('reportJobListing'), + hasIlmMigrationBanner: () => exists('migrateReportingIndicesPolicyCallOut'), + hasIlmPolicyLink: () => exists('ilmPolicyLink'), + migrateIndices: async () => { + await act(async () => { + find('migrateReportingIndicesButton').simulate('click'); + }); + component.update(); + }, + }, + }; + }; + + const runSetup = async (props?: Partial) => { + await act(async () => { + testBed = await setup(props); + }); + testBed.component.update(); + }; + + beforeEach(async () => { + toasts = notificationServiceMock.createSetupContract().toasts; + httpService = httpServiceMock.createSetupContract(); + ilmLocator = ({ + getUrl: jest.fn(), + } as unknown) as LocatorPublic; + + urlService = ({ + locators: { + get: () => ilmLocator, + }, + } as unknown) as SharePluginSetup['url']; + await runSetup(); + }); + + afterEach(() => { + jest.clearAllMocks(); }); - it('subscribes to license changes, and unsubscribes on dismount', () => { + it('Report job listing with some items', () => { + const { actions } = testBed; + const table = actions.findListTable(); + expect(table).toMatchSnapshot(); + }); + + it('subscribes to license changes, and unsubscribes on dismount', async () => { const unsubscribeMock = jest.fn(); const subMock = { subscribe: jest.fn().mockReturnValue({ @@ -87,19 +162,103 @@ describe('ReportListing', () => { }), } as any; - const wrapper = mountWithIntl( - } - pollConfig={mockPollConfig} - redirect={jest.fn()} - toasts={toasts} - /> - ); - wrapper.update(); + await runSetup({ license$: subMock }); + expect(subMock.subscribe).toHaveBeenCalled(); expect(unsubscribeMock).not.toHaveBeenCalled(); - wrapper.unmount(); + testBed.component.unmount(); expect(unsubscribeMock).toHaveBeenCalled(); }); + + describe('ILM policy', () => { + beforeEach(async () => { + httpService = httpServiceMock.createSetupContract(); + ilmLocator = ({ + getUrl: jest.fn(), + } as unknown) as LocatorPublic; + + urlService = ({ + locators: { + get: () => ilmLocator, + }, + } as unknown) as SharePluginSetup['url']; + + await runSetup(); + }); + + it('shows the migrate banner when migration status is not "OK"', async () => { + const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; + httpService.get.mockResolvedValue({ status }); + await runSetup(); + const { actions } = testBed; + expect(actions.hasIlmMigrationBanner()).toBe(true); + }); + + it('does not show the migrate banner when migration status is "OK"', async () => { + const status: IlmPolicyMigrationStatus = 'ok'; + httpService.get.mockResolvedValue({ status }); + await runSetup(); + const { actions } = testBed; + expect(actions.hasIlmMigrationBanner()).toBe(false); + }); + + it('hides the ILM policy link if there is no ILM policy', async () => { + const status: IlmPolicyMigrationStatus = 'policy-not-found'; + httpService.get.mockResolvedValue({ status }); + await runSetup(); + const { actions } = testBed; + expect(actions.hasIlmPolicyLink()).toBe(false); + }); + + it('hides the ILM policy link if there is no ILM policy locator', async () => { + ilmLocator = undefined; + const status: IlmPolicyMigrationStatus = 'ok'; // should never happen, but need to test that when the locator is missing we don't render the link + httpService.get.mockResolvedValue({ status }); + await runSetup(); + const { actions } = testBed; + expect(actions.hasIlmPolicyLink()).toBe(false); + }); + + it('always shows the ILM policy link if there is an ILM policy', async () => { + const status: IlmPolicyMigrationStatus = 'ok'; + httpService.get.mockResolvedValue({ status }); + await runSetup(); + const { actions } = testBed; + expect(actions.hasIlmPolicyLink()).toBe(true); + + const status2: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; + httpService.get.mockResolvedValue({ status: status2 }); + await runSetup(); + expect(actions.hasIlmPolicyLink()).toBe(true); + }); + + it('hides the banner after migrating indices', async () => { + const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; + const status2: IlmPolicyMigrationStatus = 'ok'; + httpService.get.mockResolvedValueOnce({ status }); + httpService.get.mockResolvedValueOnce({ status: status2 }); + await runSetup(); + const { actions } = testBed; + + expect(actions.hasIlmMigrationBanner()).toBe(true); + await actions.migrateIndices(); + expect(actions.hasIlmMigrationBanner()).toBe(false); + expect(actions.hasIlmPolicyLink()).toBe(true); + expect(toasts.addSuccess).toHaveBeenCalledTimes(1); + }); + + it('informs users when migrations failed', async () => { + const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; + httpService.get.mockResolvedValueOnce({ status }); + reportingAPIClient.migrateReportingIndicesIlmPolicy.mockRejectedValueOnce(new Error('oops!')); + await runSetup(); + const { actions } = testBed; + + expect(actions.hasIlmMigrationBanner()).toBe(true); + await actions.migrateIndices(); + expect(toasts.addError).toHaveBeenCalledTimes(1); + expect(actions.hasIlmMigrationBanner()).toBe(true); + expect(actions.hasIlmPolicyLink()).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index 0b6ece4d8bd02..749e42de526d3 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -13,6 +13,7 @@ import { EuiSpacer, EuiText, EuiTextColor, + EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; @@ -26,10 +27,18 @@ import { JOB_STATUSES as JobStatuses } from '../../common/constants'; import { Poller } from '../../common/poller'; import { durationToNumber } from '../../common/schema_utils'; import { checkLicense } from '../lib/license_check'; -import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; +import { + JobQueueEntry, + ReportingAPIClient, + useInternalApiClient, +} from '../lib/reporting_api_client'; +import { useIlmPolicyStatus, UseIlmPolicyStatusReturn } from '../lib/ilm_policy_status_context'; +import type { SharePluginSetup } from '../shared_imports'; import { ClientConfigType } from '../plugin'; import { ReportDeleteButton, ReportDownloadButton, ReportErrorButton, ReportInfoButton } from './'; import { ReportDiagnostic } from './report_diagnostic'; +import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; +import { IlmPolicyLink } from './ilm_policy_link'; export interface Job { id: string; @@ -55,7 +64,10 @@ export interface Props { license$: LicensingPluginSetup['license$']; pollConfig: ClientConfigType['poll']; redirect: ApplicationStart['navigateToApp']; + navigateToUrl: ApplicationStart['navigateToUrl']; toasts: ToastsSetup; + urlService: SharePluginSetup['url']; + ilmPolicyContextValue: UseIlmPolicyStatusReturn; } interface State { @@ -132,6 +144,10 @@ class ReportListingUi extends Component { } public render() { + const { ilmPolicyContextValue, urlService, navigateToUrl } = this.props; + const ilmLocator = urlService.locators.get('ILM_LOCATOR_ID'); + const hasIlmPolicy = ilmPolicyContextValue.status !== 'policy-not-found'; + const showIlmPolicyLink = Boolean(ilmLocator && hasIlmPolicy); return ( <> { } /> + + {this.renderTable()} - + + + {ilmPolicyContextValue.isLoading ? ( + + ) : ( + showIlmPolicyLink && ( + + ) + )} + @@ -531,4 +558,18 @@ class ReportListingUi extends Component { } } -export const ReportListing = injectI18n(ReportListingUi); +const PrivateReportListing = injectI18n(ReportListingUi); + +export const ReportListing = ( + props: Omit +) => { + const ilmPolicyStatusValue = useIlmPolicyStatus(); + const { apiClient } = useInternalApiClient(); + return ( + + ); +}; diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index a2881af902072..fcbc4662c6e59 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -15,7 +15,6 @@ import { Plugin, PluginInitializerContext, } from 'src/core/public'; -import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public'; import { FeatureCatalogueCategory, @@ -23,7 +22,6 @@ import { HomePublicPluginStart, } from '../../../../src/plugins/home/public'; import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; -import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; import { constants, getDefaultLayoutSelectors } from '../common'; import { durationToNumber } from '../common/schema_utils'; @@ -37,6 +35,13 @@ import { getSharedComponents } from './shared'; import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting'; +import type { + SharePluginSetup, + SharePluginStart, + UiActionsSetup, + UiActionsStart, +} from './shared_imports'; + export interface ClientConfigType { poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } }; roles: { enabled: boolean }; @@ -159,6 +164,7 @@ export class ReportingPublicPlugin license$, this.config.poll, apiClient, + share.url, params ); }, diff --git a/x-pack/plugins/reporting/public/shared_imports.ts b/x-pack/plugins/reporting/public/shared_imports.ts new file mode 100644 index 0000000000000..010da46c07401 --- /dev/null +++ b/x-pack/plugins/reporting/public/shared_imports.ts @@ -0,0 +1,20 @@ +/* + * 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 type { + SharePluginSetup, + SharePluginStart, + LocatorPublic, +} from '../../../../src/plugins/share/public'; + +export { useRequest, UseRequestResponse } from '../../../../src/plugins/es_ui_shared/public'; + +export type { SerializableState } from 'src/plugins/kibana_utils/common'; + +export type { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; + +export type { ManagementAppMountParams } from 'src/plugins/management/public'; diff --git a/x-pack/plugins/reporting/server/lib/deprecations/check_ilm_migration_status.ts b/x-pack/plugins/reporting/server/lib/deprecations/check_ilm_migration_status.ts new file mode 100644 index 0000000000000..dc20f92f38c94 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/deprecations/check_ilm_migration_status.ts @@ -0,0 +1,41 @@ +/* + * 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 type { + IndicesIndexStatePrefixedSettings, + IndicesIndexSettings, +} from '@elastic/elasticsearch/api/types'; +import { ILM_POLICY_NAME } from '../../../common/constants'; +import { IlmPolicyMigrationStatus } from '../../../common/types'; +import { IlmPolicyManager } from '../../lib/store/ilm_policy_manager'; +import type { DeprecationsDependencies } from './types'; + +export const checkIlmMigrationStatus = async ({ + reportingCore, + elasticsearchClient, +}: DeprecationsDependencies): Promise => { + const ilmPolicyManager = IlmPolicyManager.create({ client: elasticsearchClient }); + if (!(await ilmPolicyManager.doesIlmPolicyExist())) { + return 'policy-not-found'; + } + + const store = await reportingCore.getStore(); + const indexPattern = store.getReportingIndexPattern(); + + const { body: reportingIndicesSettings } = await elasticsearchClient.indices.getSettings({ + index: indexPattern, + }); + + const hasUnmanagedIndices = Object.values(reportingIndicesSettings).some((settings) => { + return ( + (settings?.settings as IndicesIndexStatePrefixedSettings)?.index?.lifecycle?.name !== + ILM_POLICY_NAME && + (settings?.settings as IndicesIndexSettings)?.['index.lifecycle']?.name !== ILM_POLICY_NAME + ); + }); + + return hasUnmanagedIndices ? 'indices-not-managed-by-policy' : 'ok'; +}; diff --git a/x-pack/plugins/reporting/server/lib/deprecations/index.ts b/x-pack/plugins/reporting/server/lib/deprecations/index.ts new file mode 100644 index 0000000000000..95594940e07e2 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/deprecations/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { checkIlmMigrationStatus } from './check_ilm_migration_status'; + +export const deprecations = { + checkIlmMigrationStatus, +}; diff --git a/x-pack/plugins/reporting/server/lib/deprecations/types.ts b/x-pack/plugins/reporting/server/lib/deprecations/types.ts new file mode 100644 index 0000000000000..c6e9e3b7ad920 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/deprecations/types.ts @@ -0,0 +1,14 @@ +/* + * 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 type { ElasticsearchClient } from 'src/core/server'; +import type { ReportingCore } from '../../core'; + +export interface DeprecationsDependencies { + reportingCore: ReportingCore; + elasticsearchClient: ElasticsearchClient; +} diff --git a/x-pack/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts index e66f72f88f8ea..b2a2a1edcd6a5 100644 --- a/x-pack/plugins/reporting/server/lib/index.ts +++ b/x-pack/plugins/reporting/server/lib/index.ts @@ -10,5 +10,5 @@ export { cryptoFactory } from './crypto'; export { ExportTypesRegistry, getExportTypesRegistry } from './export_types_registry'; export { LevelLogger } from './level_logger'; export { statuses } from './statuses'; -export { ReportingStore } from './store'; +export { ReportingStore, IlmPolicyManager } from './store'; export { startTrace } from './trace'; diff --git a/x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts b/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/constants.ts similarity index 83% rename from x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts rename to x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/constants.ts index 90636e3c523a3..bea2ba21c0846 100644 --- a/x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts +++ b/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/constants.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IlmPutLifecycleRequest } from '@elastic/elasticsearch/api/types'; +import type { IlmPutLifecycleRequest } from '@elastic/elasticsearch/api/types'; export const reportingIlmPolicy: IlmPutLifecycleRequest['body'] = { policy: { diff --git a/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/ilm_policy_manager.ts b/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/ilm_policy_manager.ts new file mode 100644 index 0000000000000..ca0a74cae8726 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/ilm_policy_manager.ts @@ -0,0 +1,46 @@ +/* + * 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 type { ElasticsearchClient } from 'src/core/server'; +import { ILM_POLICY_NAME } from '../../../../common/constants'; + +import { reportingIlmPolicy } from './constants'; + +/** + * Responsible for detecting and provisioning the reporting ILM policy. + * + * Uses the provided {@link ElasticsearchClient} to scope request privileges. + */ +export class IlmPolicyManager { + constructor(private readonly client: ElasticsearchClient) {} + + public static create(opts: { client: ElasticsearchClient }) { + return new IlmPolicyManager(opts.client); + } + + public async doesIlmPolicyExist(): Promise { + try { + await this.client.ilm.getLifecycle({ policy: ILM_POLICY_NAME }); + return true; + } catch (e) { + if (e.statusCode === 404) { + return false; + } + throw e; + } + } + + /** + * Create the Reporting ILM policy + */ + public async createIlmPolicy(): Promise { + await this.client.ilm.putLifecycle({ + policy: ILM_POLICY_NAME, + body: reportingIlmPolicy, + }); + } +} diff --git a/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/index.ts b/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/index.ts new file mode 100644 index 0000000000000..045a9ecb59997 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { reportingIlmPolicy } from './constants'; +export { IlmPolicyManager } from './ilm_policy_manager'; diff --git a/x-pack/plugins/reporting/server/lib/store/index.ts b/x-pack/plugins/reporting/server/lib/store/index.ts index 6b979325921a6..888918abbc344 100644 --- a/x-pack/plugins/reporting/server/lib/store/index.ts +++ b/x-pack/plugins/reporting/server/lib/store/index.ts @@ -8,3 +8,4 @@ export { ReportDocument } from '../../../common/types'; export { Report } from './report'; export { ReportingStore } from './store'; +export { IlmPolicyManager } from './ilm_policy_manager'; diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 17c067a255b38..7a7dd20e1b25c 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -10,12 +10,15 @@ import { ElasticsearchClient } from 'src/core/server'; import { LevelLogger, statuses } from '../'; import { ReportingCore } from '../../'; import { JobStatus } from '../../../common/types'; + +import { ILM_POLICY_NAME } from '../../../common/constants'; + import { ReportTaskParams } from '../tasks'; + +import { MIGRATION_VERSION, Report, ReportDocument, ReportSource } from './report'; import { indexTimestamp } from './index_timestamp'; import { mapping } from './mapping'; -import { MIGRATION_VERSION, Report, ReportDocument, ReportSource } from './report'; - -import { reportingIlmPolicy } from './report_ilm_policy'; +import { IlmPolicyManager } from './ilm_policy_manager'; /* * When an instance of Kibana claims a report job, this information tells us about that instance @@ -92,6 +95,7 @@ export class ReportingStore { private readonly indexPrefix: string; // config setting of index prefix in system index name private readonly indexInterval: string; // config setting of index prefix: how often to poll for pending work private client?: ElasticsearchClient; + private ilmPolicyManager?: IlmPolicyManager; constructor(private reportingCore: ReportingCore, private logger: LevelLogger) { const config = reportingCore.getConfig(); @@ -109,6 +113,15 @@ export class ReportingStore { return this.client; } + private async getIlmPolicyManager() { + if (!this.ilmPolicyManager) { + const client = await this.getClient(); + this.ilmPolicyManager = IlmPolicyManager.create({ client }); + } + + return this.ilmPolicyManager; + } + private async createIndex(indexName: string) { const client = await this.getClient(); const { body: exists } = await client.indices.exists({ index: indexName }); @@ -125,7 +138,7 @@ export class ReportingStore { number_of_shards: 1, auto_expand_replicas: '0-1', lifecycle: { - name: this.ilmPolicyName, + name: ILM_POLICY_NAME, }, }, mappings: { @@ -181,37 +194,19 @@ export class ReportingStore { return client.indices.refresh({ index }); } - private readonly ilmPolicyName = 'kibana-reporting'; - - private async doesIlmPolicyExist(): Promise { - const client = await this.getClient(); - try { - await client.ilm.getLifecycle({ policy: this.ilmPolicyName }); - return true; - } catch (e) { - if (e.statusCode === 404) { - return false; - } - throw e; - } - } - /** * Function to be called during plugin start phase. This ensures the environment is correctly * configured for storage of reports. */ public async start() { - const client = await this.getClient(); + const ilmPolicyManager = await this.getIlmPolicyManager(); try { - if (await this.doesIlmPolicyExist()) { - this.logger.debug(`Found ILM policy ${this.ilmPolicyName}; skipping creation.`); + if (await ilmPolicyManager.doesIlmPolicyExist()) { + this.logger.debug(`Found ILM policy ${ILM_POLICY_NAME}; skipping creation.`); return; } - this.logger.info(`Creating ILM policy for managing reporting indices: ${this.ilmPolicyName}`); - await client.ilm.putLifecycle({ - policy: this.ilmPolicyName, - body: reportingIlmPolicy, - }); + this.logger.info(`Creating ILM policy for managing reporting indices: ${ILM_POLICY_NAME}`); + await ilmPolicyManager.createIlmPolicy(); } catch (e) { this.logger.error('Error in start phase'); this.logger.error(e.body.error); @@ -446,4 +441,8 @@ export class ReportingStore { return body.hits?.hits[0] as ReportRecordTimeout; } + + public getReportingIndexPattern(): string { + return `${this.indexPrefix}-*`; + } } diff --git a/x-pack/plugins/reporting/server/routes/deprecations.ts b/x-pack/plugins/reporting/server/routes/deprecations.ts new file mode 100644 index 0000000000000..7a38faf60f6bb --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/deprecations.ts @@ -0,0 +1,110 @@ +/* + * 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 { errors } from '@elastic/elasticsearch'; +import { + API_MIGRATE_ILM_POLICY_URL, + API_GET_ILM_POLICY_STATUS, + ILM_POLICY_NAME, +} from '../../common/constants'; +import { IlmPolicyStatusResponse } from '../../common/types'; +import { deprecations } from '../lib/deprecations'; +import { ReportingCore } from '../core'; +import { IlmPolicyManager, LevelLogger as Logger } from '../lib'; + +export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Logger) => { + const { router } = reporting.getPluginSetupDeps(); + + router.get( + { + path: API_GET_ILM_POLICY_STATUS, + validate: false, + }, + async ( + { + core: { + elasticsearch: { client: scopedClient }, + }, + }, + req, + res + ) => { + const checkIlmMigrationStatus = () => { + return deprecations.checkIlmMigrationStatus({ + reportingCore: reporting, + // We want to make the current status visible to all reporting users + elasticsearchClient: scopedClient.asInternalUser, + }); + }; + + try { + const response: IlmPolicyStatusResponse = { + status: await checkIlmMigrationStatus(), + }; + return res.ok({ body: response }); + } catch (e) { + return res.customError({ statusCode: e?.statusCode ?? 500, body: { message: e.message } }); + } + } + ); + + router.put( + { path: API_MIGRATE_ILM_POLICY_URL, validate: false }, + async ({ core: { elasticsearch } }, req, res) => { + const store = await reporting.getStore(); + const { + client: { asCurrentUser: client }, + } = elasticsearch; + + const scopedIlmPolicyManager = IlmPolicyManager.create({ + client, + }); + + // First we ensure that the reporting ILM policy exists in the cluster + try { + // We don't want to overwrite an existing reporting policy because it may contain alterations made by users + if (!(await scopedIlmPolicyManager.doesIlmPolicyExist())) { + await scopedIlmPolicyManager.createIlmPolicy(); + } + } catch (e) { + return res.customError({ statusCode: e?.statusCode ?? 500, body: { message: e.message } }); + } + + const indexPattern = store.getReportingIndexPattern(); + + // Second we migrate all of the existing indices to be managed by the reporting ILM policy + try { + await client.indices.putSettings({ + index: indexPattern, + body: { + 'index.lifecycle': { + name: ILM_POLICY_NAME, + }, + }, + }); + return res.ok(); + } catch (err) { + logger.error(err); + + if (err instanceof errors.ResponseError) { + // If there were no reporting indices to update, that's OK because then there is nothing to migrate + if (err.statusCode === 404) { + return res.ok(); + } + return res.customError({ + statusCode: err.statusCode ?? 500, + body: { + message: err.message, + name: err.name, + }, + }); + } + + throw err; + } + } + ); +}; diff --git a/x-pack/plugins/reporting/server/routes/index.ts b/x-pack/plugins/reporting/server/routes/index.ts index e061bd4f7d66c..a462da3849083 100644 --- a/x-pack/plugins/reporting/server/routes/index.ts +++ b/x-pack/plugins/reporting/server/routes/index.ts @@ -6,15 +6,17 @@ */ import { LevelLogger as Logger } from '../lib'; +import { registerDeprecationsRoutes } from './deprecations'; +import { registerDiagnosticRoutes } from './diagnostic'; import { registerJobGenerationRoutes } from './generation'; import { registerJobInfoRoutes } from './jobs'; import { ReportingCore } from '../core'; -import { registerDiagnosticRoutes } from './diagnostic'; export function registerRoutes(reporting: ReportingCore, logger: Logger) { + registerDeprecationsRoutes(reporting, logger); + registerDiagnosticRoutes(reporting, logger); registerJobGenerationRoutes(reporting, logger); registerJobInfoRoutes(reporting); - registerDiagnosticRoutes(reporting, logger); } export interface ReportingRequestPre { diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts b/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts new file mode 100644 index 0000000000000..a0f4a3f91fe32 --- /dev/null +++ b/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts @@ -0,0 +1,115 @@ +/* + * 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 expect from '@kbn/expect'; +import { JOB_PARAMS_RISON_CSV_DEPRECATED } from '../services/fixtures'; +import { FtrProviderContext } from '../ftr_provider_context'; + +import { ILM_POLICY_NAME } from '../../../plugins/reporting/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const supertestNoAuth = getService('supertestWithoutAuth'); + const reportingAPI = getService('reportingAPI'); + + describe('ILM policy migration APIs', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/reporting/logs'); + await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/reporting/logs'); + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + }); + + afterEach(async () => { + await reportingAPI.deleteAllReports(); + await reportingAPI.migrateReportingIndices(); // ensure that the ILM policy exists + }); + + it('detects when no migration is needed', async () => { + expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('ok'); + + // try creating a report + await supertestNoAuth + .post(`/api/reporting/generate/csv`) + .set('kbn-xsrf', 'xxx') + .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); + + expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('ok'); + }); + + it('detects when reporting indices should be migrated due to missing ILM policy', async () => { + await reportingAPI.makeAllReportingPoliciesUnmanaged(); + // TODO: Remove "any" when no longer through type issue "policy_id" missing + await es.ilm.deleteLifecycle({ policy: ILM_POLICY_NAME } as any); + + await supertestNoAuth + .post(`/api/reporting/generate/csv`) + .set('kbn-xsrf', 'xxx') + .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); + + expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('policy-not-found'); + // assert that migration fixes this + await reportingAPI.migrateReportingIndices(); + expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('ok'); + }); + + it('detects when reporting indices should be migrated due to unmanaged indices', async () => { + await reportingAPI.makeAllReportingPoliciesUnmanaged(); + await supertestNoAuth + .post(`/api/reporting/generate/csv`) + .set('kbn-xsrf', 'xxx') + .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); + + expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('indices-not-managed-by-policy'); + // assert that migration fixes this + await reportingAPI.migrateReportingIndices(); + expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('ok'); + }); + + it('does not override an existing ILM policy', async () => { + const customLifecycle = { + policy: { + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + delete: { + min_age: '0ms', + actions: { + delete: { + delete_searchable_snapshot: true, + }, + }, + }, + }, + }, + }; + + // customize the lifecycle policy + await es.ilm.putLifecycle({ + policy: ILM_POLICY_NAME, + body: customLifecycle, + }); + + await reportingAPI.migrateReportingIndices(); + + const { + body: { + [ILM_POLICY_NAME]: { policy }, + }, + } = await es.ilm.getLifecycle({ policy: ILM_POLICY_NAME }); + + expect(policy).to.eql(customLifecycle.policy); + }); + }); +} diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/index.ts b/x-pack/test/reporting_api_integration/reporting_without_security/index.ts index 15960e45d4a62..fed842427ab90 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security/index.ts @@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Reporting API Integration Tests with Security disabled', function () { this.tags('ciGroup13'); loadTestFile(require.resolve('./job_apis')); + loadTestFile(require.resolve('./ilm_migration_apis')); }); } diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts index e45af4bd140b0..eb32de9d0dc9c 100644 --- a/x-pack/test/reporting_api_integration/services/scenarios.ts +++ b/x-pack/test/reporting_api_integration/services/scenarios.ts @@ -164,6 +164,36 @@ export function createScenarios({ getService }: Pick { + log.debug('ReportingAPI.checkIlmMigrationStatus'); + const { body } = await supertestWithoutAuth + .get('/api/reporting/ilm_policy_status') + .set('kbn-xsrf', 'xxx') + .expect(200); + return body.status; + }; + + const migrateReportingIndices = async () => { + log.debug('ReportingAPI.migrateReportingIndices'); + await supertestWithoutAuth + .put('/api/reporting/deprecations/migrate_ilm_policy') + .set('kbn-xsrf', 'xxx') + .expect(200); + }; + + const makeAllReportingPoliciesUnmanaged = async () => { + log.debug('ReportingAPI.makeAllReportingPoliciesUnmanaged'); + const settings: any = { + 'index.lifecycle.name': null, + }; + await esSupertest + .put('/.reporting*/_settings') + .send({ + settings, + }) + .expect(200); + }; + return { initEcommerce, teardownEcommerce, @@ -182,5 +212,8 @@ export function createScenarios({ getService }: Pick