diff --git a/package.json b/package.json index 97bc80005c09b..24909111afc95 100644 --- a/package.json +++ b/package.json @@ -615,7 +615,7 @@ "@types/redux-actions": "^2.6.1", "@types/redux-logger": "^3.0.8", "@types/seedrandom": ">=2.0.0 <4.0.0", - "@types/selenium-webdriver": "^4.0.16", + "@types/selenium-webdriver": "^4.0.18", "@types/semver": "^7", "@types/set-value": "^2.0.0", "@types/sinon": "^7.0.13", @@ -672,7 +672,7 @@ "callsites": "^3.1.0", "chai": "3.5.0", "chance": "1.0.18", - "chromedriver": "^97.0.2", + "chromedriver": "^98.0.0", "clean-webpack-plugin": "^3.0.0", "cmd-shim": "^2.1.0", "compression-webpack-plugin": "^4.0.0", diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index ecd64c051a5a4..ec689e67cefcb 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -252,6 +252,7 @@ exports.Cluster = class Cluster { const esArgs = [ 'action.destructive_requires_name=true', 'ingest.geoip.downloader.enabled=false', + 'cluster.routing.allocation.disk.threshold_enabled=false', ].concat(options.esArgs || []); // Add to esArgs if ssl is enabled diff --git a/packages/kbn-es/src/integration_tests/cluster.test.js b/packages/kbn-es/src/integration_tests/cluster.test.js index c4b1512331483..f3bdd49103614 100644 --- a/packages/kbn-es/src/integration_tests/cluster.test.js +++ b/packages/kbn-es/src/integration_tests/cluster.test.js @@ -309,6 +309,7 @@ describe('#start(installPath)', () => { Array [ "action.destructive_requires_name=true", "ingest.geoip.downloader.enabled=false", + "cluster.routing.allocation.disk.threshold_enabled=false", ], undefined, Object { @@ -387,6 +388,7 @@ describe('#run()', () => { Array [ "action.destructive_requires_name=true", "ingest.geoip.downloader.enabled=false", + "cluster.routing.allocation.disk.threshold_enabled=false", ], undefined, Object { diff --git a/x-pack/plugins/ml/common/types/annotations.ts b/x-pack/plugins/ml/common/types/annotations.ts index dbc146c1175d8..57b7551c2308a 100644 --- a/x-pack/plugins/ml/common/types/annotations.ts +++ b/x-pack/plugins/ml/common/types/annotations.ts @@ -6,10 +6,10 @@ */ // The Annotation interface is based on annotation documents stored in the -// `.ml-annotations-6` index, accessed via the `.ml-annotations-[read|write]` aliases. +// `.ml-annotations-*` index, accessed via the `.ml-annotations-[read|write]` aliases. // Annotation document mapping: -// PUT .ml-annotations-6 +// PUT .ml-annotations-000001 // { // "mappings": { // "annotation": { @@ -54,8 +54,8 @@ // POST /_aliases // { // "actions" : [ -// { "add" : { "index" : ".ml-annotations-6", "alias" : ".ml-annotations-read" } }, -// { "add" : { "index" : ".ml-annotations-6", "alias" : ".ml-annotations-write" } } +// { "add" : { "index" : ".ml-annotations-000001", "alias" : ".ml-annotations-read" } }, +// { "add" : { "index" : ".ml-annotations-000001", "alias" : ".ml-annotations-write" } } // ] // } diff --git a/x-pack/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json b/x-pack/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json index a0b8f6b242319..829b9c6581bea 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json +++ b/x-pack/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json @@ -15,8 +15,7 @@ "max_score": 0, "hits": [ { - "_index": ".ml-annotations-6", - "_type": "doc", + "_index": ".ml-annotations-000001", "_id": "T-CNvmgBQUJYQVn7TCPA", "_score": 0, "_source": { @@ -32,8 +31,7 @@ } }, { - "_index": ".ml-annotations-6", - "_type": "doc", + "_index": ".ml-annotations-000001", "_id": "3lVpvmgB5xYzd3PM-MSe", "_score": 0, "_source": { diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts index 975070e92a7ec..5663ee6ac79ca 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts @@ -41,7 +41,7 @@ describe('annotation_service', () => { const annotationMockId = 'mockId'; const deleteParamsMock: DeleteParams = { - index: '.ml-annotations-6', + index: '.ml-annotations-000001', id: annotationMockId, refresh: 'wait_for', }; diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts index 227bcee256fb4..f018d44f53c84 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts @@ -72,6 +72,31 @@ export interface DeleteParams { } export function annotationProvider({ asInternalUser }: IScopedClusterClient) { + // Find the index the annotation is stored in. + async function fetchAnnotationIndex(id: string) { + const searchParams: estypes.SearchRequest = { + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + size: 1, + body: { + query: { + ids: { + values: [id], + }, + }, + }, + }; + + const { body } = await asInternalUser.search(searchParams); + const totalCount = + typeof body.hits.total === 'number' ? body.hits.total : body.hits.total!.value; + + if (totalCount === 0) { + throw Boom.notFound(`Cannot find annotation with ID ${id}`); + } + + return body.hits.hits[0]._index; + } + async function indexAnnotation(annotation: Annotation, username: string) { if (isAnnotation(annotation) === false) { // No need to translate, this will not be exposed in the UI. @@ -95,6 +120,8 @@ export function annotationProvider({ asInternalUser }: IScopedClusterClient) { if (typeof annotation._id !== 'undefined') { params.id = annotation._id; + params.index = await fetchAnnotationIndex(annotation._id); + params.require_alias = false; delete params.body._id; delete params.body.key; } @@ -385,28 +412,7 @@ export function annotationProvider({ asInternalUser }: IScopedClusterClient) { } async function deleteAnnotation(id: string) { - // Find the index the annotation is stored in. - const searchParams: estypes.SearchRequest = { - index: ML_ANNOTATIONS_INDEX_ALIAS_READ, - size: 1, - body: { - query: { - ids: { - values: [id], - }, - }, - }, - }; - - const { body } = await asInternalUser.search(searchParams); - const totalCount = - typeof body.hits.total === 'number' ? body.hits.total : body.hits.total.value; - - if (totalCount === 0) { - throw Boom.notFound(`Cannot find annotation with ID ${id}`); - } - - const index = body.hits.hits[0]._index; + const index = await fetchAnnotationIndex(id); const deleteParams: DeleteParams = { index, diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts index 25742958aa243..559b01f1cc6ab 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts @@ -18,7 +18,6 @@ const defaultReindexStatusMeta: ReindexStatusResponse['meta'] = { aliases: [], }; -// Note: The reindexing flyout UX is subject to change; more tests should be added here once functionality is built out describe('Reindex deprecation flyout', () => { let testBed: ElasticsearchTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -50,6 +49,7 @@ describe('Reindex deprecation flyout', () => { aliases: [], }, }); + httpRequestsMockHelpers.setLoadNodeDiskSpaceResponse([]); await act(async () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); @@ -236,4 +236,32 @@ describe('Reindex deprecation flyout', () => { expect(find('reindexChecklistTitle').text()).toEqual('Reindexing process'); }); }); + + describe('low disk space', () => { + it('renders a warning callout if nodes detected with low disk space', async () => { + httpRequestsMockHelpers.setLoadNodeDiskSpaceResponse([ + { + nodeId: '9OFkjpAKS_aPzJAuEOSg7w', + nodeName: 'MacBook-Pro.local', + available: '25%', + lowDiskWatermarkSetting: '50%', + }, + ]); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + const { actions, find } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + expect(find('lowDiskSpaceCallout').text()).toContain('Nodes with low disk space'); + expect(find('impactedNodeListItem').length).toEqual(1); + expect(find('impactedNodeListItem').at(0).text()).toContain( + 'MacBook-Pro.local (25% available)' + ); + }); + }); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts index 7599491ed244f..7e417b5d40936 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts @@ -214,6 +214,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadNodeDiskSpaceResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/node_disk_space`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setClusterSettingsResponse = (response?: object, error?: ResponseError) => { const status = error ? error.statusCode || 400 : 200; const body = error ? error : response; @@ -242,6 +253,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setLoadMlUpgradeModeResponse, setGetUpgradeStatusResponse, setLoadRemoteClustersResponse, + setLoadNodeDiskSpaceResponse, setClusterSettingsResponse, }; }; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/logs_step/logs_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/logs_step/logs_step.test.tsx new file mode 100644 index 0000000000000..31cbb8ebef456 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/logs_step/logs_step.test.tsx @@ -0,0 +1,159 @@ +/* + * 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 { DEPRECATION_LOGS_INDEX } from '../../../../common/constants'; +import { setupEnvironment } from '../../helpers'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; + +describe('Overview - Logs Step', () => { + let testBed: OverviewTestBed; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + describe('error state', () => { + beforeEach(async () => { + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + testBed.component.update(); + }); + + test('is rendered', () => { + const { exists } = testBed; + expect(exists('deprecationLogsErrorCallout')).toBe(true); + expect(exists('deprecationLogsRetryButton')).toBe(true); + }); + }); + + describe('success state', () => { + describe('logging enabled', () => { + beforeEach(() => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ + isDeprecationLogIndexingEnabled: true, + isDeprecationLoggingEnabled: true, + }); + }); + + test('renders step as complete when a user has 0 logs', async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 0, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { component, exists } = testBed; + + component.update(); + + expect(exists('logsStep-complete')).toBe(true); + }); + + test('renders step as incomplete when a user has >0 logs', async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 10, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { component, exists } = testBed; + + component.update(); + + expect(exists('logsStep-incomplete')).toBe(true); + }); + + test('renders deprecation issue count and button to view logs', async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 10, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { component, find } = testBed; + + component.update(); + + expect(find('logsCountDescription').text()).toContain('You have 10 deprecation issues'); + expect(find('viewLogsLink').text()).toContain('View logs'); + }); + }); + + describe('logging disabled', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ + isDeprecationLogIndexingEnabled: false, + isDeprecationLoggingEnabled: true, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { component } = testBed; + + component.update(); + }); + + test('renders button to enable logs', () => { + const { find, exists } = testBed; + + expect(exists('logsCountDescription')).toBe(false); + expect(find('enableLogsLink').text()).toContain('Enable logging'); + }); + }); + }); + + describe('privileges', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ + isDeprecationLogIndexingEnabled: true, + isDeprecationLoggingEnabled: true, + }); + + await act(async () => { + testBed = await setupOverviewPage({ + privileges: { + hasAllPrivileges: true, + missingPrivileges: { + index: [DEPRECATION_LOGS_INDEX], + }, + }, + }); + }); + + const { component } = testBed; + + component.update(); + }); + + test('warns the user of missing index privileges', () => { + const { exists } = testBed; + + expect(exists('missingPrivilegesCallout')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecation_logs/fix_deprecation_logs/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecation_logs/fix_deprecation_logs/index.ts index c0af5524e3a14..ffc1a60e8a2fb 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecation_logs/fix_deprecation_logs/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecation_logs/fix_deprecation_logs/index.ts @@ -6,3 +6,4 @@ */ export { FixDeprecationLogs } from './fix_deprecation_logs'; +export { useDeprecationLogging } from './use_deprecation_logging'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecation_logs/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecation_logs/index.ts index 336aa14642f7d..978fb18cbe2a7 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecation_logs/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecation_logs/index.ts @@ -6,3 +6,4 @@ */ export { EsDeprecationLogs } from './es_deprecation_logs'; +export { useDeprecationLogging } from './fix_deprecation_logs'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx index d8e95cd2456de..e181ca97d21fa 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx @@ -23,6 +23,9 @@ jest.mock('../../../../../app_context', () => { useAppContext: () => { return { services: { + api: { + useLoadNodeDiskSpace: () => [], + }, core: { docLinks: docLinksServiceMock.createStartContract(), }, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx index e416198a69318..3b64a3e050345 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx @@ -79,6 +79,7 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ }> = ({ closeFlyout, reindexState, startReindex, cancelReindex }) => { const { services: { + api, core: { docLinks }, }, } = useAppContext(); @@ -89,6 +90,8 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ const hasFetchFailed = status === ReindexStatus.fetchFailed; const hasReindexingFailed = status === ReindexStatus.failed; + const { data: nodes } = api.useLoadNodeDiskSpace(); + return ( @@ -107,6 +110,48 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ /> )} + + {nodes && nodes.length > 0 && ( + <> + + } + > + <> + + + + +
    + {nodes.map(({ nodeName, available, nodeId }) => ( +
  • + +
  • + ))} +
+ +
+ + + )} + {(hasFetchFailed || hasReindexingFailed) && ( <> )} +

