From 5dd8742d1750c9307fa1af05acc1a38e48a949b2 Mon Sep 17 00:00:00 2001 From: Gerard Soldevila Date: Wed, 1 Mar 2023 10:26:04 +0100 Subject: [PATCH] Allow for additive mappings update without creating a new version index (#149326) Fixes [#147237](https://github.com/elastic/kibana/issues/147237) Based on the same principle as [#147371](https://github.com/elastic/kibana/pull/147371), the goal of this PR is to **avoid reindexing if possible**. This time, the idea is to check whether the new mappings are still compatible with the ones stored in ES. To to so, we attempt to update the mappings in place in the existing index, introducing a new `CHECK_COMPATIBLE_MAPPINGS` step: * If the update operation fails, we assume the mappings are NOT compatible, and we continue with the normal reindexing flow. * If the update operation succeeds, we assume the mappings ARE compatible, and we skip reindexing, just like [#147371](https://github.com/elastic/kibana/pull/147371) does. ![image](https://user-images.githubusercontent.com/25349407/216979882-9fe9f034-b521-4171-b85d-50be6a13e179.png) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../index.ts | 2 +- .../src/actions/index.ts | 14 +- .../update_and_pickup_mappings.test.ts | 9 +- .../src/actions/update_and_pickup_mappings.ts | 4 +- .../src/actions/update_mappings.test.ts | 147 +++++++ .../src/actions/update_mappings.ts | 63 +++ .../update_target_mappings_meta.test.ts | 80 ---- .../actions/update_target_mappings_meta.ts | 55 --- .../src/model/helpers.ts | 8 +- .../src/model/model.test.ts | 51 ++- .../src/model/model.ts | 29 +- .../src/next.ts | 62 +-- .../src/state.ts | 8 + .../archives/7.13.0_with_corrupted_so.zip | Bin 46485 -> 0 bytes .../group1/7_13_0_failed_action_tasks.test.ts | 160 ++++---- .../group1/7_13_0_transform_failures.test.ts | 30 +- .../group2/check_target_mappings.test.ts | 73 +--- .../migrations/group2/cleanup.test.ts | 255 +++++++----- .../group2/multiple_kibana_nodes.test.ts | 10 +- .../migrations/group2/outdated_docs.test.ts | 4 +- .../migrations/group3/actions/actions.test.ts | 84 +++- .../migrations/group3/active_delete.test.ts | 388 ++++++++++-------- .../group3/multiple_es_nodes.test.ts | 6 +- .../migrations/group3/rewriting_id.test.ts | 4 +- .../migrations/group3/skip_reindex.test.ts | 220 +++++----- .../kibana_migrator_test_kit.fixtures.ts | 82 ++++ .../migrations/kibana_migrator_test_kit.ts | 136 +++++- 27 files changed, 1254 insertions(+), 730 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_mappings.test.ts create mode 100644 packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_mappings.ts delete mode 100644 packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_target_mappings_meta.test.ts delete mode 100644 packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_target_mappings_meta.ts delete mode 100644 src/core/server/integration_tests/saved_objects/migrations/archives/7.13.0_with_corrupted_so.zip create mode 100644 src/core/server/integration_tests/saved_objects/migrations/kibana_migrator_test_kit.fixtures.ts diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/index.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/index.ts index 9a4a728184388..61856a30cfc10 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/index.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/index.ts @@ -28,7 +28,7 @@ export { cloneIndex, waitForTask, updateAndPickupMappings, - updateTargetMappingsMeta, + updateMappings, updateAliases, transformDocs, setWriteBlock, diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/index.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/index.ts index 2593ac7867d1e..7e380d3a7ad17 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/index.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/index.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { type Either, right } from 'fp-ts/lib/Either'; +import type { Either } from 'fp-ts/lib/Either'; +import { right } from 'fp-ts/lib/Either'; import type { RetryableEsClientError } from './catch_retryable_es_client_errors'; import type { DocumentsTransformFailed } from '../core/migrate_raw_docs'; @@ -37,11 +38,8 @@ export type { CloneIndexResponse, CloneIndexParams } from './clone_index'; export { cloneIndex } from './clone_index'; export type { WaitForIndexStatusParams, IndexNotYellowTimeout } from './wait_for_index_status'; -import { - type IndexNotGreenTimeout, - type IndexNotYellowTimeout, - waitForIndexStatus, -} from './wait_for_index_status'; +import type { IndexNotGreenTimeout, IndexNotYellowTimeout } from './wait_for_index_status'; +import { waitForIndexStatus } from './wait_for_index_status'; export type { WaitForTaskResponse, WaitForTaskCompletionTimeout } from './wait_for_task'; import { waitForTask, WaitForTaskCompletionTimeout } from './wait_for_task'; @@ -85,8 +83,6 @@ export { createIndex } from './create_index'; export { checkTargetMappings } from './check_target_mappings'; -export { updateTargetMappingsMeta } from './update_target_mappings_meta'; - export const noop = async (): Promise> => right('noop' as const); export type { @@ -95,6 +91,8 @@ export type { } from './update_and_pickup_mappings'; export { updateAndPickupMappings } from './update_and_pickup_mappings'; +export { updateMappings } from './update_mappings'; + import type { UnknownDocsFound } from './check_for_unknown_docs'; import type { IncompatibleClusterRoutingAllocation } from './initialize_action'; import { ClusterShardLimitExceeded } from './create_index'; diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_and_pickup_mappings.test.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_and_pickup_mappings.test.ts index c1fd2f7b0b0fb..da243af9a7ebc 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_and_pickup_mappings.test.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_and_pickup_mappings.test.ts @@ -46,7 +46,7 @@ describe('updateAndPickupMappings', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); - it('updates the _mapping properties but not the _meta information', async () => { + it('calls the indices.putMapping with the mapping properties as well as the _meta information', async () => { const task = updateAndPickupMappings({ client, index: 'new_index', @@ -82,6 +82,13 @@ describe('updateAndPickupMappings', () => { dynamic: false, }, }, + _meta: { + migrationMappingPropertyHashes: { + references: '7997cf5a56cc02bdc9c93361bde732b0', + 'epm-packages': '860e23f4404fa1c33f430e6dad5d8fa2', + 'cases-connector-mappings': '17d2e9e0e170a21a471285a5d845353c', + }, + }, }); }); }); diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_and_pickup_mappings.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_and_pickup_mappings.ts index 7f89f862ce128..653a90746dea0 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_and_pickup_mappings.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_and_pickup_mappings.ts @@ -45,13 +45,11 @@ export const updateAndPickupMappings = ({ RetryableEsClientError, 'update_mappings_succeeded' > = () => { - // ._meta property will be updated on a later step - const { _meta, ...mappingsWithoutMeta } = mappings; return client.indices .putMapping({ index, timeout: DEFAULT_TIMEOUT, - ...mappingsWithoutMeta, + ...mappings, }) .then(() => { // Ignore `acknowledged: false`. When the coordinating node accepts diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_mappings.test.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_mappings.test.ts new file mode 100644 index 0000000000000..133f07d7460e5 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_mappings.test.ts @@ -0,0 +1,147 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as Either from 'fp-ts/lib/Either'; +import type { TransportResult } from '@elastic/elasticsearch'; +import { errors as EsErrors } from '@elastic/elasticsearch'; +import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; +import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors'; +import { updateMappings } from './update_mappings'; +import { DEFAULT_TIMEOUT } from './constants'; + +jest.mock('./catch_retryable_es_client_errors'); + +describe('updateMappings', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const createErrorClient = (response: Partial>>) => { + // Create a mock client that returns the desired response + const apiResponse = elasticsearchClientMock.createApiResponse(response); + const error = new EsErrors.ResponseError(apiResponse); + const client = elasticsearchClientMock.createInternalClient( + elasticsearchClientMock.createErrorTransportRequestPromise(error) + ); + + return { client, error }; + }; + + it('resolves left if the mappings are not compatible (aka 400 illegal_argument_exception from ES)', async () => { + const { client } = createErrorClient({ + statusCode: 400, + body: { + error: { + type: 'illegal_argument_exception', + reason: 'mapper [action.actionTypeId] cannot be changed from type [keyword] to [text]', + }, + }, + }); + + const task = updateMappings({ + client, + index: 'new_index', + mappings: { + properties: { + created_at: { + type: 'date', + }, + }, + _meta: { + migrationMappingPropertyHashes: { + references: '7997cf5a56cc02bdc9c93361bde732b0', + 'epm-packages': '860e23f4404fa1c33f430e6dad5d8fa2', + 'cases-connector-mappings': '17d2e9e0e170a21a471285a5d845353c', + }, + }, + }, + }); + + const res = await task(); + + expect(Either.isLeft(res)).toEqual(true); + expect(res).toMatchInlineSnapshot(` + Object { + "_tag": "Left", + "left": Object { + "type": "incompatible_mapping_exception", + }, + } + `); + }); + + it('calls catchRetryableEsClientErrors when the promise rejects', async () => { + const { client, error: retryableError } = createErrorClient({ + statusCode: 503, + body: { error: { type: 'es_type', reason: 'es_reason' } }, + }); + + const task = updateMappings({ + client, + index: 'new_index', + mappings: { + properties: { + created_at: { + type: 'date', + }, + }, + _meta: {}, + }, + }); + try { + await task(); + } catch (e) { + /** ignore */ + } + + expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); + }); + + it('updates the mapping information of the desired index', async () => { + const client = elasticsearchClientMock.createInternalClient(); + + const task = updateMappings({ + client, + index: 'new_index', + mappings: { + properties: { + created_at: { + type: 'date', + }, + }, + _meta: { + migrationMappingPropertyHashes: { + references: '7997cf5a56cc02bdc9c93361bde732b0', + 'epm-packages': '860e23f4404fa1c33f430e6dad5d8fa2', + 'cases-connector-mappings': '17d2e9e0e170a21a471285a5d845353c', + }, + }, + }, + }); + + const res = await task(); + expect(Either.isRight(res)).toBe(true); + expect(client.indices.putMapping).toHaveBeenCalledTimes(1); + expect(client.indices.putMapping).toHaveBeenCalledWith({ + index: 'new_index', + timeout: DEFAULT_TIMEOUT, + properties: { + created_at: { + type: 'date', + }, + }, + _meta: { + migrationMappingPropertyHashes: { + references: '7997cf5a56cc02bdc9c93361bde732b0', + 'epm-packages': '860e23f4404fa1c33f430e6dad5d8fa2', + 'cases-connector-mappings': '17d2e9e0e170a21a471285a5d845353c', + }, + }, + }); + }); +}); diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_mappings.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_mappings.ts new file mode 100644 index 0000000000000..4cf57f3ce7a8d --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_mappings.ts @@ -0,0 +1,63 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as Either from 'fp-ts/lib/Either'; +import * as TaskEither from 'fp-ts/lib/TaskEither'; +import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import type { IndexMapping } from '@kbn/core-saved-objects-base-server-internal'; +import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors'; +import type { RetryableEsClientError } from './catch_retryable_es_client_errors'; +import { DEFAULT_TIMEOUT } from './constants'; + +/** @internal */ +export interface UpdateMappingsParams { + client: ElasticsearchClient; + index: string; + mappings: IndexMapping; +} + +/** @internal */ +export interface IncompatibleMappingException { + type: 'incompatible_mapping_exception'; +} + +/** + * Updates an index's mappings and runs an pickupUpdatedMappings task so that the mapping + * changes are "picked up". Returns a taskId to track progress. + */ +export const updateMappings = ({ + client, + index, + mappings, +}: UpdateMappingsParams): TaskEither.TaskEither< + RetryableEsClientError | IncompatibleMappingException, + 'update_mappings_succeeded' +> => { + return () => { + return client.indices + .putMapping({ + index, + timeout: DEFAULT_TIMEOUT, + ...mappings, + }) + .then(() => Either.right('update_mappings_succeeded' as const)) + .catch((res) => { + const errorType = res?.body?.error?.type; + // ES throws this exact error when attempting to make incompatible updates to the mappigns + if ( + res?.statusCode === 400 && + (errorType === 'illegal_argument_exception' || + errorType === 'strict_dynamic_mapping_exception' || + errorType === 'mapper_parsing_exception') + ) { + return Either.left({ type: 'incompatible_mapping_exception' }); + } + return catchRetryableEsClientErrors(res); + }); + }; +}; diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_target_mappings_meta.test.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_target_mappings_meta.test.ts deleted file mode 100644 index 9116d5389f2ec..0000000000000 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_target_mappings_meta.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors'; -import { errors as EsErrors } from '@elastic/elasticsearch'; -import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; -import { updateTargetMappingsMeta } from './update_target_mappings_meta'; -import { DEFAULT_TIMEOUT } from './constants'; - -jest.mock('./catch_retryable_es_client_errors'); - -describe('updateTargetMappingsMeta', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - // Create a mock client that rejects all methods with a 503 status code - // response. - const retryableError = new EsErrors.ResponseError( - elasticsearchClientMock.createApiResponse({ - statusCode: 503, - body: { error: { type: 'es_type', reason: 'es_reason' } }, - }) - ); - const client = elasticsearchClientMock.createInternalClient( - elasticsearchClientMock.createErrorTransportRequestPromise(retryableError) - ); - - it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = updateTargetMappingsMeta({ - client, - index: 'new_index', - meta: {}, - }); - try { - await task(); - } catch (e) { - /** ignore */ - } - - expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); - }); - - it('updates the _meta information of the desired index', async () => { - const task = updateTargetMappingsMeta({ - client, - index: 'new_index', - meta: { - migrationMappingPropertyHashes: { - references: '7997cf5a56cc02bdc9c93361bde732b0', - 'epm-packages': '860e23f4404fa1c33f430e6dad5d8fa2', - 'cases-connector-mappings': '17d2e9e0e170a21a471285a5d845353c', - }, - }, - }); - try { - await task(); - } catch (e) { - /** ignore */ - } - - expect(client.indices.putMapping).toHaveBeenCalledTimes(1); - expect(client.indices.putMapping).toHaveBeenCalledWith({ - index: 'new_index', - timeout: DEFAULT_TIMEOUT, - _meta: { - migrationMappingPropertyHashes: { - references: '7997cf5a56cc02bdc9c93361bde732b0', - 'epm-packages': '860e23f4404fa1c33f430e6dad5d8fa2', - 'cases-connector-mappings': '17d2e9e0e170a21a471285a5d845353c', - }, - }, - }); - }); -}); diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_target_mappings_meta.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_target_mappings_meta.ts deleted file mode 100644 index 05f954b38fe71..0000000000000 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/update_target_mappings_meta.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import * as Either from 'fp-ts/lib/Either'; -import * as TaskEither from 'fp-ts/lib/TaskEither'; - -import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { IndexMappingMeta } from '@kbn/core-saved-objects-base-server-internal'; - -import { - catchRetryableEsClientErrors, - RetryableEsClientError, -} from './catch_retryable_es_client_errors'; -import { DEFAULT_TIMEOUT } from './constants'; - -/** @internal */ -export interface UpdateTargetMappingsMetaParams { - client: ElasticsearchClient; - index: string; - meta?: IndexMappingMeta; -} -/** - * Updates an index's mappings _meta information - */ -export const updateTargetMappingsMeta = - ({ - client, - index, - meta, - }: UpdateTargetMappingsMetaParams): TaskEither.TaskEither< - RetryableEsClientError, - 'update_mappings_meta_succeeded' - > => - () => { - return client.indices - .putMapping({ - index, - timeout: DEFAULT_TIMEOUT, - _meta: meta || {}, - }) - .then(() => { - // Ignore `acknowledged: false`. When the coordinating node accepts - // the new cluster state update but not all nodes have applied the - // update within the timeout `acknowledged` will be false. However, - // retrying this update will always immediately result in `acknowledged: - // true` even if there are still nodes which are falling behind with - // cluster state updates. - return Either.right('update_mappings_meta_succeeded' as const); - }) - .catch(catchRetryableEsClientErrors); - }; diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/helpers.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/helpers.ts index 19ef5d66c0eb5..15691632a4399 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/helpers.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/helpers.ts @@ -107,12 +107,12 @@ export function addExcludedTypesToBoolQuery( /** * Add the given clauses to the 'must' of the given query + * @param filterClauses the clauses to be added to a 'must' * @param boolQuery the bool query to be enriched - * @param mustClauses the clauses to be added to a 'must' * @returns a new query container with the enriched query */ export function addMustClausesToBoolQuery( - mustClauses: QueryDslQueryContainer[], + filterClauses: QueryDslQueryContainer[], boolQuery?: QueryDslBoolQuery ): QueryDslQueryContainer { let must: QueryDslQueryContainer[] = []; @@ -121,7 +121,7 @@ export function addMustClausesToBoolQuery( must = must.concat(boolQuery.must); } - must.push(...mustClauses); + must.push(...filterClauses); return { bool: { @@ -133,8 +133,8 @@ export function addMustClausesToBoolQuery( /** * Add the given clauses to the 'must_not' of the given query - * @param boolQuery the bool query to be enriched * @param filterClauses the clauses to be added to a 'must_not' + * @param boolQuery the bool query to be enriched * @returns a new query container with the enriched query */ export function addMustNotClausesToBoolQuery( diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/model.test.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/model.test.ts index c07538d1c1184..4eccf11e6a65b 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/model.test.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/model.test.ts @@ -13,6 +13,7 @@ import type { IndexMapping } from '@kbn/core-saved-objects-base-server-internal' import type { BaseState, CalculateExcludeFiltersState, + UpdateSourceMappingsState, CheckTargetMappingsState, CheckUnknownDocumentsState, CheckVersionIndexReadyActions, @@ -1298,13 +1299,12 @@ describe('migrations v2 model', () => { sourceIndexMappings: actualMappings, }; - test('WAIT_FOR_YELLOW_SOURCE -> CHECK_UNKNOWN_DOCUMENTS', () => { + test('WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS', () => { const res: ResponseType<'WAIT_FOR_YELLOW_SOURCE'> = Either.right({}); const newState = model(changedMappingsState, res); - expect(newState.controlState).toEqual('CHECK_UNKNOWN_DOCUMENTS'); expect(newState).toMatchObject({ - controlState: 'CHECK_UNKNOWN_DOCUMENTS', + controlState: 'UPDATE_SOURCE_MAPPINGS', sourceIndex: Option.some('.kibana_7.11.0_001'), sourceIndexMappings: actualMappings, }); @@ -1330,6 +1330,49 @@ describe('migrations v2 model', () => { }); }); + describe('UPDATE_SOURCE_MAPPINGS', () => { + const checkCompatibleMappingsState: UpdateSourceMappingsState = { + ...baseState, + controlState: 'UPDATE_SOURCE_MAPPINGS', + sourceIndex: Option.some('.kibana_7.11.0_001') as Option.Some, + sourceIndexMappings: baseState.targetIndexMappings, + aliases: { + '.kibana': '.kibana_7.11.0_001', + '.kibana_7.11.0': '.kibana_7.11.0_001', + }, + }; + + describe('if action succeeds', () => { + test('UPDATE_SOURCE_MAPPINGS -> CLEANUP_UNKNOWN_AND_EXCLUDED', () => { + const res: ResponseType<'UPDATE_SOURCE_MAPPINGS'> = Either.right( + 'update_mappings_succeeded' as const + ); + const newState = model(checkCompatibleMappingsState, res); + + expect(newState).toMatchObject({ + controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED', + targetIndex: '.kibana_7.11.0_001', + versionIndexReadyActions: Option.none, + }); + }); + }); + + describe('if action fails', () => { + test('UPDATE_SOURCE_MAPPINGS -> CHECK_UNKNOWN_DOCUMENTS', () => { + const res: ResponseType<'UPDATE_SOURCE_MAPPINGS'> = Either.left({ + type: 'incompatible_mapping_exception', + }); + const newState = model(checkCompatibleMappingsState, res); + + expect(newState).toMatchObject({ + controlState: 'CHECK_UNKNOWN_DOCUMENTS', + sourceIndex: Option.some('.kibana_7.11.0_001'), + sourceIndexMappings: baseState.targetIndexMappings, + }); + }); + }); + }); + describe('CLEANUP_UNKNOWN_AND_EXCLUDED', () => { const cleanupUnknownAndExcluded: CleanupUnknownAndExcluded = { ...baseState, @@ -2693,7 +2736,7 @@ describe('migrations v2 model', () => { test('UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS if the mapping _meta information is successfully updated', () => { const res: ResponseType<'UPDATE_TARGET_MAPPINGS_META'> = Either.right( - 'update_mappings_meta_succeeded' + 'update_mappings_succeeded' ); const newState = model(updateTargetMappingsMetaState, res) as CheckVersionIndexReadyActions; expect(newState.controlState).toBe('CHECK_VERSION_INDEX_READY_ACTIONS'); diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/model.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/model.ts index 4c2a9147eb125..f1cb94d276b35 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/model.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/model.ts @@ -424,7 +424,6 @@ export const model = (currentState: State, resW: ResponseType): } else if (stateP.controlState === 'WAIT_FOR_YELLOW_SOURCE') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { - // check the existing mappings to see if we can avoid reindexing if ( // source exists Boolean(stateP.sourceIndexMappings._meta?.migrationMappingPropertyHashes) && @@ -434,9 +433,9 @@ export const model = (currentState: State, resW: ResponseType): stateP.sourceIndexMappings, /* expected */ stateP.targetIndexMappings - ) && - Math.random() < 10 + ) ) { + // the existing mappings match, we can avoid reindexing return { ...stateP, controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED', @@ -446,7 +445,7 @@ export const model = (currentState: State, resW: ResponseType): } else { return { ...stateP, - controlState: 'CHECK_UNKNOWN_DOCUMENTS', + controlState: 'UPDATE_SOURCE_MAPPINGS', }; } } else if (Either.isLeft(res)) { @@ -465,6 +464,28 @@ export const model = (currentState: State, resW: ResponseType): } else { return throwBadResponse(stateP, res); } + } else if (stateP.controlState === 'UPDATE_SOURCE_MAPPINGS') { + const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { + return { + ...stateP, + controlState: 'CLEANUP_UNKNOWN_AND_EXCLUDED', + targetIndex: stateP.sourceIndex.value!, // We preserve the same index, source == target (E.g: ".xx8.7.0_001") + versionIndexReadyActions: Option.none, + }; + } else if (Either.isLeft(res)) { + const left = res.left; + if (isTypeof(left, 'incompatible_mapping_exception')) { + return { + ...stateP, + controlState: 'CHECK_UNKNOWN_DOCUMENTS', + }; + } else { + return throwBadResponse(stateP, left as never); + } + } else { + return throwBadResponse(stateP, res); + } } else if (stateP.controlState === 'CLEANUP_UNKNOWN_AND_EXCLUDED') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/next.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/next.ts index 605dd149855e7..8cebce9995900 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/next.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/next.ts @@ -7,44 +7,46 @@ */ import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { omit } from 'lodash'; import type { AllActionStates, - ReindexSourceToTempOpenPit, - ReindexSourceToTempRead, - ReindexSourceToTempClosePit, - ReindexSourceToTempTransform, - MarkVersionIndexReady, + CalculateExcludeFiltersState, + UpdateSourceMappingsState, + CheckTargetMappingsState, + CheckUnknownDocumentsState, + CleanupUnknownAndExcluded, + CleanupUnknownAndExcludedWaitForTaskState, + CloneTempToSource, + CreateNewTargetState, + CreateReindexTempState, InitState, LegacyCreateReindexTargetState, LegacyDeleteState, LegacyReindexState, LegacyReindexWaitForTaskState, LegacySetWriteBlockState, + MarkVersionIndexReady, + MarkVersionIndexReadyConflict, + OutdatedDocumentsRefresh, + OutdatedDocumentsSearchClosePit, + OutdatedDocumentsSearchOpenPit, + OutdatedDocumentsSearchRead, OutdatedDocumentsTransform, + PrepareCompatibleMigration, + RefreshTarget, + ReindexSourceToTempClosePit, + ReindexSourceToTempIndexBulk, + ReindexSourceToTempOpenPit, + ReindexSourceToTempRead, + ReindexSourceToTempTransform, SetSourceWriteBlockState, + SetTempWriteBlock, State, + TransformedDocumentsBulkIndex, UpdateTargetMappingsState, UpdateTargetMappingsWaitForTaskState, - CreateReindexTempState, - MarkVersionIndexReadyConflict, - CreateNewTargetState, - CloneTempToSource, - SetTempWriteBlock, - WaitForYellowSourceState, - TransformedDocumentsBulkIndex, - ReindexSourceToTempIndexBulk, - OutdatedDocumentsSearchOpenPit, - OutdatedDocumentsSearchRead, - OutdatedDocumentsSearchClosePit, - RefreshTarget, - OutdatedDocumentsRefresh, - CheckUnknownDocumentsState, - CalculateExcludeFiltersState, WaitForMigrationCompletionState, - CheckTargetMappingsState, - PrepareCompatibleMigration, - CleanupUnknownAndExcluded, - CleanupUnknownAndExcludedWaitForTaskState, + WaitForYellowSourceState, } from './state'; import type { TransformRawDocs } from './types'; import * as Actions from './actions'; @@ -70,6 +72,12 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra Actions.fetchIndices({ client, indices: [state.currentAlias, state.versionAlias] }), WAIT_FOR_YELLOW_SOURCE: (state: WaitForYellowSourceState) => Actions.waitForIndexStatus({ client, index: state.sourceIndex.value, status: 'yellow' }), + UPDATE_SOURCE_MAPPINGS: (state: UpdateSourceMappingsState) => + Actions.updateMappings({ + client, + index: state.sourceIndex.value, // attempt to update source mappings in-place + mappings: omit(state.targetIndexMappings, ['_meta']), // ._meta property will be updated on a later step + }), CLEANUP_UNKNOWN_AND_EXCLUDED: (state: CleanupUnknownAndExcluded) => Actions.cleanupUnknownAndExcluded({ client, @@ -163,7 +171,7 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra Actions.updateAndPickupMappings({ client, index: state.targetIndex, - mappings: state.targetIndexMappings, + mappings: omit(state.targetIndexMappings, ['_meta']), // ._meta property will be updated on a later step }), UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK: (state: UpdateTargetMappingsWaitForTaskState) => Actions.waitForPickupUpdatedMappingsTask({ @@ -172,10 +180,10 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra timeout: '60s', }), UPDATE_TARGET_MAPPINGS_META: (state: UpdateTargetMappingsState) => - Actions.updateTargetMappingsMeta({ + Actions.updateMappings({ client, index: state.targetIndex, - meta: state.targetIndexMappings._meta, + mappings: state.targetIndexMappings, }), CHECK_VERSION_INDEX_READY_ACTIONS: () => Actions.noop, OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT: (state: OutdatedDocumentsSearchOpenPit) => diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/state.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/state.ts index a091a2972343f..4ac550c89ff58 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/state.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/state.ts @@ -243,6 +243,13 @@ export interface WaitForYellowSourceState extends BaseWithSource { readonly aliases: Record; } +export interface UpdateSourceMappingsState extends BaseState { + readonly controlState: 'UPDATE_SOURCE_MAPPINGS'; + readonly sourceIndex: Option.Some; + readonly sourceIndexMappings: IndexMapping; + readonly aliases: Record; +} + export interface CheckUnknownDocumentsState extends BaseWithSource { /** Check if any unknown document is present in the source index */ readonly controlState: 'CHECK_UNKNOWN_DOCUMENTS'; @@ -493,6 +500,7 @@ export type State = Readonly< | WaitForMigrationCompletionState | DoneState | WaitForYellowSourceState + | UpdateSourceMappingsState | CheckUnknownDocumentsState | SetSourceWriteBlockState | CalculateExcludeFiltersState diff --git a/src/core/server/integration_tests/saved_objects/migrations/archives/7.13.0_with_corrupted_so.zip b/src/core/server/integration_tests/saved_objects/migrations/archives/7.13.0_with_corrupted_so.zip deleted file mode 100644 index f4a89fbcb251480f71b36698f9b9ad28ac212079..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46485 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 ZyHfxM)C*l+a)|(?-~$0I$6fx{{{f3b)u;df diff --git a/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_failed_action_tasks.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_failed_action_tasks.test.ts index 1538e9cfe8ef4..89478ea377f6c 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_failed_action_tasks.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_failed_action_tasks.test.ts @@ -24,90 +24,104 @@ async function removeLogFile() { } describe('migration from 7.13 to 7.14+ with many failed action_tasks', () => { - let esServer: TestElasticsearchUtils; - let root: Root; - let startES: () => Promise; + describe('if mappings are incompatible (reindex required)', () => { + let esServer: TestElasticsearchUtils; + let root: Root; + let startES: () => Promise; - beforeAll(async () => { - await removeLogFile(); - }); + beforeAll(async () => { + await removeLogFile(); + }); - beforeEach(() => { - ({ startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - dataArchive: Path.join(__dirname, '..', 'archives', '7.13_1.5k_failed_action_tasks.zip'), + beforeEach(() => { + ({ startES } = createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'basic', + dataArchive: Path.join( + __dirname, + '..', + 'archives', + '7.13_1.5k_failed_action_tasks.zip' + ), + }, }, - }, - })); - }); + })); + }); - afterEach(async () => { - if (root) { - await root.shutdown(); - } - if (esServer) { - await esServer.stop(); - } + afterEach(async () => { + if (root) { + await root.shutdown(); + } + if (esServer) { + await esServer.stop(); + } - await new Promise((resolve) => setTimeout(resolve, 10000)); - }); + await new Promise((resolve) => setTimeout(resolve, 10000)); + }); - const getCounts = async ( - kibanaIndexName = '.kibana', - taskManagerIndexName = '.kibana_task_manager' - ): Promise<{ tasksCount: number; actionTaskParamsCount: number }> => { - const esClient: ElasticsearchClient = esServer.es.getClient(); - - const actionTaskParamsResponse = await esClient.count({ - index: kibanaIndexName, - body: { - query: { - bool: { must: { term: { type: 'action_task_params' } } }, + const getCounts = async ( + kibanaIndexName = '.kibana', + taskManagerIndexName = '.kibana_task_manager' + ): Promise<{ tasksCount: number; actionTaskParamsCount: number }> => { + const esClient: ElasticsearchClient = esServer.es.getClient(); + + const actionTaskParamsResponse = await esClient.count({ + index: kibanaIndexName, + body: { + query: { + bool: { must: { term: { type: 'action_task_params' } } }, + }, }, - }, - }); - const tasksResponse = await esClient.count({ - index: taskManagerIndexName, - body: { - query: { - bool: { must: { term: { type: 'task' } } }, + }); + const tasksResponse = await esClient.count({ + index: taskManagerIndexName, + body: { + query: { + bool: { must: { term: { type: 'task' } } }, + }, }, - }, - }); + }); - return { - actionTaskParamsCount: actionTaskParamsResponse.count, - tasksCount: tasksResponse.count, + return { + actionTaskParamsCount: actionTaskParamsResponse.count, + tasksCount: tasksResponse.count, + }; }; - }; - - it('filters out all outdated action_task_params and action tasks', async () => { - esServer = await startES(); - - // Verify counts in current index before migration starts - expect(await getCounts()).toEqual({ - actionTaskParamsCount: 2010, - tasksCount: 2020, - }); - root = createRoot(); - await root.preboot(); - await root.setup(); - await root.start(); - - // Bulk of tasks should have been filtered out of current index - const { actionTaskParamsCount, tasksCount } = await getCounts(); - // Use toBeLessThan to avoid flakiness in the case that TM starts manipulating docs before the counts are taken - expect(actionTaskParamsCount).toBeLessThan(1000); - expect(tasksCount).toBeLessThan(1000); - - // Verify that docs were not deleted from old index - expect(await getCounts('.kibana_7.13.5_001', '.kibana_task_manager_7.13.5_001')).toEqual({ - actionTaskParamsCount: 2010, - tasksCount: 2020, + it('filters out all outdated action_task_params and action tasks', async () => { + esServer = await startES(); + + // Verify counts in current index before migration starts + expect(await getCounts()).toEqual({ + actionTaskParamsCount: 2010, + tasksCount: 2020, + }); + + root = createRoot(); + await root.preboot(); + await root.setup(); + await root.start(); + + // Bulk of tasks should have been filtered out of current index + const { actionTaskParamsCount, tasksCount } = await getCounts(); + // Use toBeLessThan to avoid flakiness in the case that TM starts manipulating docs before the counts are taken + expect(actionTaskParamsCount).toBeLessThan(1000); + expect(tasksCount).toBeLessThan(1000); + + const { + actionTaskParamsCount: oldIndexActionTaskParamsCount, + tasksCount: oldIndexTasksCount, + } = await getCounts('.kibana_7.13.5_001', '.kibana_task_manager_7.13.5_001'); + + // .kibana mappings changes are NOT compatible, we reindex and preserve old index's documents + expect(oldIndexActionTaskParamsCount).toEqual(2010); + + // ATM .kibana_task_manager mappings changes are compatible, we skip reindex and actively delete unwanted documents + // if the mappings become incompatible in the future, the we will reindex and the old index must still contain all 2020 docs + // if the mappings remain compatible, we reuse the existing index and actively delete unwanted documents from it + expect(oldIndexTasksCount === 2020 || oldIndexTasksCount < 1000).toEqual(true); }); }); }); diff --git a/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_transform_failures.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_transform_failures.test.ts index 065a4a4241d07..8a798508ce18f 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_transform_failures.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_transform_failures.test.ts @@ -122,30 +122,30 @@ describe('migration v2', () => { // 23 saved objects + 14 corrupt (discarded) = 37 total in the old index expect((docs.hits.total as SearchTotalHits).value).toEqual(23); - expect(docs.hits.hits.map(({ _id }) => _id)).toEqual([ + expect(docs.hits.hits.map(({ _id }) => _id).sort()).toEqual([ 'config:7.13.0', 'index-pattern:logs-*', 'index-pattern:metrics-*', + 'ui-metric:console:DELETE_delete', + 'ui-metric:console:GET_get', + 'ui-metric:console:GET_search', + 'ui-metric:console:POST_delete_by_query', + 'ui-metric:console:POST_index', + 'ui-metric:console:PUT_indices.put_mapping', 'usage-counters:uiCounter:21052021:click:global_search_bar:user_navigated_to_application', + 'usage-counters:uiCounter:21052021:click:global_search_bar:user_navigated_to_application_unknown', + 'usage-counters:uiCounter:21052021:count:console:DELETE_delete', 'usage-counters:uiCounter:21052021:count:console:GET_cat.aliases', - 'usage-counters:uiCounter:21052021:loaded:console:opened_app', 'usage-counters:uiCounter:21052021:count:console:GET_cat.indices', + 'usage-counters:uiCounter:21052021:count:console:GET_get', + 'usage-counters:uiCounter:21052021:count:console:GET_search', + 'usage-counters:uiCounter:21052021:count:console:POST_delete_by_query', + 'usage-counters:uiCounter:21052021:count:console:POST_index', + 'usage-counters:uiCounter:21052021:count:console:PUT_indices.put_mapping', 'usage-counters:uiCounter:21052021:count:global_search_bar:search_focus', - 'usage-counters:uiCounter:21052021:click:global_search_bar:user_navigated_to_application_unknown', 'usage-counters:uiCounter:21052021:count:global_search_bar:search_request', 'usage-counters:uiCounter:21052021:count:global_search_bar:shortcut_used', - 'ui-metric:console:POST_delete_by_query', - 'usage-counters:uiCounter:21052021:count:console:PUT_indices.put_mapping', - 'usage-counters:uiCounter:21052021:count:console:POST_delete_by_query', - 'usage-counters:uiCounter:21052021:count:console:GET_search', - 'ui-metric:console:PUT_indices.put_mapping', - 'ui-metric:console:GET_search', - 'usage-counters:uiCounter:21052021:count:console:DELETE_delete', - 'ui-metric:console:DELETE_delete', - 'usage-counters:uiCounter:21052021:count:console:GET_get', - 'ui-metric:console:GET_get', - 'usage-counters:uiCounter:21052021:count:console:POST_index', - 'ui-metric:console:POST_index', + 'usage-counters:uiCounter:21052021:loaded:console:opened_app', ]); }); }); diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_target_mappings.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_target_mappings.test.ts index 19326c15e0f25..54ab116d2c596 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_target_mappings.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_target_mappings.test.ts @@ -8,12 +8,10 @@ import Path from 'path'; import fs from 'fs/promises'; -import JSON5 from 'json5'; import { Env } from '@kbn/config'; import { REPO_ROOT } from '@kbn/repo-info'; import { getEnvOptions } from '@kbn/config-mocks'; import { Root } from '@kbn/core-root-server-internal'; -import { LogRecord } from '@kbn/logging'; import { createRootWithCorePlugins, createTestServers, @@ -23,30 +21,14 @@ import { delay } from '../test_utils'; const logFilePath = Path.join(__dirname, 'check_target_mappings.log'); -async function removeLogFile() { - // ignore errors if it doesn't exist - await fs.unlink(logFilePath).catch(() => void 0); -} - -async function parseLogFile() { - const logFileContent = await fs.readFile(logFilePath, 'utf-8'); - - return logFileContent - .split('\n') - .filter(Boolean) - .map((str) => JSON5.parse(str)) as LogRecord[]; -} - -function logIncludes(logs: LogRecord[], message: string): boolean { - return Boolean(logs?.find((rec) => rec.message.includes(message))); -} - describe('migration v2 - CHECK_TARGET_MAPPINGS', () => { let esServer: TestElasticsearchUtils; let root: Root; - let logs: LogRecord[]; + let logs: string; - beforeEach(async () => await removeLogFile()); + beforeEach(async () => { + await fs.unlink(logFilePath).catch(() => {}); + }); afterEach(async () => { await root?.shutdown(); @@ -71,9 +53,10 @@ describe('migration v2 - CHECK_TARGET_MAPPINGS', () => { await root.start(); // Check for migration steps present in the logs - logs = await parseLogFile(); - expect(logIncludes(logs, 'CREATE_NEW_TARGET')).toEqual(true); - expect(logIncludes(logs, 'CHECK_TARGET_MAPPINGS')).toEqual(false); + logs = await fs.readFile(logFilePath, 'utf-8'); + + expect(logs).toMatch('CREATE_NEW_TARGET'); + expect(logs).not.toMatch('CHECK_TARGET_MAPPINGS'); }); describe('when the indices are aligned with the stack version', () => { @@ -98,7 +81,7 @@ describe('migration v2 - CHECK_TARGET_MAPPINGS', () => { // stop Kibana and remove logs await root.shutdown(); await delay(10); - await removeLogFile(); + await fs.unlink(logFilePath).catch(() => {}); root = createRoot(); await root.preboot(); @@ -106,14 +89,12 @@ describe('migration v2 - CHECK_TARGET_MAPPINGS', () => { await root.start(); // Check for migration steps present in the logs - logs = await parseLogFile(); - expect(logIncludes(logs, 'CREATE_NEW_TARGET')).toEqual(false); - expect( - logIncludes(logs, 'CHECK_TARGET_MAPPINGS -> CHECK_VERSION_INDEX_READY_ACTIONS') - ).toEqual(true); - expect(logIncludes(logs, 'UPDATE_TARGET_MAPPINGS')).toEqual(false); - expect(logIncludes(logs, 'UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK')).toEqual(false); - expect(logIncludes(logs, 'UPDATE_TARGET_MAPPINGS_META')).toEqual(false); + logs = await fs.readFile(logFilePath, 'utf-8'); + expect(logs).not.toMatch('CREATE_NEW_TARGET'); + expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> CHECK_VERSION_INDEX_READY_ACTIONS'); + expect(logs).not.toMatch('UPDATE_TARGET_MAPPINGS'); + expect(logs).not.toMatch('UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK'); + expect(logs).not.toMatch('UPDATE_TARGET_MAPPINGS_META'); }); }); @@ -140,23 +121,13 @@ describe('migration v2 - CHECK_TARGET_MAPPINGS', () => { await root.start(); // Check for migration steps present in the logs - logs = await parseLogFile(); - expect(logIncludes(logs, 'CREATE_NEW_TARGET')).toEqual(false); - expect(logIncludes(logs, 'CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS')).toEqual(true); - expect( - logIncludes(logs, 'UPDATE_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK') - ).toEqual(true); - expect( - logIncludes(logs, 'UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK -> UPDATE_TARGET_MAPPINGS_META') - ).toEqual(true); - expect( - logIncludes(logs, 'UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS') - ).toEqual(true); - expect( - logIncludes(logs, 'CHECK_VERSION_INDEX_READY_ACTIONS -> MARK_VERSION_INDEX_READY') - ).toEqual(true); - expect(logIncludes(logs, 'MARK_VERSION_INDEX_READY -> DONE')).toEqual(true); - expect(logIncludes(logs, 'Migration completed')).toEqual(true); + logs = await fs.readFile(logFilePath, 'utf-8'); + expect(logs).not.toMatch('CREATE_NEW_TARGET'); + expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS'); + expect(logs).toMatch('UPDATE_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK'); + expect(logs).toMatch('UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK -> UPDATE_TARGET_MAPPINGS_META'); + expect(logs).toMatch('UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS'); + expect(logs).toMatch('Migration completed'); }); }); }); diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/cleanup.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/cleanup.test.ts index 8df491c36f4e3..e1030fe9805e9 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/cleanup.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/cleanup.test.ts @@ -10,122 +10,48 @@ import Path from 'path'; import Fs from 'fs'; import Util from 'util'; import JSON5 from 'json5'; +import { type TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server'; +import { SavedObjectsType } from '@kbn/core-saved-objects-server'; +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { getMigrationDocLink, delay } from '../test_utils'; import { - createTestServers, - createRootWithCorePlugins, - type TestElasticsearchUtils, -} from '@kbn/core-test-helpers-kbn-server'; -import { Root } from '@kbn/core-root-server-internal'; -import { getMigrationDocLink } from '../test_utils'; + clearLog, + currentVersion, + defaultKibanaIndex, + getKibanaMigratorTestKit, + nextMinor, + startElasticsearch, +} from '../kibana_migrator_test_kit'; const migrationDocLink = getMigrationDocLink().resolveMigrationFailures; const logFilePath = Path.join(__dirname, 'cleanup.log'); -const asyncUnlink = Util.promisify(Fs.unlink); const asyncReadFile = Util.promisify(Fs.readFile); -async function removeLogFile() { - // ignore errors if it doesn't exist - await asyncUnlink(logFilePath).catch(() => void 0); -} - -function createRoot() { - return createRootWithCorePlugins( - { - migrations: { - skip: false, - }, - logging: { - appenders: { - file: { - type: 'file', - fileName: logFilePath, - layout: { - type: 'json', - }, - }, - }, - loggers: [ - { - name: 'root', - appenders: ['file'], - level: 'debug', // DEBUG logs are required to retrieve the PIT _id from the action response logs - }, - ], - }, - }, - { - oss: true, - } - ); -} - describe('migration v2', () => { - let esServer: TestElasticsearchUtils; - let root: Root; + let esServer: TestElasticsearchUtils['es']; + let esClient: ElasticsearchClient; beforeAll(async () => { - await removeLogFile(); + esServer = await startElasticsearch(); }); - afterAll(async () => { - if (root) { - await root.shutdown(); - } - if (esServer) { - await esServer.stop(); - } - - await new Promise((resolve) => setTimeout(resolve, 10000)); + beforeEach(async () => { + esClient = await setupBaseline(); + await clearLog(logFilePath); }); it('clean ups if migration fails', async () => { - const { startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - // original SO: - // { - // _index: '.kibana_7.13.0_001', - // _type: '_doc', - // _id: 'index-pattern:test_index*', - // _version: 1, - // result: 'created', - // _shards: { total: 2, successful: 1, failed: 0 }, - // _seq_no: 0, - // _primary_term: 1 - // } - dataArchive: Path.join(__dirname, '..', 'archives', '7.13.0_with_corrupted_so.zip'), - }, - }, - }); - - root = createRoot(); - - esServer = await startES(); - await root.preboot(); - const coreSetup = await root.setup(); - - coreSetup.savedObjects.registerType({ - name: 'foo', - hidden: false, - mappings: { - properties: {}, - }, - namespaceType: 'agnostic', - migrations: { - '7.14.0': (doc) => doc, - }, - }); + const { migrator, client } = await setupNextMinor(); + migrator.prepareMigrations(); - await expect(root.start()).rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: 1 corrupt saved object documents were found: index-pattern:test_index* + await expect(migrator.runMigrations()).rejects.toThrowErrorMatchingInlineSnapshot(` + "Unable to complete saved object migrations for the [${defaultKibanaIndex}] index: Migrations failed. Reason: 1 corrupt saved object documents were found: corrupt:2baf4de0-a6d4-11ed-ba5a-39196fc76e60 - To allow migrations to proceed, please delete or fix these documents. - Note that you can configure Kibana to automatically discard corrupt documents and transform errors for this migration. - Please refer to ${migrationDocLink} for more information." - `); + To allow migrations to proceed, please delete or fix these documents. + Note that you can configure Kibana to automatically discard corrupt documents and transform errors for this migration. + Please refer to ${migrationDocLink} for more information." + `); const logFileContent = await asyncReadFile(logFilePath, 'utf-8'); const records = logFileContent @@ -134,7 +60,7 @@ describe('migration v2', () => { .map((str) => JSON5.parse(str)); const logRecordWithPit = records.find( - (rec) => rec.message === '[.kibana] REINDEX_SOURCE_TO_TEMP_OPEN_PIT RESPONSE' + (rec) => rec.message === `[${defaultKibanaIndex}] REINDEX_SOURCE_TO_TEMP_OPEN_PIT RESPONSE` ); expect(logRecordWithPit).toBeTruthy(); @@ -142,7 +68,6 @@ describe('migration v2', () => { const pitId = logRecordWithPit.right.pitId; expect(pitId).toBeTruthy(); - const client = esServer.es.getClient(); await expect( client.search({ body: { @@ -152,4 +77,132 @@ describe('migration v2', () => { // throws an exception that cannot search with closed PIT ).rejects.toThrow(/search_phase_execution_exception/); }); + + afterEach(async () => { + await esClient?.indices.delete({ index: `${defaultKibanaIndex}_${currentVersion}_001` }); + }); + + afterAll(async () => { + await esServer?.stop(); + await delay(10); + }); }); + +const setupBaseline = async () => { + const typesCurrent: SavedObjectsType[] = [ + { + name: 'complex', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + name: { type: 'text' }, + value: { type: 'integer' }, + }, + }, + migrations: {}, + }, + ]; + + const savedObjects = [ + { + id: 'complex:4baf4de0-a6d4-11ed-ba5a-39196fc76e60', + body: { + type: 'complex', + complex: { + name: 'foo', + value: 5, + }, + references: [], + coreMigrationVersion: currentVersion, + updated_at: '2023-02-07T11:04:44.914Z', + created_at: '2023-02-07T11:04:44.914Z', + }, + }, + { + id: 'corrupt:2baf4de0-a6d4-11ed-ba5a-39196fc76e60', // incorrect id => corrupt object + body: { + type: 'complex', + complex: { + name: 'bar', + value: 3, + }, + references: [], + coreMigrationVersion: currentVersion, + updated_at: '2023-02-07T11:04:44.914Z', + created_at: '2023-02-07T11:04:44.914Z', + }, + }, + ]; + + const { migrator: baselineMigrator, client } = await getKibanaMigratorTestKit({ + types: typesCurrent, + logFilePath, + }); + + baselineMigrator.prepareMigrations(); + await baselineMigrator.runMigrations(); + + // inject corrupt saved objects directly using esClient + await Promise.all( + savedObjects.map((savedObject) => { + client.create({ + index: defaultKibanaIndex, + refresh: 'wait_for', + ...savedObject, + }); + }) + ); + + return client; +}; + +const setupNextMinor = async () => { + const typesNextMinor: SavedObjectsType[] = [ + { + name: 'complex', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + name: { type: 'keyword' }, + value: { type: 'long' }, + }, + }, + migrations: { + [nextMinor]: (doc) => doc, + }, + }, + ]; + + const { migrator, client } = await getKibanaMigratorTestKit({ + types: typesNextMinor, + kibanaVersion: nextMinor, + logFilePath, + settings: { + migrations: { + skip: false, + }, + logging: { + appenders: { + file: { + type: 'file', + fileName: logFilePath, + layout: { + type: 'json', + }, + }, + }, + loggers: [ + { + name: 'root', + appenders: ['file'], + level: 'debug', // DEBUG logs are required to retrieve the PIT _id from the action response logs + }, + ], + }, + }, + }); + + return { migrator, client }; +}; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kibana_nodes.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kibana_nodes.test.ts index f4577c0379096..51e3c7ce53fe0 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kibana_nodes.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kibana_nodes.test.ts @@ -112,7 +112,7 @@ describe('migration v2', () => { let rootB: Root; let rootC: Root; - const migratedIndex = `.kibana_${pkg.version}_001`; + const migratedIndexAlias = `.kibana_${pkg.version}`; const fooType: SavedObjectsType = { name: 'foo', hidden: false, @@ -189,7 +189,7 @@ describe('migration v2', () => { await startWithDelay([rootA, rootB, rootC], 0); const esClient = esServer.es.getClient(); - const migratedDocs = await fetchDocs(esClient, migratedIndex); + const migratedDocs = await fetchDocs(esClient, migratedIndexAlias); expect(migratedDocs.length).toBe(5000); @@ -208,7 +208,7 @@ describe('migration v2', () => { await startWithDelay([rootA, rootB, rootC], 1); const esClient = esServer.es.getClient(); - const migratedDocs = await fetchDocs(esClient, migratedIndex); + const migratedDocs = await fetchDocs(esClient, migratedIndexAlias); expect(migratedDocs.length).toBe(5000); @@ -227,7 +227,7 @@ describe('migration v2', () => { await startWithDelay([rootA, rootB, rootC], 5); const esClient = esServer.es.getClient(); - const migratedDocs = await fetchDocs(esClient, migratedIndex); + const migratedDocs = await fetchDocs(esClient, migratedIndexAlias); expect(migratedDocs.length).toBe(5000); @@ -246,7 +246,7 @@ describe('migration v2', () => { await startWithDelay([rootA, rootB, rootC], 20); const esClient = esServer.es.getClient(); - const migratedDocs = await fetchDocs(esClient, migratedIndex); + const migratedDocs = await fetchDocs(esClient, migratedIndexAlias); expect(migratedDocs.length).toBe(5000); diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/outdated_docs.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/outdated_docs.test.ts index 119ee16e9cddc..5c9ee13f3f825 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/outdated_docs.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/outdated_docs.test.ts @@ -46,7 +46,7 @@ describe('migration v2', () => { }); it('migrates the documents to the highest version', async () => { - const migratedIndex = `.kibana_${pkg.version}_001`; + const migratedIndexAlias = `.kibana_${pkg.version}`; const { startES } = createTestServers({ adjustTimeout: (t: number) => jest.setTimeout(t), settings: { @@ -90,7 +90,7 @@ describe('migration v2', () => { const coreStart = await root.start(); const esClient = coreStart.elasticsearch.client.asInternalUser; - const migratedDocs = await fetchDocs(esClient, migratedIndex); + const migratedDocs = await fetchDocs(esClient, migratedIndexAlias); expect(migratedDocs.length).toBe(1); const [doc] = migratedDocs; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions.test.ts index 691800feee0e3..64592be985719 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions.test.ts @@ -34,7 +34,7 @@ import { type UpdateByQueryResponse, updateAndPickupMappings, type UpdateAndPickupMappingsResponse, - updateTargetMappingsMeta, + updateMappings, removeWriteBlock, transformDocs, waitForIndexStatus, @@ -71,7 +71,11 @@ describe('migration actions', () => { indexName: 'existing_index_with_docs', mappings: { dynamic: true, - properties: {}, + properties: { + someProperty: { + type: 'integer', + }, + }, _meta: { migrationMappingPropertyHashes: { references: '7997cf5a56cc02bdc9c93361bde732b0', @@ -1486,15 +1490,22 @@ describe('migration actions', () => { }); }); - describe('updateTargetMappingsMeta', () => { + describe('updateMappings', () => { it('rejects if ES throws an error', async () => { - const task = updateTargetMappingsMeta({ + const task = updateMappings({ client, index: 'no_such_index', - meta: { - migrationMappingPropertyHashes: { - references: 'updateda56cc02bdc9c93361bupdated', - newReferences: 'fooBarHashMd509387420934879300d9', + mappings: { + properties: { + created_at: { + type: 'date', + }, + }, + _meta: { + migrationMappingPropertyHashes: { + references: 'updateda56cc02bdc9c93361bupdated', + newReferences: 'fooBarHashMd509387420934879300d9', + }, }, }, })(); @@ -1502,13 +1513,51 @@ describe('migration actions', () => { await expect(task).rejects.toThrow('index_not_found_exception'); }); - it('resolves right when mappings._meta are correctly updated', async () => { - const res = await updateTargetMappingsMeta({ + it('resolves left when the mappings are incompatible', async () => { + const res = await updateMappings({ client, index: 'existing_index_with_docs', - meta: { - migrationMappingPropertyHashes: { - newReferences: 'fooBarHashMd509387420934879300d9', + mappings: { + properties: { + someProperty: { + type: 'date', // attempt to change an existing field's type in an incompatible fashion + }, + }, + _meta: { + migrationMappingPropertyHashes: { + references: 'updateda56cc02bdc9c93361bupdated', + newReferences: 'fooBarHashMd509387420934879300d9', + }, + }, + }, + })(); + + expect(Either.isLeft(res)).toBe(true); + expect(res).toMatchInlineSnapshot(` + Object { + "_tag": "Left", + "left": Object { + "type": "incompatible_mapping_exception", + }, + } + `); + }); + + it('resolves right when mappings are correctly updated', async () => { + const res = await updateMappings({ + client, + index: 'existing_index_with_docs', + mappings: { + properties: { + created_at: { + type: 'date', + }, + }, + _meta: { + migrationMappingPropertyHashes: { + references: 'updateda56cc02bdc9c93361bupdated', + newReferences: 'fooBarHashMd509387420934879300d9', + }, }, }, })(); @@ -1519,8 +1568,17 @@ describe('migration actions', () => { index: ['existing_index_with_docs'], }); + expect(indices.existing_index_with_docs.mappings?.properties).toEqual( + expect.objectContaining({ + created_at: { + type: 'date', + }, + }) + ); + expect(indices.existing_index_with_docs.mappings?._meta).toEqual({ migrationMappingPropertyHashes: { + references: 'updateda56cc02bdc9c93361bupdated', newReferences: 'fooBarHashMd509387420934879300d9', }, }); diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/active_delete.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/active_delete.test.ts index 80681ffd0a5af..793ed9d100685 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/active_delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/active_delete.test.ts @@ -6,56 +6,28 @@ * Side Public License, v 1. */ -import Path from 'path'; -import fs from 'fs/promises'; -import { SemVer } from 'semver'; -import { Env } from '@kbn/config'; -import type { AggregationsAggregate, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import { getEnvOptions } from '@kbn/config-mocks'; -import { REPO_ROOT } from '@kbn/repo-info'; -import { createTestServers, type TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server'; -import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import type { SavedObjectsType } from '@kbn/core-saved-objects-server'; -import { getKibanaMigratorTestKit } from '../kibana_migrator_test_kit'; -import { baselineDocuments, baselineTypes } from './active_delete.fixtures'; +import { AggregationsAggregate, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import { TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server'; +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { + readLog, + clearLog, + nextMinor, + createBaseline, + currentVersion, + defaultKibanaIndex, + startElasticsearch, + getCompatibleMappingsMigrator, + getIdenticalMappingsMigrator, + getIncompatibleMappingsMigrator, + getNonDeprecatedMappingsMigrator, +} from '../kibana_migrator_test_kit'; import { delay } from '../test_utils'; -const kibanaIndex = '.kibana_migrator_tests'; -export const logFilePath = Path.join(__dirname, 'active_delete.test.log'); -const currentVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; -const nextMinor = new SemVer(currentVersion).inc('minor').format(); - describe('when upgrading to a new stack version', () => { let esServer: TestElasticsearchUtils['es']; let esClient: ElasticsearchClient; - const startElasticsearch = async () => { - const { startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - }, - }, - }); - return await startES(); - }; - - const createBaseline = async () => { - const { client, runMigrations, savedObjectsRepository } = await getKibanaMigratorTestKit({ - kibanaIndex, - types: baselineTypes, - }); - - await runMigrations(); - - await savedObjectsRepository.bulkCreate(baselineDocuments, { - refresh: 'wait_for', - }); - - return client; - }; - beforeAll(async () => { esServer = await startElasticsearch(); }); @@ -65,92 +37,66 @@ describe('when upgrading to a new stack version', () => { await delay(10); }); - describe('and the mappings match (diffMappings() === false)', () => { + describe('if the mappings match (diffMappings() === false)', () => { describe('and discardUnknownObjects = true', () => { let indexContents: SearchResponse<{ type: string }, Record>; beforeAll(async () => { esClient = await createBaseline(); - await fs.unlink(logFilePath).catch(() => {}); + await clearLog(); // remove the 'deprecated' type from the mappings, so that it is considered unknown - const types = baselineTypes.filter((type) => type.name !== 'deprecated'); - const { client, runMigrations } = await getKibanaMigratorTestKit({ + const { client, runMigrations } = await getNonDeprecatedMappingsMigrator({ settings: { migrations: { discardUnknownObjects: nextMinor, }, }, - kibanaIndex, - types, - kibanaVersion: nextMinor, - logFilePath, }); await runMigrations(); - indexContents = await client.search({ index: kibanaIndex, size: 100 }); + indexContents = await client.search({ index: defaultKibanaIndex, size: 100 }); }); afterAll(async () => { - await esClient?.indices.delete({ index: `${kibanaIndex}_${currentVersion}_001` }); + await esClient?.indices.delete({ index: `${defaultKibanaIndex}_${currentVersion}_001` }); }); it('the migrator is skipping reindex operation and executing CLEANUP_UNKNOWN_AND_EXCLUDED step', async () => { - const logs = await fs.readFile(logFilePath, 'utf-8'); - expect(logs).toMatch('[.kibana_migrator_tests] INIT -> WAIT_FOR_YELLOW_SOURCE'); - expect(logs).toMatch( - '[.kibana_migrator_tests] WAIT_FOR_YELLOW_SOURCE -> CLEANUP_UNKNOWN_AND_EXCLUDED' - ); + const logs = await readLog(); + expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE'); + expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> CLEANUP_UNKNOWN_AND_EXCLUDED'); // we gotta inform that we are deleting unknown documents too (discardUnknownObjects: true) expect(logs).toMatch( - '[.kibana_migrator_tests] Kibana has been configured to discard unknown documents for this migration.' + 'Kibana has been configured to discard unknown documents for this migration.' ); - expect(logs).toMatch( 'Therefore, the following documents with unknown types will not be taken into account and they will not be available after the migration:' ); expect(logs).toMatch( - '[.kibana_migrator_tests] CLEANUP_UNKNOWN_AND_EXCLUDED -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK' - ); - expect(logs).toMatch( - '[.kibana_migrator_tests] CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> PREPARE_COMPATIBLE_MIGRATION' - ); - expect(logs).toMatch( - '[.kibana_migrator_tests] PREPARE_COMPATIBLE_MIGRATION -> REFRESH_TARGET' - ); - expect(logs).toMatch( - '[.kibana_migrator_tests] REFRESH_TARGET -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT' + 'CLEANUP_UNKNOWN_AND_EXCLUDED -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK' ); expect(logs).toMatch( - '[.kibana_migrator_tests] CHECK_TARGET_MAPPINGS -> CHECK_VERSION_INDEX_READY_ACTIONS' + 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> PREPARE_COMPATIBLE_MIGRATION' ); - expect(logs).toMatch('[.kibana_migrator_tests] CHECK_VERSION_INDEX_READY_ACTIONS -> DONE'); + expect(logs).toMatch('PREPARE_COMPATIBLE_MIGRATION -> REFRESH_TARGET'); + expect(logs).toMatch('REFRESH_TARGET -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT'); + expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> CHECK_VERSION_INDEX_READY_ACTIONS'); + expect(logs).toMatch('CHECK_VERSION_INDEX_READY_ACTIONS -> DONE'); }); describe('CLEANUP_UNKNOWN_AND_EXCLUDED', () => { it('preserves documents with known types', async () => { - const basicDocumentCount = indexContents.hits.hits.filter( - (result) => result._source?.type === 'basic' - ).length; - - expect(basicDocumentCount).toEqual(3); + expect(countResultsByType(indexContents, 'basic')).toEqual(3); }); it('deletes documents with unknown types', async () => { - const deprecatedDocumentCount = indexContents.hits.hits.filter( - (result) => result._source?.type === 'deprecated' - ).length; - - expect(deprecatedDocumentCount).toEqual(0); + expect(countResultsByType(indexContents, 'deprecated')).toEqual(0); }); it('deletes documents that belong to REMOVED_TYPES', async () => { - const serverDocumentCount = indexContents.hits.hits.filter( - (result) => result._source?.type === 'server' - ).length; - - expect(serverDocumentCount).toEqual(0); + expect(countResultsByType(indexContents, 'server')).toEqual(0); }); it("deletes documents that have been excludeOnUpgrade'd via plugin hook", async () => { @@ -186,21 +132,15 @@ describe('when upgrading to a new stack version', () => { esClient = await createBaseline(); }); afterAll(async () => { - await esClient?.indices.delete({ index: `${kibanaIndex}_${currentVersion}_001` }); + await esClient?.indices.delete({ index: `${defaultKibanaIndex}_${currentVersion}_001` }); }); beforeEach(async () => { - await fs.unlink(logFilePath).catch(() => {}); + await clearLog(); }); it('fails if unknown documents exist', async () => { - // remove the 'deprecated' type from the mappings, so that SO of this type are considered unknown - const types = baselineTypes.filter((type) => type.name !== 'deprecated'); - const { runMigrations } = await getKibanaMigratorTestKit({ - kibanaIndex, - types, - kibanaVersion: nextMinor, - logFilePath, - }); + // remove the 'deprecated' type from the mappings, so that it is considered unknown + const { runMigrations } = await getNonDeprecatedMappingsMigrator(); try { await runMigrations(); @@ -215,121 +155,237 @@ describe('when upgrading to a new stack version', () => { expect(errorMessage).toMatch(/deprecated:.*\(type: "deprecated"\)/); } - const logs = await fs.readFile(logFilePath, 'utf-8'); - expect(logs).toMatch('[.kibana_migrator_tests] INIT -> WAIT_FOR_YELLOW_SOURCE'); + const logs = await readLog(); + expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE.'); + expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> CLEANUP_UNKNOWN_AND_EXCLUDED.'); + expect(logs).toMatch('CLEANUP_UNKNOWN_AND_EXCLUDED -> FATAL.'); + }); + + it('proceeds if there are no unknown documents', async () => { + const { client, runMigrations } = await getIdenticalMappingsMigrator(); + + await runMigrations(); + + const logs = await readLog(); + expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE.'); + expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> CLEANUP_UNKNOWN_AND_EXCLUDED.'); + expect(logs).toMatch( + 'CLEANUP_UNKNOWN_AND_EXCLUDED -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK.' + ); expect(logs).toMatch( - '[.kibana_migrator_tests] WAIT_FOR_YELLOW_SOURCE -> CLEANUP_UNKNOWN_AND_EXCLUDED' + 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> PREPARE_COMPATIBLE_MIGRATION.' ); - expect(logs).toMatch('[.kibana_migrator_tests] CLEANUP_UNKNOWN_AND_EXCLUDED -> FATAL'); + expect(logs).toMatch('PREPARE_COMPATIBLE_MIGRATION -> REFRESH_TARGET.'); + expect(logs).toMatch('REFRESH_TARGET -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT.'); + expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> CHECK_VERSION_INDEX_READY_ACTIONS.'); + expect(logs).toMatch('CHECK_VERSION_INDEX_READY_ACTIONS -> DONE.'); + + const indexContents = await client.search({ index: defaultKibanaIndex, size: 100 }); + expect(indexContents.hits.hits.length).toEqual(8); }); + }); + }); - it('proceeds if there are no unknown documents', async () => { - const { client, runMigrations } = await getKibanaMigratorTestKit({ - kibanaIndex, - types: baselineTypes, - kibanaVersion: nextMinor, - logFilePath, + describe('if the mappings are compatible', () => { + describe('and discardUnknownObjects = true', () => { + let indexContents: SearchResponse<{ type: string }, Record>; + + beforeAll(async () => { + esClient = await createBaseline(); + + await clearLog(); + const { client, runMigrations } = await getCompatibleMappingsMigrator({ + filterDeprecated: true, // remove the 'deprecated' type from the mappings, so that it is considered unknown + settings: { + migrations: { + discardUnknownObjects: nextMinor, + }, + }, }); await runMigrations(); - const logs = await fs.readFile(logFilePath, 'utf-8'); - expect(logs).toMatch('[.kibana_migrator_tests] INIT -> WAIT_FOR_YELLOW_SOURCE'); + indexContents = await client.search({ index: defaultKibanaIndex, size: 100 }); + }); + + afterAll(async () => { + await esClient?.indices.delete({ index: `${defaultKibanaIndex}_${currentVersion}_001` }); + }); + + it('the migrator is skipping reindex operation and executing CLEANUP_UNKNOWN_AND_EXCLUDED step', async () => { + const logs = await readLog(); + + expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE.'); + expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS.'); + // this step is run only if mappings are compatible but NOT equal + expect(logs).toMatch('UPDATE_SOURCE_MAPPINGS -> CLEANUP_UNKNOWN_AND_EXCLUDED.'); + // we gotta inform that we are deleting unknown documents too (discardUnknownObjects: true), expect(logs).toMatch( - '[.kibana_migrator_tests] WAIT_FOR_YELLOW_SOURCE -> CLEANUP_UNKNOWN_AND_EXCLUDED' + 'Kibana has been configured to discard unknown documents for this migration.' ); expect(logs).toMatch( - '[.kibana_migrator_tests] CLEANUP_UNKNOWN_AND_EXCLUDED -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK' + 'Therefore, the following documents with unknown types will not be taken into account and they will not be available after the migration:' ); expect(logs).toMatch( - '[.kibana_migrator_tests] CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> PREPARE_COMPATIBLE_MIGRATION' + 'CLEANUP_UNKNOWN_AND_EXCLUDED -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK.' ); expect(logs).toMatch( - '[.kibana_migrator_tests] PREPARE_COMPATIBLE_MIGRATION -> REFRESH_TARGET' + 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> PREPARE_COMPATIBLE_MIGRATION.' ); + expect(logs).toMatch('PREPARE_COMPATIBLE_MIGRATION -> REFRESH_TARGET.'); + expect(logs).toMatch('REFRESH_TARGET -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT.'); + expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS.'); + expect(logs).toMatch('UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS.'); + expect(logs).toMatch('CHECK_VERSION_INDEX_READY_ACTIONS -> DONE.'); + }); + + describe('CLEANUP_UNKNOWN_AND_EXCLUDED', () => { + it('preserves documents with known types', async () => { + expect(countResultsByType(indexContents, 'basic')).toEqual(3); + }); + + it('deletes documents with unknown types', async () => { + expect(countResultsByType(indexContents, 'deprecated')).toEqual(0); + }); + + it('deletes documents that belong to REMOVED_TYPES', async () => { + expect(countResultsByType(indexContents, 'server')).toEqual(0); + }); + + it("deletes documents that have been excludeOnUpgrade'd via plugin hook", async () => { + const complexDocuments = indexContents.hits.hits.filter( + (result) => result._source?.type === 'complex' + ); + + expect(complexDocuments.length).toEqual(2); + expect(complexDocuments[0]._source).toEqual( + expect.objectContaining({ + complex: { + name: 'complex-baz', + value: 2, + }, + type: 'complex', + }) + ); + expect(complexDocuments[1]._source).toEqual( + expect.objectContaining({ + complex: { + name: 'complex-lipsum', + value: 3, + }, + type: 'complex', + }) + ); + }); + }); + }); + + describe('and discardUnknownObjects = false', () => { + beforeAll(async () => { + esClient = await createBaseline(); + }); + afterAll(async () => { + await esClient?.indices.delete({ index: `${defaultKibanaIndex}_${currentVersion}_001` }); + }); + beforeEach(async () => { + await clearLog(); + }); + + it('fails if unknown documents exist', async () => { + const { runMigrations } = await getCompatibleMappingsMigrator({ + filterDeprecated: true, // remove the 'deprecated' type from the mappings, so that it is considered unknown + }); + + try { + await runMigrations(); + } catch (err) { + const errorMessage = err.message; + expect(errorMessage).toMatch( + 'Unable to complete saved object migrations for the [.kibana_migrator_tests] index: Migration failed because some documents were found which use unknown saved object types:' + ); + expect(errorMessage).toMatch( + 'To proceed with the migration you can configure Kibana to discard unknown saved objects for this migration.' + ); + expect(errorMessage).toMatch(/deprecated:.*\(type: "deprecated"\)/); + } + + const logs = await readLog(); + expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE.'); + expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS.'); // this step is run only if mappings are compatible but NOT equal + expect(logs).toMatch('UPDATE_SOURCE_MAPPINGS -> CLEANUP_UNKNOWN_AND_EXCLUDED.'); + expect(logs).toMatch('CLEANUP_UNKNOWN_AND_EXCLUDED -> FATAL.'); + }); + + it('proceeds if there are no unknown documents', async () => { + const { client, runMigrations } = await getCompatibleMappingsMigrator(); + + await runMigrations(); + + const logs = await readLog(); + expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE.'); + expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS.'); + expect(logs).toMatch('UPDATE_SOURCE_MAPPINGS -> CLEANUP_UNKNOWN_AND_EXCLUDED.'); expect(logs).toMatch( - '[.kibana_migrator_tests] REFRESH_TARGET -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT' + 'CLEANUP_UNKNOWN_AND_EXCLUDED -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK.' ); expect(logs).toMatch( - '[.kibana_migrator_tests] CHECK_TARGET_MAPPINGS -> CHECK_VERSION_INDEX_READY_ACTIONS' + 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> PREPARE_COMPATIBLE_MIGRATION.' ); - expect(logs).toMatch('[.kibana_migrator_tests] CHECK_VERSION_INDEX_READY_ACTIONS -> DONE'); + expect(logs).toMatch('PREPARE_COMPATIBLE_MIGRATION -> REFRESH_TARGET.'); + expect(logs).toMatch('REFRESH_TARGET -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT.'); + expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS.'); + expect(logs).toMatch('UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS.'); + expect(logs).toMatch('CHECK_VERSION_INDEX_READY_ACTIONS -> DONE.'); - const indexContents = await client.search({ index: kibanaIndex, size: 100 }); + const indexContents = await client.search({ index: defaultKibanaIndex, size: 100 }); expect(indexContents.hits.hits.length).toEqual(8); }); }); }); - describe('and the mappings do NOT match (diffMappings() === true)', () => { + describe('if the mappings do NOT match (diffMappings() === true) and they are NOT compatible', () => { beforeAll(async () => { esClient = await createBaseline(); }); afterAll(async () => { - await esClient?.indices.delete({ index: `${kibanaIndex}_${currentVersion}_001` }); + await esClient?.indices.delete({ index: `${defaultKibanaIndex}_${currentVersion}_001` }); }); beforeEach(async () => { - await fs.unlink(logFilePath).catch(() => {}); + await clearLog(); }); it('the migrator does not skip reindexing', async () => { - const incompatibleTypes: Array> = baselineTypes.map((type) => { - if (type.name === 'complex') { - return { - ...type, - mappings: { - properties: { - name: { type: 'keyword' }, // text => keyword - value: { type: 'long' }, // integer => long - }, - }, - }; - } else { - return type; - } - }); - - const { client, runMigrations } = await getKibanaMigratorTestKit({ - kibanaIndex, - types: incompatibleTypes, - kibanaVersion: nextMinor, - logFilePath, - }); + const { client, runMigrations } = await getIncompatibleMappingsMigrator(); await runMigrations(); - const logs = await fs.readFile(logFilePath, 'utf-8'); - expect(logs).toMatch('[.kibana_migrator_tests] INIT -> WAIT_FOR_YELLOW_SOURCE'); - expect(logs).toMatch( - '[.kibana_migrator_tests] WAIT_FOR_YELLOW_SOURCE -> CHECK_UNKNOWN_DOCUMENTS.' - ); - expect(logs).toMatch( - '[.kibana_migrator_tests] CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK.' - ); - expect(logs).toMatch( - '[.kibana_migrator_tests] CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS.' - ); - expect(logs).toMatch( - '[.kibana_migrator_tests] UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS.' - ); - expect(logs).toMatch( - '[.kibana_migrator_tests] CHECK_VERSION_INDEX_READY_ACTIONS -> MARK_VERSION_INDEX_READY.' - ); - expect(logs).toMatch('[.kibana_migrator_tests] MARK_VERSION_INDEX_READY -> DONE'); + const logs = await readLog(); + expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE'); + expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS.'); + expect(logs).toMatch('UPDATE_SOURCE_MAPPINGS -> CHECK_UNKNOWN_DOCUMENTS.'); + expect(logs).toMatch('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK.'); + expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS.'); + expect(logs).toMatch('UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS.'); + expect(logs).toMatch('CHECK_VERSION_INDEX_READY_ACTIONS -> MARK_VERSION_INDEX_READY.'); + expect(logs).toMatch('MARK_VERSION_INDEX_READY -> DONE'); const indexContents: SearchResponse< { type: string }, Record - > = await client.search({ index: kibanaIndex, size: 100 }); + > = await client.search({ index: defaultKibanaIndex, size: 100 }); expect(indexContents.hits.hits.length).toEqual(8); // we're removing a couple of 'complex' (value < = 1) // double-check that the deprecated documents have not been deleted - const deprecatedDocumentCount = indexContents.hits.hits.filter( - (result) => result._source?.type === 'deprecated' - ).length; - expect(deprecatedDocumentCount).toEqual(3); + expect(countResultsByType(indexContents, 'deprecated')).toEqual(3); }); }); }); + +const countResultsByType = ( + indexContents: SearchResponse<{ type: string }, Record>, + type: string +): number => { + return indexContents.hits.hits.filter((result) => result._source?.type === type).length; +}; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/multiple_es_nodes.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/multiple_es_nodes.test.ts index 5cb4028eba2ca..1240f5873e3a0 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/multiple_es_nodes.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/multiple_es_nodes.test.ts @@ -97,7 +97,7 @@ function createRoot({ logFileName, hosts }: RootConfig) { describe('migration v2', () => { let esServer: TestElasticsearchUtils; let root: Root; - const migratedIndex = `.kibana_${pkg.version}_001`; + const migratedIndexAlias = `.kibana_${pkg.version}`; beforeAll(async () => { await removeLogFile(); @@ -186,7 +186,7 @@ describe('migration v2', () => { await root.start(); const esClient = esServer.es.getClient(); - const migratedFooDocs = await fetchDocs(esClient, migratedIndex, 'foo'); + const migratedFooDocs = await fetchDocs(esClient, migratedIndexAlias, 'foo'); expect(migratedFooDocs.length).toBe(2500); migratedFooDocs.forEach((doc, i) => { expect(doc.id).toBe(`foo:${i}`); @@ -194,7 +194,7 @@ describe('migration v2', () => { expect(doc.migrationVersion.foo).toBe('7.14.0'); }); - const migratedBarDocs = await fetchDocs(esClient, migratedIndex, 'bar'); + const migratedBarDocs = await fetchDocs(esClient, migratedIndexAlias, 'bar'); expect(migratedBarDocs.length).toBe(2500); migratedBarDocs.forEach((doc, i) => { expect(doc.id).toBe(`bar:${i}`); diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/rewriting_id.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/rewriting_id.test.ts index ae90b81482f4c..88193063d5526 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/rewriting_id.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/rewriting_id.test.ts @@ -113,7 +113,7 @@ describe('migration v2', () => { }); it('rewrites id deterministically for SO with namespaceType: "multiple" and "multiple-isolated"', async () => { - const migratedIndex = `.kibana_${pkg.version}_001`; + const migratedIndexAlias = `.kibana_${pkg.version}`; const { startES } = createTestServers({ adjustTimeout: (t: number) => jest.setTimeout(t), settings: { @@ -172,7 +172,7 @@ describe('migration v2', () => { const coreStart = await root.start(); const esClient = coreStart.elasticsearch.client.asInternalUser; - const migratedDocs = await fetchDocs(esClient, migratedIndex); + const migratedDocs = await fetchDocs(esClient, migratedIndexAlias); // each newly converted multi-namespace object in a non-default space has its ID deterministically regenerated, and a legacy-url-alias // object is created which links the old ID to the new ID diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/skip_reindex.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/skip_reindex.test.ts index aa5d1c0c06eb4..5354a958e8cb7 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/skip_reindex.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/skip_reindex.test.ts @@ -5,121 +5,137 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import Path from 'path'; -import fs from 'fs/promises'; -import { Env } from '@kbn/config'; -import { getEnvOptions } from '@kbn/config-mocks'; -import { REPO_ROOT } from '@kbn/repo-info'; -import type { Root } from '@kbn/core-root-server-internal'; + +import { type TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server'; +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { IKibanaMigrator } from '@kbn/core-saved-objects-base-server-internal'; import { - createRootWithCorePlugins, - createTestServers, - type TestElasticsearchUtils, -} from '@kbn/core-test-helpers-kbn-server'; + readLog, + clearLog, + createBaseline, + currentVersion, + defaultKibanaIndex, + getCompatibleMappingsMigrator, + getIdenticalMappingsMigrator, + getIncompatibleMappingsMigrator, + startElasticsearch, +} from '../kibana_migrator_test_kit'; import { delay } from '../test_utils'; -import { SemVer } from 'semver'; - -const logFilePath = Path.join(__dirname, 'skip_reindex.log'); -describe('skip reindexing', () => { - const currentVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; +describe('when migrating to a new version', () => { let esServer: TestElasticsearchUtils['es']; - let root: Root; + let esClient: ElasticsearchClient; + let migrator: IKibanaMigrator; - afterEach(async () => { - await root?.shutdown(); - await esServer?.stop(); - await delay(10); + beforeAll(async () => { + esServer = await startElasticsearch(); + }); + + beforeEach(async () => { + esClient = await createBaseline(); + await clearLog(); }); - it('when migrating to a new version, but mappings remain the same', async () => { - let logs: string; - const { startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - }, - }, + describe('and the mappings remain the same', () => { + it('the migrator skips reindexing', async () => { + // we run the migrator with the same identic baseline types + migrator = (await getIdenticalMappingsMigrator()).migrator; + migrator.prepareMigrations(); + await migrator.runMigrations(); + + const logs = await readLog(); + expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE.'); + expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> CLEANUP_UNKNOWN_AND_EXCLUDED.'); + expect(logs).toMatch( + 'CLEANUP_UNKNOWN_AND_EXCLUDED -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK.' + ); + expect(logs).toMatch( + 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> PREPARE_COMPATIBLE_MIGRATION.' + ); + expect(logs).toMatch('PREPARE_COMPATIBLE_MIGRATION -> REFRESH_TARGET.'); + expect(logs).toMatch('REFRESH_TARGET -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT.'); + expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> CHECK_VERSION_INDEX_READY_ACTIONS.'); + expect(logs).toMatch('CHECK_VERSION_INDEX_READY_ACTIONS -> DONE.'); + + expect(logs).not.toMatch('CREATE_NEW_TARGET'); + expect(logs).not.toMatch('CHECK_UNKNOWN_DOCUMENTS'); + expect(logs).not.toMatch('REINDEX'); + expect(logs).not.toMatch('UPDATE_TARGET_MAPPINGS'); }); - esServer = await startES(); - root = createRoot(); + }); - // Run initial migrations - await root.preboot(); - await root.setup(); - await root.start(); + describe("and the mappings' changes are still compatible", () => { + it('the migrator skips reindexing', async () => { + // we run the migrator with altered, compatible mappings + migrator = (await getCompatibleMappingsMigrator()).migrator; + migrator.prepareMigrations(); + await migrator.runMigrations(); - // stop Kibana and remove logs - await root.shutdown(); - await delay(10); - await fs.unlink(logFilePath).catch(() => {}); - - const nextPatch = new SemVer(currentVersion).inc('patch').format(); - root = createRoot(nextPatch); - await root.preboot(); - await root.setup(); - await root.start(); - - logs = await fs.readFile(logFilePath, 'utf-8'); - - expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE'); - expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> CLEANUP_UNKNOWN_AND_EXCLUDED'); - expect(logs).toMatch( - 'CLEANUP_UNKNOWN_AND_EXCLUDED -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK' - ); - expect(logs).toMatch( - 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> PREPARE_COMPATIBLE_MIGRATION' - ); - expect(logs).toMatch('PREPARE_COMPATIBLE_MIGRATION -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT'); - expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> CHECK_VERSION_INDEX_READY_ACTIONS'); - expect(logs).toMatch('CHECK_VERSION_INDEX_READY_ACTIONS -> DONE'); + const logs = await readLog(); + expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE.'); + expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS.'); + expect(logs).toMatch('UPDATE_SOURCE_MAPPINGS -> CLEANUP_UNKNOWN_AND_EXCLUDED.'); + expect(logs).toMatch( + 'CLEANUP_UNKNOWN_AND_EXCLUDED -> CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK.' + ); + expect(logs).toMatch( + 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK -> PREPARE_COMPATIBLE_MIGRATION.' + ); + expect(logs).toMatch('PREPARE_COMPATIBLE_MIGRATION -> REFRESH_TARGET.'); + expect(logs).toMatch('REFRESH_TARGET -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT.'); + expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS.'); + expect(logs).toMatch('UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS.'); + expect(logs).toMatch('CHECK_VERSION_INDEX_READY_ACTIONS -> DONE.'); - expect(logs).not.toMatch('CREATE_NEW_TARGET'); - expect(logs).not.toMatch('CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS'); + expect(logs).not.toMatch('CREATE_NEW_TARGET'); + expect(logs).not.toMatch('CHECK_UNKNOWN_DOCUMENTS'); + expect(logs).not.toMatch('REINDEX'); + }); + }); - // We restart Kibana again after doing a "compatible migration" to ensure that - // the next time state is loaded everything still works as expected. - // For instance, we might see something like: - // Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Unexpected Elasticsearch ResponseError: statusCode: 404, method: POST, url: /.kibana_8.7.1_001/_pit?keep_alive=10m error: [index_not_found_exception]: no such index [.kibana_8.7.1_001] - await root.shutdown(); - await delay(10); - await fs.unlink(logFilePath).catch(() => {}); + describe("and the mappings' changes are NOT compatible", () => { + it('the migrator reindexes documents to a new index', async () => { + // we run the migrator with altered, compatible mappings + migrator = (await getIncompatibleMappingsMigrator()).migrator; + migrator.prepareMigrations(); + await migrator.runMigrations(); - root = createRoot(nextPatch); - await root.preboot(); - await root.setup(); - await root.start(); + const logs = await readLog(); + expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE.'); + expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS.'); + expect(logs).toMatch('UPDATE_SOURCE_MAPPINGS -> CHECK_UNKNOWN_DOCUMENTS.'); + expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS.'); + expect(logs).toMatch('UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS.'); + expect(logs).toMatch('CHECK_VERSION_INDEX_READY_ACTIONS -> MARK_VERSION_INDEX_READY.'); + expect(logs).toMatch('MARK_VERSION_INDEX_READY -> DONE.'); - logs = await fs.readFile(logFilePath, 'utf-8'); - expect(logs).toMatch('INIT -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT'); - expect(logs).not.toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE'); + expect(logs).not.toMatch('CREATE_NEW_TARGET'); + expect(logs).not.toMatch('CLEANUP_UNKNOWN_AND_EXCLUDED'); + expect(logs).not.toMatch('PREPARE_COMPATIBLE_MIGRATION'); + }); + }); + + afterEach(async () => { + // we run the migrator again to ensure that the next time state is loaded everything still works as expected + await clearLog(); + await migrator.runMigrations({ rerun: true }); + + const logs = await readLog(); + expect(logs).toMatch('INIT -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT.'); + expect(logs).toMatch('CHECK_VERSION_INDEX_READY_ACTIONS -> DONE.'); + + expect(logs).not.toMatch('WAIT_FOR_YELLOW_SOURCE'); + expect(logs).not.toMatch('CLEANUP_UNKNOWN_AND_EXCLUCED'); + expect(logs).not.toMatch('CREATE_NEW_TARGET'); + expect(logs).not.toMatch('PREPARE_COMPATIBLE_MIGRATION'); + expect(logs).not.toMatch('UPDATE_TARGET_MAPPINGS'); + + // clear the system index for next test + await esClient?.indices.delete({ index: `${defaultKibanaIndex}_${currentVersion}_001` }); }); -}); -function createRoot(kibanaVersion?: string): Root { - return createRootWithCorePlugins( - { - logging: { - appenders: { - file: { - type: 'file', - fileName: logFilePath, - layout: { - type: 'json', - }, - }, - }, - loggers: [ - { - name: 'root', - level: 'info', - appenders: ['file'], - }, - ], - }, - }, - { oss: true }, - kibanaVersion - ); -} + afterAll(async () => { + await esServer?.stop(); + await delay(10); + }); +}); diff --git a/src/core/server/integration_tests/saved_objects/migrations/kibana_migrator_test_kit.fixtures.ts b/src/core/server/integration_tests/saved_objects/migrations/kibana_migrator_test_kit.fixtures.ts new file mode 100644 index 0000000000000..7d605cf116341 --- /dev/null +++ b/src/core/server/integration_tests/saved_objects/migrations/kibana_migrator_test_kit.fixtures.ts @@ -0,0 +1,82 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectsBulkCreateObject } from '@kbn/core-saved-objects-api-server'; +import type { SavedObjectsType } from '@kbn/core-saved-objects-server'; + +const defaultType: SavedObjectsType = { + name: 'defaultType', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + name: { type: 'keyword' }, + }, + }, + migrations: {}, +}; + +export const baselineTypes: Array> = [ + { + ...defaultType, + name: 'server', + }, + { + ...defaultType, + name: 'basic', + }, + { + ...defaultType, + name: 'deprecated', + }, + { + ...defaultType, + name: 'complex', + mappings: { + properties: { + name: { type: 'text' }, + value: { type: 'integer' }, + }, + }, + excludeOnUpgrade: () => { + return { + bool: { + must: [{ term: { type: 'complex' } }, { range: { 'complex.value': { lte: 1 } } }], + }, + }; + }, + }, +]; + +export const baselineDocuments: SavedObjectsBulkCreateObject[] = [ + ...['server-foo', 'server-bar', 'server-baz'].map((name) => ({ + type: 'server', + attributes: { + name, + }, + })), + ...['basic-foo', 'basic-bar', 'basic-baz'].map((name) => ({ + type: 'basic', + attributes: { + name, + }, + })), + ...['deprecated-foo', 'deprecated-bar', 'deprecated-baz'].map((name) => ({ + type: 'deprecated', + attributes: { + name, + }, + })), + ...['complex-foo', 'complex-bar', 'complex-baz', 'complex-lipsum'].map((name, index) => ({ + type: 'complex', + attributes: { + name, + value: index, + }, + })), +]; diff --git a/src/core/server/integration_tests/saved_objects/migrations/kibana_migrator_test_kit.ts b/src/core/server/integration_tests/saved_objects/migrations/kibana_migrator_test_kit.ts index df3cce7dbdca6..f6760aa3264fc 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/kibana_migrator_test_kit.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/kibana_migrator_test_kit.ts @@ -7,6 +7,9 @@ */ import Path from 'path'; +import fs from 'fs/promises'; +import { SemVer } from 'semver'; + import { defaultsDeep } from 'lodash'; import { BehaviorSubject, firstValueFrom, map } from 'rxjs'; import { ConfigService, Env } from '@kbn/config'; @@ -19,8 +22,8 @@ import { type SavedObjectsConfigType, type SavedObjectsMigrationConfigType, SavedObjectTypeRegistry, - IKibanaMigrator, - MigrationResult, + type IKibanaMigrator, + type MigrationResult, } from '@kbn/core-saved-objects-base-server-internal'; import { SavedObjectsRepository } from '@kbn/core-saved-objects-api-server-internal'; import { @@ -32,20 +35,23 @@ import { type LoggingConfigType, LoggingSystem } from '@kbn/core-logging-server- import type { ISavedObjectTypeRegistry, SavedObjectsType } from '@kbn/core-saved-objects-server'; import { esTestConfig, kibanaServerTestUser } from '@kbn/test'; -import { LoggerFactory } from '@kbn/logging'; +import type { LoggerFactory } from '@kbn/logging'; +import { createTestServers } from '@kbn/core-test-helpers-kbn-server'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { registerServiceConfig } from '@kbn/core-root-server-internal'; -import { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server'; +import type { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server'; import { getDocLinks, getDocLinksMeta } from '@kbn/doc-links'; -import { DocLinksServiceStart } from '@kbn/core-doc-links-server'; -import { createTestServers } from '@kbn/core-test-helpers-kbn-server'; +import type { DocLinksServiceStart } from '@kbn/core-doc-links-server'; +import { baselineDocuments, baselineTypes } from './kibana_migrator_test_kit.fixtures'; export const defaultLogFilePath = Path.join(__dirname, 'kibana_migrator_test_kit.log'); const env = Env.createDefault(REPO_ROOT, getEnvOptions()); // Extract current stack version from Env, to use as a default -const currentVersion = env.packageInfo.version; -const currentBranch = env.packageInfo.branch; +export const currentVersion = env.packageInfo.version; +export const nextMinor = new SemVer(currentVersion).inc('minor').format(); +export const currentBranch = env.packageInfo.branch; +export const defaultKibanaIndex = '.kibana_migrator_tests'; export interface GetEsClientParams { settings?: Record; @@ -76,7 +82,7 @@ export const startElasticsearch = async ({ }: { basePath?: string; dataArchive?: string; -}) => { +} = {}) => { const { startES } = createTestServers({ adjustTimeout: (t: number) => jest.setTimeout(t), settings: { @@ -109,7 +115,7 @@ export const getEsClient = async ({ export const getKibanaMigratorTestKit = async ({ settings = {}, - kibanaIndex = '.kibana', + kibanaIndex = defaultKibanaIndex, kibanaVersion = currentVersion, kibanaBranch = currentBranch, types = [], @@ -272,3 +278,113 @@ const registerTypes = ( ) => { (types || []).forEach((type) => typeRegistry.registerType(type)); }; + +export const createBaseline = async () => { + const { client, migrator, savedObjectsRepository } = await getKibanaMigratorTestKit({ + kibanaIndex: defaultKibanaIndex, + types: baselineTypes, + }); + + migrator.prepareMigrations(); + await migrator.runMigrations(); + + await savedObjectsRepository.bulkCreate(baselineDocuments, { + refresh: 'wait_for', + }); + + return client; +}; + +interface GetMutatedMigratorParams { + kibanaVersion?: string; + settings?: Record; +} + +export const getIdenticalMappingsMigrator = async ({ + kibanaVersion = nextMinor, + settings = {}, +}: GetMutatedMigratorParams = {}) => { + return await getKibanaMigratorTestKit({ + types: baselineTypes, + kibanaVersion, + settings, + }); +}; + +export const getNonDeprecatedMappingsMigrator = async ({ + kibanaVersion = nextMinor, + settings = {}, +}: GetMutatedMigratorParams = {}) => { + return await getKibanaMigratorTestKit({ + types: baselineTypes.filter((type) => type.name !== 'deprecated'), + kibanaVersion, + settings, + }); +}; + +export const getCompatibleMappingsMigrator = async ({ + filterDeprecated = false, + kibanaVersion = nextMinor, + settings = {}, +}: GetMutatedMigratorParams & { filterDeprecated?: boolean } = {}) => { + const types = baselineTypes + .filter((type) => !filterDeprecated || type.name !== 'deprecated') + .map((type) => { + if (type.name === 'complex') { + return { + ...type, + mappings: { + properties: { + name: { type: 'text' }, + value: { type: 'integer' }, + createdAt: { type: 'date' }, + }, + }, + }; + } else { + return type; + } + }); + + return await getKibanaMigratorTestKit({ + types, + kibanaVersion, + settings, + }); +}; + +export const getIncompatibleMappingsMigrator = async ({ + kibanaVersion = nextMinor, + settings = {}, +}: GetMutatedMigratorParams = {}) => { + const types = baselineTypes.map((type) => { + if (type.name === 'complex') { + return { + ...type, + mappings: { + properties: { + name: { type: 'keyword' }, + value: { type: 'long' }, + createdAt: { type: 'date' }, + }, + }, + }; + } else { + return type; + } + }); + + return await getKibanaMigratorTestKit({ + types, + kibanaVersion, + settings, + }); +}; + +export const readLog = async (logFilePath: string = defaultLogFilePath): Promise => { + return await fs.readFile(logFilePath, 'utf-8'); +}; + +export const clearLog = async (logFilePath: string = defaultLogFilePath): Promise => { + await fs.truncate(logFilePath).catch(() => {}); +};