= ({ setIsComplete }) => { ); }; -interface CustomProps { - navigateToEsDeprecationLogs: () => void; -} - -const AccessDeprecationLogsMessage = ({ navigateToEsDeprecationLogs }: CustomProps) => { - return ( - - {({ hasPrivileges, isLoading }) => { - if (isLoading || !hasPrivileges) { - // Don't show the message with the link to access deprecation logs - // to users who can't access the UI anyways. - return null; - } - - return ( - - {i18n.translate('xpack.upgradeAssistant.overview.esDeprecationLogsLink', { - defaultMessage: 'Elasticsearch deprecation logs', - })} - - ), - }} - /> - ); - }} - - ); -}; - export const getFixIssuesStep = ({ isComplete, setIsComplete, - navigateToEsDeprecationLogs, -}: OverviewStepProps & CustomProps): EuiStepProps => { +}: OverviewStepProps): EuiStepProps => { const status = isComplete ? 'complete' : 'incomplete'; return { @@ -105,14 +65,7 @@ export const getFixIssuesStep = ({

- ), - }} + defaultMessage="You must resolve any critical Elasticsearch and Kibana configuration issues before upgrading to Elastic 8.x. Ignoring warnings might result in differences in behavior after you upgrade." />

diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/logs_step/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/logs_step/index.ts new file mode 100644 index 0000000000000..96e6cf4d71c08 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/logs_step/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getLogsStep } from './logs_step'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/logs_step/logs_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/logs_step/logs_step.tsx new file mode 100644 index 0000000000000..6f4bfcfa2791e --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/logs_step/logs_step.tsx @@ -0,0 +1,243 @@ +/* + * 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 } from 'react'; + +import { + EuiText, + EuiSpacer, + EuiButton, + EuiCallOut, + EuiLoadingContent, + EuiCode, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react'; +import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; + +import { DEPRECATION_LOGS_INDEX } from '../../../../../common/constants'; +import { WithPrivileges, MissingPrivileges } from '../../../../shared_imports'; +import { useAppContext } from '../../../app_context'; +import { loadLogsCheckpoint } from '../../../lib/logs_checkpoint'; +import type { OverviewStepProps } from '../../types'; +import { useDeprecationLogging } from '../../es_deprecation_logs'; + +const i18nTexts = { + logsStepTitle: i18n.translate('xpack.upgradeAssistant.overview.logsStep.title', { + defaultMessage: 'Address API deprecations', + }), + logsStepDescription: i18n.translate('xpack.upgradeAssistant.overview.logsStep.description', { + defaultMessage: `Review the Elasticsearch deprecation logs to ensure you're not using deprecated APIs.`, + }), + viewLogsButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.logsStep.viewLogsButtonLabel', + { + defaultMessage: 'View logs', + } + ), + enableLogsButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.logsStep.enableLogsButtonLabel', + { + defaultMessage: 'Enable logging', + } + ), + logsCountDescription: (deprecationCount: number, checkpoint: string) => ( + + {' '} + + + ), + }} + /> + ), + missingPrivilegesTitle: i18n.translate( + 'xpack.upgradeAssistant.overview.logsStep.missingPrivilegesTitle', + { + defaultMessage: 'You require index privileges to analyze the deprecation logs', + } + ), + missingPrivilegesDescription: (privilegesMissing: MissingPrivileges) => ( + {privilegesMissing?.index?.join(', ')} + ), + privilegesCount: privilegesMissing?.index?.length, + }} + /> + ), + loadingError: i18n.translate('xpack.upgradeAssistant.overview.logsStep.loadingError', { + defaultMessage: 'An error occurred while retrieving the deprecation log count', + }), + retryButton: i18n.translate('xpack.upgradeAssistant.overview.logsStep.retryButton', { + defaultMessage: 'Try again', + }), +}; + +interface LogStepProps { + setIsComplete: (isComplete: boolean) => void; + hasPrivileges: boolean; + privilegesMissing: MissingPrivileges; + navigateToEsDeprecationLogs: () => void; +} + +const LogStepDescription = () => ( + +

{i18nTexts.logsStepDescription}

+
+); + +const LogsStep = ({ + setIsComplete, + hasPrivileges, + privilegesMissing, + navigateToEsDeprecationLogs, +}: LogStepProps) => { + const { + services: { api }, + } = useAppContext(); + + const { isDeprecationLogIndexingEnabled } = useDeprecationLogging(); + + const checkpoint = loadLogsCheckpoint(); + + const { + data: logsCount, + error, + isLoading, + resendRequest, + isInitialRequest, + } = api.getDeprecationLogsCount(checkpoint); + + useEffect(() => { + if (!isDeprecationLogIndexingEnabled) { + setIsComplete(false); + } + + setIsComplete(logsCount?.count === 0); + + // Depending upon setIsComplete would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDeprecationLogIndexingEnabled, logsCount]); + + if (hasPrivileges === false && isDeprecationLogIndexingEnabled) { + return ( + <> + + + + + +

{i18nTexts.missingPrivilegesDescription(privilegesMissing)}

+
+ + ); + } + + if (isLoading && isInitialRequest) { + return ; + } + + if (hasPrivileges && error) { + return ( + +

+ {error.statusCode} - {error.message} +

+ + + {i18nTexts.retryButton} + +
+ ); + } + + return ( + <> + + + {isDeprecationLogIndexingEnabled && logsCount ? ( + <> + + + +

+ {i18nTexts.logsCountDescription(logsCount.count, checkpoint)} +

+
+ + + + + {i18nTexts.viewLogsButtonLabel} + + + ) : ( + <> + + + + {i18nTexts.enableLogsButtonLabel} + + + )} + + + ); +}; + +interface CustomProps { + navigateToEsDeprecationLogs: () => void; +} + +export const getLogsStep = ({ + isComplete, + setIsComplete, + navigateToEsDeprecationLogs, +}: OverviewStepProps & CustomProps): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + + return { + status, + title: i18nTexts.logsStepTitle, + 'data-test-subj': `logsStep-${status}`, + children: ( + + {({ hasPrivileges, isLoading, privilegesMissing }) => ( + + )} + + ), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx index 567b3b7bbea0d..b6230393dacb8 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx @@ -28,8 +28,9 @@ import { getBackupStep } from './backup_step'; import { getFixIssuesStep } from './fix_issues_step'; import { getUpgradeStep } from './upgrade_step'; import { getMigrateSystemIndicesStep } from './migrate_system_indices'; +import { getLogsStep } from './logs_step'; -type OverviewStep = 'backup' | 'migrate_system_indices' | 'fix_issues'; +type OverviewStep = 'backup' | 'migrate_system_indices' | 'fix_issues' | 'logs'; export const Overview = withRouter(({ history }: RouteComponentProps) => { const { @@ -52,6 +53,7 @@ export const Overview = withRouter(({ history }: RouteComponentProps) => { backup: false, migrate_system_indices: false, fix_issues: false, + logs: false, }); const isStepComplete = (step: OverviewStep) => completedStepsMap[step]; @@ -114,6 +116,10 @@ export const Overview = withRouter(({ history }: RouteComponentProps) => { getFixIssuesStep({ isComplete: isStepComplete('fix_issues'), setIsComplete: setCompletedStep.bind(null, 'fix_issues'), + }), + getLogsStep({ + isComplete: isStepComplete('logs'), + setIsComplete: setCompletedStep.bind(null, 'logs'), navigateToEsDeprecationLogs: () => history.push('/es_deprecation_logs'), }), getUpgradeStep(), diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts index cdf5cb5fa57dc..f55a6c04c640e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts @@ -256,6 +256,20 @@ export class ApiService { method: 'get', }); } + + public useLoadNodeDiskSpace() { + return this.useRequest< + Array<{ + nodeId: string; + nodeName: string; + available: string; + lowDiskWatermarkSetting: string; + }> + >({ + path: `${API_BASE_PATH}/node_disk_space`, + method: 'get', + }); + } } export const apiService = new ApiService(); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/node_disk_space.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/node_disk_space.test.ts new file mode 100644 index 0000000000000..7d4d8da1c585e --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/node_disk_space.test.ts @@ -0,0 +1,258 @@ +/* + * 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 } from 'src/core/server'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; +import { createRequestMock } from './__mocks__/request.mock'; +import { handleEsError } from '../shared_imports'; + +jest.mock('../lib/es_version_precheck', () => ({ + versionCheckHandlerWrapper: (a: any) => a, +})); + +import { registerNodeDiskSpaceRoute } from './node_disk_space'; + +describe('Disk space API', () => { + let mockRouter: MockRouter; + let routeDependencies: any; + + beforeEach(() => { + mockRouter = createMockRouter(); + routeDependencies = { + router: mockRouter, + lib: { handleEsError }, + }; + registerNodeDiskSpaceRoute(routeDependencies); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /api/upgrade_assistant/node_disk_space', () => { + beforeEach(() => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.nodes.stats as jest.Mock + ).mockResolvedValue({ + body: { + nodes: { + '1YOaoS9lTNOiTxR1uzSgRA': { + name: 'node_name', + fs: { + total: { + // Keeping these numbers (inaccurately) small so it's easier to reason the math when scanning through :) + total_in_bytes: 100, + available_in_bytes: 20, + }, + }, + }, + }, + }, + }); + }); + + it('returns the default low watermark disk usage setting', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster + .getSettings as jest.Mock + ).mockResolvedValue({ + body: { + defaults: { + 'cluster.routing.allocation.disk.watermark.low': '75%', + }, + transient: {}, + persistent: {}, + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/node_disk_space', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual([ + { + nodeName: 'node_name', + nodeId: '1YOaoS9lTNOiTxR1uzSgRA', + available: '20%', + lowDiskWatermarkSetting: '75%', + }, + ]); + }); + + it('returns the persistent low watermark disk usage setting', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster + .getSettings as jest.Mock + ).mockResolvedValue({ + body: { + defaults: {}, + transient: {}, + persistent: { 'cluster.routing.allocation.disk.watermark.low': '75%' }, + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/node_disk_space', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual([ + { + nodeName: 'node_name', + nodeId: '1YOaoS9lTNOiTxR1uzSgRA', + available: '20%', + lowDiskWatermarkSetting: '75%', + }, + ]); + }); + + it('returns the transient low watermark disk usage setting', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster + .getSettings as jest.Mock + ).mockResolvedValue({ + body: { + defaults: {}, + transient: { 'cluster.routing.allocation.disk.watermark.low': '80%' }, + persistent: { 'cluster.routing.allocation.disk.watermark.low': '85%' }, + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/node_disk_space', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual([ + { + nodeName: 'node_name', + nodeId: '1YOaoS9lTNOiTxR1uzSgRA', + available: '20%', + lowDiskWatermarkSetting: '80%', + }, + ]); + }); + + it('returns nodes with low disk space when low watermark disk usage setting is bytes value', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster + .getSettings as jest.Mock + ).mockResolvedValue({ + body: { + defaults: { + 'cluster.routing.allocation.disk.watermark.low': '80b', + }, + transient: {}, + persistent: {}, + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/node_disk_space', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual([ + { + nodeName: 'node_name', + nodeId: '1YOaoS9lTNOiTxR1uzSgRA', + available: '20%', + lowDiskWatermarkSetting: '80b', + }, + ]); + }); + + it('returns empty array if low watermark disk usage setting is undefined', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster + .getSettings as jest.Mock + ).mockResolvedValue({ + body: { + defaults: {}, + transient: {}, + persistent: {}, + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/node_disk_space', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual([]); + }); + + it('returns empty array if nodes have not reached low disk usage', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster + .getSettings as jest.Mock + ).mockResolvedValue({ + body: { + defaults: { + 'cluster.routing.allocation.disk.watermark.low': '10%', + }, + transient: {}, + persistent: {}, + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/node_disk_space', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual([]); + }); + + describe('Error handling', () => { + it('returns an error if cluster.getSettings throws', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster + .getSettings as jest.Mock + ).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/node_disk_space', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); + }); + + it('returns an error if node.stats throws', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster + .getSettings as jest.Mock + ).mockResolvedValue({ + body: { + defaults: { + 'cluster.routing.allocation.disk.watermark.low': '85%', + }, + transient: {}, + persistent: {}, + }, + }); + + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.nodes.stats as jest.Mock + ).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/node_disk_space', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); + }); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/node_disk_space.ts b/x-pack/plugins/upgrade_assistant/server/routes/node_disk_space.ts new file mode 100644 index 0000000000000..cd93742b83bc1 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/node_disk_space.ts @@ -0,0 +1,138 @@ +/* + * 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 { ClusterGetSettingsResponse } from '@elastic/elasticsearch/api/types'; +import { ByteSizeValue } from '@kbn/config-schema'; +import { API_BASE_PATH } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; + +interface NodeWithLowDiskSpace { + nodeId: string; + nodeName: string; + available: string; + lowDiskWatermarkSetting: string; +} + +const getLowDiskWatermarkSetting = ( + clusterSettings: ClusterGetSettingsResponse +): string | undefined => { + const { defaults, persistent, transient } = clusterSettings; + + const defaultLowDiskWatermarkSetting = + defaults && defaults['cluster.routing.allocation.disk.watermark.low']; + const transientLowDiskWatermarkSetting = + transient && transient['cluster.routing.allocation.disk.watermark.low']; + const persistentLowDiskWatermarkSetting = + persistent && persistent['cluster.routing.allocation.disk.watermark.low']; + + // ES applies cluster settings in the following order of precendence: transient, persistent, default + if (transientLowDiskWatermarkSetting) { + return transientLowDiskWatermarkSetting; + } else if (persistentLowDiskWatermarkSetting) { + return persistentLowDiskWatermarkSetting; + } else if (defaultLowDiskWatermarkSetting) { + return defaultLowDiskWatermarkSetting; + } + + // May be undefined if defined in elasticsearch.yml + return undefined; +}; + +export function registerNodeDiskSpaceRoute({ router, lib: { handleEsError } }: RouteDependencies) { + router.get( + { + path: `${API_BASE_PATH}/node_disk_space`, + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + const { body: clusterSettings } = await client.asCurrentUser.cluster.getSettings({ + flat_settings: true, + include_defaults: true, + }); + + const lowDiskWatermarkSetting = getLowDiskWatermarkSetting(clusterSettings); + + if (lowDiskWatermarkSetting) { + const { body: nodeStats } = await client.asCurrentUser.nodes.stats({ + metric: 'fs', + }); + + const nodeIds = Object.keys(nodeStats.nodes); + + const nodesWithLowDiskSpace: NodeWithLowDiskSpace[] = []; + + nodeIds.forEach((nodeId) => { + const node = nodeStats.nodes[nodeId]; + const byteStats = node.fs.total; + const { total_in_bytes: totalInBytes, available_in_bytes: availableInBytes } = + byteStats; + + // Regex to determine if the low disk watermark setting is configured as a percentage value + // Elasticsearch accepts a percentage or bytes value + const isLowDiskWatermarkPercentage = /^(\d+|(\.\d+))(\.\d+)?%$/.test( + lowDiskWatermarkSetting! + ); + + if (isLowDiskWatermarkPercentage) { + const percentageAvailable = (availableInBytes / totalInBytes) * 100; + const rawLowDiskWatermarkPercentageValue = Number( + lowDiskWatermarkSetting!.replace('%', '') + ); + + // If the percentage available is < the low disk watermark setting, mark node as having low disk space + if (percentageAvailable < rawLowDiskWatermarkPercentageValue) { + nodesWithLowDiskSpace.push({ + nodeId, + nodeName: node.name, + available: `${Math.round(percentageAvailable)}%`, + lowDiskWatermarkSetting: lowDiskWatermarkSetting!, + }); + } + } else { + // If not a percentage value, assume user configured low disk watermark setting in bytes + const rawLowDiskWatermarkBytesValue = ByteSizeValue.parse( + lowDiskWatermarkSetting! + ).getValueInBytes(); + + const percentageAvailable = (availableInBytes / totalInBytes) * 100; + + // If bytes available < the low disk watermarket setting, mark node as having low disk space + if (availableInBytes < rawLowDiskWatermarkBytesValue) { + nodesWithLowDiskSpace.push({ + nodeId, + nodeName: node.name, + available: `${Math.round(percentageAvailable)}%`, + lowDiskWatermarkSetting: lowDiskWatermarkSetting!, + }); + } + } + }); + + return response.ok({ body: nodesWithLowDiskSpace }); + } + + // If the low disk watermark setting is undefined, send empty array + // This could occur if the setting is configured in elasticsearch.yml + return response.ok({ body: [] }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts index 2be0d1fafc6ae..ddac8c2576ee6 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts @@ -19,6 +19,7 @@ import { registerMlSnapshotRoutes } from './ml_snapshots'; import { ReindexWorker } from '../lib/reindexing'; import { registerUpgradeStatusRoute } from './status'; import { registerRemoteClustersRoute } from './remote_clusters'; +import { registerNodeDiskSpaceRoute } from './node_disk_space'; import { registerClusterSettingsRoute } from './cluster_settings'; export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) { @@ -35,5 +36,6 @@ export function registerRoutes(dependencies: RouteDependencies, getWorker: () => // Route for cloud to retrieve the upgrade status for ES and Kibana registerUpgradeStatusRoute(dependencies); registerRemoteClustersRoute(dependencies); + registerNodeDiskSpaceRoute(dependencies); registerClusterSettingsRoute(dependencies); } diff --git a/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts index c5f7aebc208c9..2d19674d63a75 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts @@ -121,8 +121,7 @@ describe('Status API', () => { expect(resp.payload).toEqual({ readyForUpgrade: false, details: - 'You have 0 system indices that must be migrated and ' + - '1 Elasticsearch deprecation issue and 1 Kibana deprecation issue that must be resolved before upgrading.', + 'The following issues must be resolved before upgrading: 1 Elasticsearch deprecation issue, 1 Kibana deprecation issue.', }); }); @@ -144,8 +143,7 @@ describe('Status API', () => { expect(resp.payload).toEqual({ readyForUpgrade: false, details: - 'You have 1 system index that must be migrated and ' + - '1 Elasticsearch deprecation issue and 1 Kibana deprecation issue that must be resolved before upgrading.', + 'The following issues must be resolved before upgrading: 1 unmigrated system index, 1 Elasticsearch deprecation issue, 1 Kibana deprecation issue.', }); }); @@ -167,8 +165,7 @@ describe('Status API', () => { expect(resp.payload).toEqual({ readyForUpgrade: false, details: - 'You have 1 system index that must be migrated and ' + - '0 Elasticsearch deprecation issues and 0 Kibana deprecation issues that must be resolved before upgrading.', + 'The following issues must be resolved before upgrading: 1 unmigrated system index.', }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/status.ts b/x-pack/plugins/upgrade_assistant/server/routes/status.ts index cfa5d6d14c85c..9e8d3f9b2734b 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/status.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/status.ts @@ -63,12 +63,44 @@ export function registerUpgradeStatusRoute({ router, lib: { handleEsError } }: R ); } + const upgradeIssues: string[] = []; + + if (notMigratedSystemIndices) { + upgradeIssues.push( + i18n.translate('xpack.upgradeAssistant.status.systemIndicesMessage', { + defaultMessage: + '{notMigratedSystemIndices} unmigrated system {notMigratedSystemIndices, plural, one {index} other {indices}}', + values: { notMigratedSystemIndices }, + }) + ); + } + + if (esTotalCriticalDeps) { + upgradeIssues.push( + i18n.translate('xpack.upgradeAssistant.status.esTotalCriticalDepsMessage', { + defaultMessage: + '{esTotalCriticalDeps} Elasticsearch deprecation {esTotalCriticalDeps, plural, one {issue} other {issues}}', + values: { esTotalCriticalDeps }, + }) + ); + } + + if (kibanaTotalCriticalDeps) { + upgradeIssues.push( + i18n.translate('xpack.upgradeAssistant.status.kibanaTotalCriticalDepsMessage', { + defaultMessage: + '{kibanaTotalCriticalDeps} Kibana deprecation {kibanaTotalCriticalDeps, plural, one {issue} other {issues}}', + values: { kibanaTotalCriticalDeps }, + }) + ); + } + return i18n.translate('xpack.upgradeAssistant.status.deprecationsUnresolvedMessage', { defaultMessage: - 'You have {notMigratedSystemIndices} system {notMigratedSystemIndices, plural, one {index} other {indices}} that must be migrated ' + - 'and {esTotalCriticalDeps} Elasticsearch deprecation {esTotalCriticalDeps, plural, one {issue} other {issues}} ' + - 'and {kibanaTotalCriticalDeps} Kibana deprecation {kibanaTotalCriticalDeps, plural, one {issue} other {issues}} that must be resolved before upgrading.', - values: { esTotalCriticalDeps, kibanaTotalCriticalDeps, notMigratedSystemIndices }, + 'The following issues must be resolved before upgrading: {upgradeIssues}.', + values: { + upgradeIssues: upgradeIssues.join(', '), + }, }); }; diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/index_detail.js b/x-pack/test/api_integration/apis/monitoring/elasticsearch/index_detail.js index 87371187c83e8..e243c3a372f7b 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/index_detail.js +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/index_detail.js @@ -41,9 +41,6 @@ export default function ({ getService }) { }) .expect(200); - // Work around ESTF failure outlined in https://github.com/elastic/kibana/issues/124594 - indexDetailFixture.logs = body.logs; - expect(body).to.eql(indexDetailFixture); }); diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/node_disk_space.ts b/x-pack/test/api_integration/apis/upgrade_assistant/node_disk_space.ts new file mode 100644 index 0000000000000..fce68f4a344c9 --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/node_disk_space.ts @@ -0,0 +1,31 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; +import { API_BASE_PATH } from '../../../../plugins/upgrade_assistant/common/constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Node disk space', () => { + describe('GET /api/upgrade_assistant/node_disk_space', () => { + it('returns an array of nodes', async () => { + const { body: apiRequestResponse } = await supertest + .get(`${API_BASE_PATH}/node_disk_space`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + // It's tricky to assert the correct node values on CI + // For now, this serves as a smoke test and verifies an array is returned + // There are jest unit tests that test additional logic + expect(Array.isArray(apiRequestResponse)).be(true); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/snapshot_restore/home_page.ts b/x-pack/test/functional/apps/snapshot_restore/home_page.ts index b72656a96980f..7e8962b3acc82 100644 --- a/x-pack/test/functional/apps/snapshot_restore/home_page.ts +++ b/x-pack/test/functional/apps/snapshot_restore/home_page.ts @@ -12,9 +12,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'snapshotRestore']); const log = getService('log'); const es = getService('es'); + const security = getService('security'); describe('Home page', function () { before(async () => { + await security.testUser.setRoles(['snapshot_restore_user'], false); await pageObjects.common.navigateToApp('snapshotRestore'); }); @@ -46,9 +48,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('cleanup repository', async () => { await pageObjects.snapshotRestore.viewRepositoryDetails('my-repository'); - await pageObjects.common.sleep(25000); const cleanupResponse = await pageObjects.snapshotRestore.performRepositoryCleanup(); - await pageObjects.common.sleep(25000); expect(cleanupResponse).to.contain('results'); expect(cleanupResponse).to.contain('deleted_bytes'); expect(cleanupResponse).to.contain('deleted_blobs'); @@ -57,6 +57,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await es.snapshot.deleteRepository({ repository: 'my-repository', }); + await security.testUser.restoreDefaults(); }); }); }); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 91879031327a1..646c7c04cb1c3 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -541,6 +541,25 @@ export default async function ({ readConfigFile }) { }, ], }, + // https://www.elastic.co/guide/en/elasticsearch/reference/master/snapshots-register-repository.html#snapshot-repo-prereqs + snapshot_restore_user: { + elasticsearch: { + cluster: [ + 'monitor', + 'manage_slm', + 'cluster:admin/snapshot', + 'cluster:admin/repository', + ], + }, + kibana: [ + { + feature: { + advancedSettings: ['read'], + }, + spaces: ['*'], + }, + ], + }, ingest_pipelines_user: { elasticsearch: { diff --git a/x-pack/test/functional/page_objects/snapshot_restore_page.ts b/x-pack/test/functional/page_objects/snapshot_restore_page.ts index 216b2bfff9d7e..e1fc50ed86fa2 100644 --- a/x-pack/test/functional/page_objects/snapshot_restore_page.ts +++ b/x-pack/test/functional/page_objects/snapshot_restore_page.ts @@ -49,12 +49,15 @@ export function SnapshotRestorePageProvider({ getService }: FtrProviderContext) const repoToView = repos.filter((r) => (r.repoName = name))[0]; await repoToView.repoLink.click(); } - await retry.waitForWithTimeout(`Repo title should be ${name}`, 10000, async () => { + await retry.waitForWithTimeout(`Repo title should be ${name}`, 25000, async () => { return (await testSubjects.getVisibleText('title')) === name; }); }, async performRepositoryCleanup() { await testSubjects.click('cleanupRepositoryButton'); + await retry.waitForWithTimeout(`wait for code block to be visible`, 25000, async () => { + return await testSubjects.isDisplayed('cleanupCodeBlock'); + }); return await testSubjects.getVisibleText('cleanupCodeBlock'); }, }; diff --git a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts index f59cf660139b9..933880dac739e 100644 --- a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts +++ b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts @@ -31,9 +31,9 @@ export class UpgradeAssistantPageObject extends FtrService { async navigateToEsDeprecationLogs() { return await this.retry.try(async () => { - await this.common.navigateToApp('settings'); - await this.testSubjects.click('upgrade_assistant'); - await this.testSubjects.click('viewElasticsearchDeprecationLogs'); + await this.common.navigateToUrl('management', 'stack/upgrade_assistant/es_deprecation_logs', { + shouldUseHashForSubUrl: false, + }); await this.retry.waitFor( 'url to contain /upgrade_assistant/es_deprecation_logs', async () => { diff --git a/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts b/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts index 20fc34f77dbf8..e7769f2761f3f 100644 --- a/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts @@ -71,6 +71,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } const advOpt = await find.byXPath(`//button[descendant::*[text()='Advanced options']]`); await advOpt.click(); + // Workaround for: https://github.com/elastic/kibana/issues/126540 + const isUrlTooLong = await testSubjects.exists('urlTooLongErrorMessage'); + if (isUrlTooLong) { + // Save dashboard + await PageObjects.dashboard.switchToEditMode(); + await PageObjects.dashboard.clickQuickSave(); + await PageObjects.share.openShareMenuItem(link); + if (type === 'pdf_optimize') { + await testSubjects.click('usePrintLayout'); + } + const advOpt2 = await find.byXPath( + `//button[descendant::*[text()='Advanced options']]` + ); + await advOpt2.click(); + } const postUrl = await find.byXPath(`//button[descendant::*[text()='Copy POST URL']]`); await postUrl.click(); const url = await browser.getClipboardValue(); diff --git a/yarn.lock b/yarn.lock index fa8a09740ca39..eae3de6aa0fbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6354,10 +6354,10 @@ resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f" integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA== -"@types/selenium-webdriver@^4.0.16": - version "4.0.16" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.16.tgz#c3205c6691a1d645cf4163684bd119230a60e6f5" - integrity sha512-0UAzu2lFXpLK4lU4yhgUtM/KxoN8hIpyI+q22KAwzIDHNk4kLJ/Ut5mJZLFSxfQx58OBQ9SJXZkSL065fe/WdQ== +"@types/selenium-webdriver@^4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.18.tgz#98f6e1ccd2d92f6fddaccfc7c148d2e158da0f92" + integrity sha512-gkrUo3QldGr8V9im/DjgKkX4UVd1rtflfEBuPG9hPSA1keu7A0rF8h/MQjpTMm2EPVhBCd2K8tn5nlC9Vsd5Xw== "@types/semver@^7": version "7.3.4" @@ -9590,10 +9590,10 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^97.0.2: - version "97.0.2" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-97.0.2.tgz#b6c26f6667ad40dc8cf08818878cc064787116fc" - integrity sha512-sOAfKCR3WsHvmKedZoWa+3tBVGdPtxq4zKxgKZCoJ2c924olBTW4Bnha6SHl93Yo7+QqsNn6ZpAC0ojhutacAg== +chromedriver@^98.0.0: + version "98.0.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-98.0.0.tgz#b2c3c1941fad4cdfadad5d4c46923e02f089fd30" + integrity sha512-Oi6Th5teK+VI4nti+423/dFkENYHEMOdUvqwJHzOaNwXqLwZ8FuSaKBybgALCctGapwJbd+tmPv3qSd6tUUIHQ== dependencies: "@testim/chrome-version" "^1.1.2" axios "^0.24.0"