From 19b94c8b9a02892d537654d2dfbbaa1cebb8a288 Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 13 Apr 2021 09:42:42 +0200 Subject: [PATCH 01/27] some typos --- .../migrations/core/elastic_index.ts | 37 ++++++++----------- .../migrationsv2/actions/index.ts | 2 +- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index 462425ff6e3e0..2f2a90a4759fa 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -14,7 +14,6 @@ import _ from 'lodash'; import { estypes } from '@elastic/elasticsearch'; import { MigrationEsClient } from './migration_es_client'; -import { CountResponse, SearchResponse } from '../../../elasticsearch'; import { IndexMapping } from '../../mappings'; import { SavedObjectsMigrationVersion } from '../../types'; import { AliasAction, RawDoc } from './call_cluster'; @@ -55,11 +54,11 @@ export async function fetchInfo(client: MigrationEsClient, index: string): Promi * Creates a reader function that serves up batches of documents from the index. We aren't using * an async generator, as that feature currently breaks Kibana's tooling. * - * @param {CallCluster} callCluster - The elastic search connection - * @param {string} - The index to be read from + * @param client - The elastic search connection + * @param index - The index to be read from * @param {opts} - * @prop {number} batchSize - The number of documents to read at a time - * @prop {string} scrollDuration - The scroll duration used for scrolling through the index + * @prop batchSize - The number of documents to read at a time + * @prop scrollDuration - The scroll duration used for scrolling through the index */ export function reader( client: MigrationEsClient, @@ -72,7 +71,7 @@ export function reader( // When migrating from the outdated index we use a read query which excludes // saved object types which are no longer used. These saved objects will // still be kept in the outdated index for backup purposes, but won't be - // availble in the upgraded index. + // available in the upgraded index. const EXCLUDE_UNUSED_TYPES = [ 'fleet-agent-events', // https://github.com/elastic/kibana/issues/91869 'tsvb-validation-telemetry', // https://github.com/elastic/kibana/issues/95617 @@ -88,11 +87,11 @@ export function reader( const nextBatch = () => scrollId !== undefined - ? client.scroll>({ + ? client.scroll({ scroll, scroll_id: scrollId, }) - : client.search>({ + : client.search({ body: { size: batchSize, query: excludeUnusedTypesQuery, @@ -120,10 +119,6 @@ export function reader( /** * Writes the specified documents to the index, throws an exception * if any of the documents fail to save. - * - * @param {CallCluster} callCluster - * @param {string} index - * @param {RawDoc[]} docs */ export async function write(client: MigrationEsClient, index: string, docs: RawDoc[]) { const { body } = await client.bulk({ @@ -161,9 +156,9 @@ export async function write(client: MigrationEsClient, index: string, docs: RawD * it performs the check *each* time it is called, rather than memoizing itself, * as this is used to determine if migrations are complete. * - * @param {CallCluster} callCluster - * @param {string} index - * @param {SavedObjectsMigrationVersion} migrationVersion - The latest versions of the migrations + * @param client - The connection to ElasticSearch + * @param index + * @param migrationVersion - The latest versions of the migrations */ export async function migrationsUpToDate( client: MigrationEsClient, @@ -184,7 +179,7 @@ export async function migrationsUpToDate( return true; } - const { body } = await client.count({ + const { body } = await client.count({ body: { query: { bool: { @@ -248,9 +243,9 @@ export async function createIndex( * is a concrete index. This function will reindex `alias` into a new index, delete the `alias` * index, and then create an alias `alias` that points to the new index. * - * @param {CallCluster} callCluster - The connection to ElasticSearch - * @param {FullIndexInfo} info - Information about the mappings and name of the new index - * @param {string} alias - The name of the index being converted to an alias + * @param client - The ElasticSearch connection + * @param info - Information about the mappings and name of the new index + * @param alias - The name of the index being converted to an alias */ export async function convertToAlias( client: MigrationEsClient, @@ -274,7 +269,7 @@ export async function convertToAlias( * alias, meaning that it will only point to one index at a time, so we * remove any other indices from the alias. * - * @param {CallCluster} callCluster + * @param {CallCluster} client * @param {string} index * @param {string} alias * @param {AliasAction[]} aliasActions - Optional actions to be added to the updateAliases call @@ -354,7 +349,7 @@ async function reindex( ) { // We poll instead of having the request wait for completion, as for large indices, // the request times out on the Elasticsearch side of things. We have a relatively tight - // polling interval, as the request is fairly efficent, and we don't + // polling interval, as the request is fairly efficient, and we don't // want to block index migrations for too long on this. const pollInterval = 250; const { body: reindexBody } = await client.reindex({ diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index 9d6afbd3b0d87..bc217fadca536 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -440,7 +440,7 @@ export const reindex = ( requireAlias: boolean, /* When reindexing we use a source query to exclude saved objects types which * are no longer used. These saved objects will still be kept in the outdated - * index for backup purposes, but won't be availble in the upgraded index. + * index for backup purposes, but won't be available in the upgraded index. */ unusedTypesToExclude: Option.Option ): TaskEither.TaskEither => () => { From 59a3fedbe6648af2f76aff5cbb8e2948c00d7cbe Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 15 Apr 2021 11:32:31 +0200 Subject: [PATCH 02/27] implement an alternative client-side migration algorithm required to enforce idempotent id generation for SO --- .../migrationsv2/actions/index.ts | 169 ++++++++++++++++-- .../saved_objects/migrationsv2/index.ts | 5 +- .../migrations_state_action_machine.ts | 6 +- .../saved_objects/migrationsv2/model.ts | 71 ++++---- .../saved_objects/migrationsv2/types.ts | 34 +++- 5 files changed, 234 insertions(+), 51 deletions(-) diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index bc217fadca536..ae4223288825d 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -17,7 +17,8 @@ import { flow } from 'fp-ts/lib/function'; import { QueryContainer } from '@elastic/eui/src/components/search_bar/query/ast_to_es_query_dsl'; import { ElasticsearchClient } from '../../../elasticsearch'; import { IndexMapping } from '../../mappings'; -import { SavedObjectsRawDoc, SavedObjectsRawDocSource } from '../../serialization'; +import type { SavedObjectsRawDoc, SavedObjectsRawDocSource } from '../../serialization'; +import type { TransformRawDocs } from '../types'; import { catchRetryableEsClientErrors, RetryableEsClientError, @@ -420,6 +421,136 @@ export const pickupUpdatedMappings = ( .catch(catchRetryableEsClientErrors); }; +/** @internal */ +export interface OpenPitResponse { + pitId: string; +} + +// how long ES should keep PIT alive +const pitKeepAlive = '10m'; +/* + * Creates a lightweight view of data when the request has been initiated. + * See https://www.elastic.co/guide/en/elasticsearch/reference/current/point-in-time-api.html + * */ +export const openPit = ( + client: ElasticsearchClient, + index: string +): TaskEither.TaskEither => () => { + return client + .openPointInTime({ + index, + keep_alive: pitKeepAlive, + }) + .then((response) => Either.right({ pitId: response.body.id })) + .catch(catchRetryableEsClientErrors); +}; + +/** @internal */ +export interface ReadWithPit { + outdatedDocuments: SavedObjectsRawDoc[]; + readonly lastHitSortValue: number[] | undefined; +} + +/* + * Requests documents from the index using PIT mechanism. + * Filter unusedTypesToExclude documents out to exclude them from being migrated. + * */ +export const readWithPit = ( + client: ElasticsearchClient, + pitId: string, + /* When reading we use a source query to exclude saved objects types which + * are no longer used. These saved objects will still be kept in the outdated + * index for backup purposes, but won't be available in the upgraded index. + */ + unusedTypesToExclude: Option.Option, + batchSize: number, + searchAfter?: number[] +): TaskEither.TaskEither => () => { + return client + .search({ + body: { + // Sort fields are required to use searchAfter, so we set some defaults here + sort: { + updated_at: { order: 'desc' }, + }, + pit: { id: pitId, keep_alive: pitKeepAlive }, + size: batchSize, + search_after: searchAfter, + // Exclude saved object types + query: Option.fold( + () => undefined, + (types) => ({ + bool: { + must_not: types.map((type) => ({ term: { type } })), + }, + }) + )(unusedTypesToExclude), + }, + }) + .then((response) => { + const hits = response.body.hits.hits; + + if (hits.length > 0) { + return Either.right({ + // @ts-expect-error @elastic/elasticsearch _source is optional + outdatedDocuments: hits as SavedObjectsRawDoc[], + lastHitSortValue: hits[hits.length - 1].sort as number[], + }); + } + + return Either.right({ + outdatedDocuments: [], + lastHitSortValue: undefined, + }); + }) + .catch(catchRetryableEsClientErrors); +}; + +/* + * Closes PIT. + * See https://www.elastic.co/guide/en/elasticsearch/reference/current/point-in-time-api.html + * */ +export const closePit = ( + client: ElasticsearchClient, + pitId: string +): TaskEither.TaskEither => () => { + return client + .closePointInTime({ + body: { id: pitId }, + }) + .then((response) => { + if (!response.body.succeeded) { + throw new Error(`Failed to close PointInTime with id: ${pitId}`); + } + return Either.right({}); + }) + .catch(catchRetryableEsClientErrors); +}; + +/* + * Transform outdated docs and write them to the index. + * */ +export const transformDocs = ( + client: ElasticsearchClient, + transformRawDocs: TransformRawDocs, + outdatedDocuments: SavedObjectsRawDoc[], + index: string, + refresh: estypes.Refresh +): TaskEither.TaskEither< + RetryableEsClientError | IndexNotFound | TargetIndexHadWriteBlock, + 'bulk_index_succeeded' +> => + pipe( + TaskEither.tryCatch( + () => transformRawDocs(outdatedDocuments), + (e) => { + throw e; + } + ), + TaskEither.chain((docs) => bulkOverwriteTransformedDocuments(client, index, docs, refresh)) + ); + +/** @internal */ export interface ReindexResponse { taskId: string; } @@ -494,10 +625,12 @@ interface WaitForReindexTaskFailure { readonly cause: { type: string; reason: string }; } +/** @internal */ export interface TargetIndexHadWriteBlock { type: 'target_index_had_write_block'; } +/** @internal */ export interface IncompatibleMappingException { type: 'incompatible_mapping_exception'; } @@ -610,14 +743,17 @@ export const waitForPickupUpdatedMappingsTask = flow( ) ); +/** @internal */ export interface AliasNotFound { type: 'alias_not_found_exception'; } +/** @internal */ export interface RemoveIndexNotAConcreteIndex { type: 'remove_index_not_a_concrete_index'; } +/** @internal */ export type AliasAction = | { remove_index: { index: string } } | { remove: { index: string; alias: string; must_exist: boolean } } @@ -684,11 +820,19 @@ export const updateAliases = ( .catch(catchRetryableEsClientErrors); }; +/** @internal */ export interface AcknowledgeResponse { acknowledged: boolean; shardsAcknowledged: boolean; } +function aliasArrayToRecord(aliases: string[]): Record { + const result: Record = {}; + for (const alias of aliases) { + result[alias] = {}; + } + return result; +} /** * Creates an index with the given mappings * @@ -703,16 +847,13 @@ export const createIndex = ( client: ElasticsearchClient, indexName: string, mappings: IndexMapping, - aliases?: string[] + aliases: string[] = [] ): TaskEither.TaskEither => { const createIndexTask: TaskEither.TaskEither< RetryableEsClientError, AcknowledgeResponse > = () => { - const aliasesObject = (aliases ?? []).reduce((acc, alias) => { - acc[alias] = {}; - return acc; - }, {} as Record); + const aliasesObject = aliasArrayToRecord(aliases); return client.indices .create( @@ -797,6 +938,7 @@ export const createIndex = ( ); }; +/** @internal */ export interface UpdateAndPickupMappingsResponse { taskId: string; } @@ -847,6 +989,8 @@ export const updateAndPickupMappings = ( }) ); }; + +/** @internal */ export interface SearchResponse { outdatedDocuments: SavedObjectsRawDoc[]; } @@ -911,7 +1055,8 @@ export const searchForOutdatedDocuments = ( export const bulkOverwriteTransformedDocuments = ( client: ElasticsearchClient, index: string, - transformedDocs: SavedObjectsRawDoc[] + transformedDocs: SavedObjectsRawDoc[], + refresh: estypes.Refresh ): TaskEither.TaskEither => () => { return client .bulk({ @@ -924,15 +1069,7 @@ export const bulkOverwriteTransformedDocuments = ( // system indices puts in place a hard control. require_alias: false, wait_for_active_shards: WAIT_FOR_ALL_SHARDS_TO_BE_ACTIVE, - // Wait for a refresh to happen before returning. This ensures that when - // this Kibana instance searches for outdated documents, it won't find - // documents that were already transformed by itself or another Kibna - // instance. However, this causes each OUTDATED_DOCUMENTS_SEARCH -> - // OUTDATED_DOCUMENTS_TRANSFORM cycle to take 1s so when batches are - // small performance will become a lot worse. - // The alternative is to use a search_after with either a tie_breaker - // field or using a Point In Time as a cursor to go through all documents. - refresh: 'wait_for', + refresh, filter_path: ['items.*.error'], body: transformedDocs.flatMap((doc) => { return [ diff --git a/src/core/server/saved_objects/migrationsv2/index.ts b/src/core/server/saved_objects/migrationsv2/index.ts index 6e65a2e700fd3..d4edda48e7728 100644 --- a/src/core/server/saved_objects/migrationsv2/index.ts +++ b/src/core/server/saved_objects/migrationsv2/index.ts @@ -9,9 +9,10 @@ import { ElasticsearchClient } from '../../elasticsearch'; import { IndexMapping } from '../mappings'; import { Logger } from '../../logging'; -import { SavedObjectsMigrationVersion } from '../types'; +import type { SavedObjectsMigrationVersion } from '../types'; +import type { TransformRawDocs } from './types'; import { MigrationResult } from '../migrations/core'; -import { next, TransformRawDocs } from './next'; +import { next } from './next'; import { createInitialState, model } from './model'; import { migrationStateActionMachine } from './migrations_state_action_machine'; import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts index e35e21421ac1f..7f016f70c927a 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts @@ -183,9 +183,13 @@ export async function migrationStateActionMachine({ ); } - throw new Error( + const newError = new Error( `Unable to complete saved object migrations for the [${initialState.indexPrefix}] index. ${e}` ); + + // restore error stack to point to a source of the problem. + newError.stack = `[${e.stack}]`; + throw newError; } } } diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index ee78692a7044f..ed02beab5fe62 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -464,32 +464,60 @@ export const model = (currentState: State, resW: ResponseType): } else if (stateP.controlState === 'CREATE_REINDEX_TEMP') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { - return { ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP' }; + return { ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT' }; } else { // If the createIndex action receives an 'resource_already_exists_exception' // it will wait until the index status turns green so we don't have any // left responses to handle here. throwBadResponse(stateP, res); } - } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP') { + } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { return { ...stateP, - controlState: 'REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK', - reindexSourceToTargetTaskId: res.right.taskId, + controlState: 'REINDEX_SOURCE_TO_TEMP_READ', + sourceIndexPitId: res.right.pitId, + lastHitSortValue: undefined, }; } else { - // Since this is a background task, the request should always succeed, - // errors only show up in the returned task. throwBadResponse(stateP, res); } - } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK') { + } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_READ') { + const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { + if (res.right.outdatedDocuments.length > 0) { + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX', + outdatedDocuments: res.right.outdatedDocuments, + lastHitSortValue: res.right.lastHitSortValue, + }; + } + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT', + }; + } else { + throwBadResponse(stateP, res); + } + } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { return { ...stateP, controlState: 'SET_TEMP_WRITE_BLOCK', + sourceIndex: stateP.sourceIndex as Option.Some, + }; + } else { + throwBadResponse(stateP, res); + } + } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_INDEX') { + const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_READ', }; } else { const left = res.left; @@ -508,28 +536,11 @@ export const model = (currentState: State, resW: ResponseType): // we know another instance already completed these. return { ...stateP, - controlState: 'SET_TEMP_WRITE_BLOCK', + controlState: 'REINDEX_SOURCE_TO_TEMP_READ', }; - } else if (isLeftTypeof(left, 'wait_for_task_completion_timeout')) { - // After waiting for the specificed timeout, the task has not yet - // completed. Retry this step to see if the task has completed after an - // exponential delay. We will basically keep polling forever until the - // Elasticeasrch task succeeds or fails. - return delayRetryState(stateP, left.message, Number.MAX_SAFE_INTEGER); - } else if ( - isLeftTypeof(left, 'index_not_found_exception') || - isLeftTypeof(left, 'incompatible_mapping_exception') - ) { - // Don't handle the following errors as the migration algorithm should - // never cause them to occur: - // - incompatible_mapping_exception the temp index has `dynamic: false` - // mappings - // - index_not_found_exception for the source index, we will never - // delete the source index - throwBadResponse(stateP, left as never); - } else { - throwBadResponse(stateP, left); } + // should never happen + throwBadResponse(stateP, res as never); } } else if (stateP.controlState === 'SET_TEMP_WRITE_BLOCK') { const res = resW as ExcludeRetryableEsError>; @@ -607,7 +618,7 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'OUTDATED_DOCUMENTS_SEARCH', }; } else { - throwBadResponse(stateP, res); + throwBadResponse(stateP, res as never); } } else if (stateP.controlState === 'UPDATE_TARGET_MAPPINGS') { const res = resW as ExcludeRetryableEsError>; @@ -645,10 +656,10 @@ export const model = (currentState: State, resW: ResponseType): } else { const left = res.left; if (isLeftTypeof(left, 'wait_for_task_completion_timeout')) { - // After waiting for the specificed timeout, the task has not yet + // After waiting for the specified timeout, the task has not yet // completed. Retry this step to see if the task has completed after an // exponential delay. We will basically keep polling forever until the - // Elasticeasrch task succeeds or fails. + // Elasticsearch task succeeds or fails. return delayRetryState(stateP, res.left.message, Number.MAX_SAFE_INTEGER); } else { throwBadResponse(stateP, left); diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index e9b351c0152fc..1beff4c56f1d1 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -157,6 +157,30 @@ export type CreateReindexTempState = PostInitState & { readonly sourceIndex: Option.Some; }; +export interface ReindexSourceToTempOpenPit extends PostInitState { + /** Open PIT to the source index */ + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT'; + readonly sourceIndex: Option.Some; +} + +export interface ReindexSourceToTempRead extends PostInitState { + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_READ'; + readonly sourceIndexPitId: string; + readonly lastHitSortValue: number[] | undefined; +} + +export interface ReindexSourceToTempClosePit extends PostInitState { + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT'; + readonly sourceIndexPitId: string; +} + +export interface ReindexSourceToTempIndex extends PostInitState { + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX'; + readonly outdatedDocuments: SavedObjectsRawDoc[]; + readonly sourceIndexPitId: string; + readonly lastHitSortValue: number[] | undefined; +} + export type ReindexSourceToTempState = PostInitState & { /** Reindex documents from the source index into the target index */ readonly controlState: 'REINDEX_SOURCE_TO_TEMP'; @@ -301,8 +325,12 @@ export type State = | SetSourceWriteBlockState | CreateNewTargetState | CreateReindexTempState - | ReindexSourceToTempState - | ReindexSourceToTempWaitForTaskState + | ReindexSourceToTempOpenPit + | ReindexSourceToTempRead + | ReindexSourceToTempClosePit + | ReindexSourceToTempIndex + // | ReindexSourceToTempState + // | ReindexSourceToTempWaitForTaskState | SetTempWriteBlock | CloneTempToSource | UpdateTargetMappingsState @@ -323,3 +351,5 @@ export type AllControlStates = State['controlState']; * 'FATAL' and 'DONE'). */ export type AllActionStates = Exclude; + +export type TransformRawDocs = (rawDocs: SavedObjectsRawDoc[]) => Promise; From 8c54a03e065cb6209b1771258615c55bc49c0a08 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 15 Apr 2021 11:33:43 +0200 Subject: [PATCH 03/27] update tests --- .../migrations/kibana/kibana_migrator.test.ts | 60 ++--- .../migrationsv2/actions/index.test.ts | 50 +++- .../integration_tests/actions.test.ts | 222 +++++++++++++++--- .../integration_tests/migration.test.ts | 2 + .../saved_objects/migrationsv2/model.test.ts | 142 +++++++---- 5 files changed, 354 insertions(+), 122 deletions(-) diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 221e78e3e12e2..c6dfd2c2d1809 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -229,48 +229,6 @@ describe('KibanaMigrator', () => { jest.clearAllMocks(); }); - it('creates a V2 migrator that initializes a new index and migrates an existing index', async () => { - const options = mockV2MigrationOptions(); - const migrator = new KibanaMigrator(options); - const migratorStatus = migrator.getStatus$().pipe(take(3)).toPromise(); - migrator.prepareMigrations(); - await migrator.runMigrations(); - - // Basic assertions that we're creating and reindexing the expected indices - expect(options.client.indices.create).toHaveBeenCalledTimes(3); - expect(options.client.indices.create.mock.calls).toEqual( - expect.arrayContaining([ - // LEGACY_CREATE_REINDEX_TARGET - expect.arrayContaining([expect.objectContaining({ index: '.my-index_pre8.2.3_001' })]), - // CREATE_REINDEX_TEMP - expect.arrayContaining([ - expect.objectContaining({ index: '.my-index_8.2.3_reindex_temp' }), - ]), - // CREATE_NEW_TARGET - expect.arrayContaining([expect.objectContaining({ index: 'other-index_8.2.3_001' })]), - ]) - ); - // LEGACY_REINDEX - expect(options.client.reindex.mock.calls[0][0]).toEqual( - expect.objectContaining({ - body: expect.objectContaining({ - source: expect.objectContaining({ index: '.my-index' }), - dest: expect.objectContaining({ index: '.my-index_pre8.2.3_001' }), - }), - }) - ); - // REINDEX_SOURCE_TO_TEMP - expect(options.client.reindex.mock.calls[1][0]).toEqual( - expect.objectContaining({ - body: expect.objectContaining({ - source: expect.objectContaining({ index: '.my-index_pre8.2.3_001' }), - dest: expect.objectContaining({ index: '.my-index_8.2.3_reindex_temp' }), - }), - }) - ); - const { status } = await migratorStatus; - return expect(status).toEqual('completed'); - }); it('emits results on getMigratorResult$()', async () => { const options = mockV2MigrationOptions(); const migrator = new KibanaMigrator(options); @@ -378,6 +336,24 @@ const mockV2MigrationOptions = () => { } as estypes.GetTaskResponse) ); + options.client.search = jest + .fn() + .mockImplementation(() => + elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { hits: [] } }) + ); + + options.client.openPointInTime = jest + .fn() + .mockImplementationOnce(() => + elasticsearchClientMock.createSuccessTransportRequestPromise({ id: 'pit_id' }) + ); + + options.client.closePointInTime = jest + .fn() + .mockImplementationOnce(() => + elasticsearchClientMock.createSuccessTransportRequestPromise({ succeeded: true }) + ); + return options; }; diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts index bee17f42d7bdb..4e9ecda05409b 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts @@ -78,6 +78,54 @@ describe('actions', () => { }); }); + describe('openPit', () => { + it('calls catchRetryableEsClientErrors when the promise rejects', async () => { + const task = Actions.openPit(client, 'my_index'); + try { + await task(); + } catch (e) { + /** ignore */ + } + expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); + }); + }); + + describe('readWithPit', () => { + it('calls catchRetryableEsClientErrors when the promise rejects', async () => { + const task = Actions.readWithPit(client, 'pitId', Option.some([]), 10_000); + try { + await task(); + } catch (e) { + /** ignore */ + } + expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); + }); + }); + + describe('closePit', () => { + it('calls catchRetryableEsClientErrors when the promise rejects', async () => { + const task = Actions.closePit(client, 'pitId'); + try { + await task(); + } catch (e) { + /** ignore */ + } + expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); + }); + }); + + describe('transformDocs', () => { + it('calls catchRetryableEsClientErrors when the promise rejects', async () => { + const task = Actions.transformDocs(client, () => Promise.resolve([]), [], 'my_index', false); + try { + await task(); + } catch (e) { + /** ignore */ + } + expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); + }); + }); + describe('reindex', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { const task = Actions.reindex( @@ -205,7 +253,7 @@ describe('actions', () => { describe('bulkOverwriteTransformedDocuments', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.bulkOverwriteTransformedDocuments(client, 'new_index', []); + const task = Actions.bulkOverwriteTransformedDocuments(client, 'new_index', [], 'wait_for'); try { await task(); } catch (e) { diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 21c05d22b0581..37d696e288bf2 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -14,9 +14,14 @@ import { SavedObjectsRawDoc } from '../../serialization'; import { bulkOverwriteTransformedDocuments, cloneIndex, + closePit, createIndex, fetchIndices, + openPit, + OpenPitResponse, reindex, + readWithPit, + ReadWithPit, searchForOutdatedDocuments, SearchResponse, setWriteBlock, @@ -61,7 +66,11 @@ describe('migration actions', () => { // Create test fixture data: await createIndex(client, 'existing_index_with_docs', { dynamic: true, - properties: {}, + properties: { + updated_at: { type: 'date' }, + title: { type: 'text' }, + type: { type: 'text' }, + }, })(); const sourceDocs = ([ { _source: { title: 'doc 1' } }, @@ -70,14 +79,20 @@ describe('migration actions', () => { { _source: { title: 'saved object 4', type: 'another_unused_type' } }, { _source: { title: 'f-agent-event 5', type: 'f_agent_event' } }, ] as unknown) as SavedObjectsRawDoc[]; - await bulkOverwriteTransformedDocuments(client, 'existing_index_with_docs', sourceDocs)(); + await bulkOverwriteTransformedDocuments( + client, + 'existing_index_with_docs', + sourceDocs, + 'wait_for' + )(); await createIndex(client, 'existing_index_2', { properties: {} })(); await createIndex(client, 'existing_index_with_write_block', { properties: {} })(); await bulkOverwriteTransformedDocuments( client, 'existing_index_with_write_block', - sourceDocs + sourceDocs, + 'wait_for' )(); await setWriteBlock(client, 'existing_index_with_write_block')(); await updateAliases(client, [ @@ -155,7 +170,12 @@ describe('migration actions', () => { { _source: { title: 'doc 4' } }, ] as unknown) as SavedObjectsRawDoc[]; await expect( - bulkOverwriteTransformedDocuments(client, 'new_index_without_write_block', sourceDocs)() + bulkOverwriteTransformedDocuments( + client, + 'new_index_without_write_block', + sourceDocs, + 'wait_for' + )() ).rejects.toMatchObject(expect.anything()); }); it('resolves left index_not_found_exception when the index does not exist', async () => { @@ -265,14 +285,14 @@ describe('migration actions', () => { const task = cloneIndex(client, 'existing_index_with_write_block', 'clone_target_1'); expect.assertions(1); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": Object { - "acknowledged": true, - "shardsAcknowledged": true, - }, - } - `); + Object { + "_tag": "Right", + "right": Object { + "acknowledged": true, + "shardsAcknowledged": true, + }, + } + `); }); it('resolves right after waiting for index status to be yellow if clone target already existed', async () => { expect.assertions(2); @@ -331,14 +351,14 @@ describe('migration actions', () => { expect.assertions(1); const task = cloneIndex(client, 'no_such_index', 'clone_target_3'); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Left", - "left": Object { - "index": "no_such_index", - "type": "index_not_found_exception", - }, - } - `); + Object { + "_tag": "Left", + "left": Object { + "index": "no_such_index", + "type": "index_not_found_exception", + }, + } + `); }); it('resolves left with a retryable_es_client_error if clone target already exists but takes longer than the specified timeout before turning yellow', async () => { // Create a red index @@ -427,11 +447,11 @@ describe('migration actions', () => { )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": "reindex_succeeded", - } - `); + Object { + "_tag": "Right", + "right": "reindex_succeeded", + } + `); const results = ((await searchForOutdatedDocuments(client, { batchSize: 1000, @@ -545,7 +565,7 @@ describe('migration actions', () => { _id, _source, })); - await bulkOverwriteTransformedDocuments(client, 'reindex_target_4', sourceDocs)(); + await bulkOverwriteTransformedDocuments(client, 'reindex_target_4', sourceDocs, 'wait_for')(); // Now do a real reindex const res = (await reindex( @@ -784,12 +804,128 @@ describe('migration actions', () => { ); task = verifyReindex(client, 'existing_index_2', 'no_such_index'); - await expect(task()).rejects.toMatchInlineSnapshot( - `[ResponseError: index_not_found_exception]` - ); + await expect(task()).rejects.toThrow('index_not_found_exception'); + }); + }); + + describe('openPit', () => { + it('opens PointInTime for an index', async () => { + const pitResponse = (await openPit( + client, + 'existing_index_with_docs' + )()) as Either.Right; + + expect(pitResponse.right.pitId).toEqual(expect.any(String)); + + const searchResponse = await client.search({ + body: { + pit: { id: pitResponse.right.pitId }, + }, + }); + + await expect(searchResponse.body.hits.hits.length).toBeGreaterThan(0); + }); + it('rejects if index does not exist', async () => { + const openPitTask = openPit(client, 'no_such_index'); + await expect(openPitTask()).rejects.toThrow('index_not_found_exception'); + }); + }); + + describe('readWithPit', () => { + it('requests documents from an index using given PIT', async () => { + const pitResponse = (await openPit( + client, + 'existing_index_with_docs' + )()) as Either.Right; + + const docsResponse = (await readWithPit( + client, + pitResponse.right.pitId, + Option.some([]), + 1000, + undefined + )()) as Either.Right; + + await expect(docsResponse.right.outdatedDocuments.length).toBe(5); + }); + + it('requests the batchSize of documents from an index', async () => { + const pitResponse = (await openPit( + client, + 'existing_index_with_docs' + )()) as Either.Right; + + const docsResponse = (await readWithPit( + client, + pitResponse.right.pitId, + Option.some([]), + 3, + undefined + )()) as Either.Right; + + await expect(docsResponse.right.outdatedDocuments.length).toBe(3); + }); + + it('excludes documents with types listed in unusedTypesToExclude', async () => { + const pitResponse = (await openPit( + client, + 'existing_index_with_docs' + )()) as Either.Right; + + const docsResponse = (await readWithPit( + client, + pitResponse.right.pitId, + Option.some(['f_agent_event', 'another_unused_type']), + 1000, + undefined + )()) as Either.Right; + + expect(docsResponse.right.outdatedDocuments.map((doc) => doc._source.title)) + .toMatchInlineSnapshot(` + Array [ + "doc 1", + "doc 2", + "doc 3", + ] + `); + }); + + it('rejects if PIT does not exist', async () => { + const readWithPitTask = readWithPit(client, 'no_such_pit', Option.some([]), 1000, undefined); + await expect(readWithPitTask()).rejects.toThrow('illegal_argument_exception'); }); }); + describe('closePit', () => { + it('closes PointInTime', async () => { + const pitResponse = (await openPit( + client, + 'existing_index_with_docs' + )()) as Either.Right; + + const pitId = pitResponse.right.pitId; + + await closePit(client, pitId)(); + + const searchTask = client.search({ + body: { + pit: { id: pitId }, + }, + }); + + await expect(searchTask).rejects.toThrow('search_phase_execution_exception'); + }); + + it('rejects if PIT does not exist', async () => { + const closePitTask = closePit(client, 'no_such_pit'); + await expect(closePitTask()).rejects.toThrow('illegal_argument_exception'); + }); + }); + + describe('transformDocs', () => { + it.todo('all the tests'); // add at least one test for id transformed + }); + describe('searchForOutdatedDocuments', () => { it('only returns documents that match the outdatedDocumentsQuery', async () => { expect.assertions(2); @@ -913,7 +1049,8 @@ describe('migration actions', () => { await bulkOverwriteTransformedDocuments( client, 'existing_index_without_mappings', - sourceDocs + sourceDocs, + 'wait_for' )(); // Assert that we can't search over the unmapped fields of the document @@ -1141,7 +1278,13 @@ describe('migration actions', () => { { _source: { title: 'doc 6' } }, { _source: { title: 'doc 7' } }, ] as unknown) as SavedObjectsRawDoc[]; - const task = bulkOverwriteTransformedDocuments(client, 'existing_index_with_docs', newDocs); + const task = bulkOverwriteTransformedDocuments( + client, + 'existing_index_with_docs', + newDocs, + 'wait_for' + ); + await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -1156,10 +1299,12 @@ describe('migration actions', () => { outdatedDocumentsQuery: undefined, })()) as Either.Right).right.outdatedDocuments; - const task = bulkOverwriteTransformedDocuments(client, 'existing_index_with_docs', [ - ...existingDocs, - ({ _source: { title: 'doc 8' } } as unknown) as SavedObjectsRawDoc, - ]); + const task = bulkOverwriteTransformedDocuments( + client, + 'existing_index_with_docs', + [...existingDocs, ({ _source: { title: 'doc 8' } } as unknown) as SavedObjectsRawDoc], + 'wait_for' + ); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -1174,7 +1319,12 @@ describe('migration actions', () => { { _source: { title: 'doc 7' } }, ] as unknown) as SavedObjectsRawDoc[]; await expect( - bulkOverwriteTransformedDocuments(client, 'existing_index_with_write_block', newDocs)() + bulkOverwriteTransformedDocuments( + client, + 'existing_index_with_write_block', + newDocs, + 'wait_for' + )() ).rejects.toMatchObject(expect.anything()); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index 1f8c3a535a902..37dfe9bc717d0 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -51,6 +51,8 @@ describe('migration v2', () => { migrations: { skip: false, enableV2: true, + // There are 53 docs in fixtures. Batch size configured to enforce 3 migration steps. + batchSize: 20, }, logging: { appenders: { diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 8aad62f13b8fe..6182b0df2c6fb 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -17,7 +17,10 @@ import type { LegacyReindexState, LegacyReindexWaitForTaskState, LegacyDeleteState, - ReindexSourceToTempState, + ReindexSourceToTempOpenPit, + ReindexSourceToTempRead, + ReindexSourceToTempClosePit, + ReindexSourceToTempIndex, UpdateTargetMappingsState, UpdateTargetMappingsWaitForTaskState, OutdatedDocumentsSearch, @@ -25,7 +28,6 @@ import type { MarkVersionIndexReady, BaseState, CreateReindexTempState, - ReindexSourceToTempWaitForTaskState, MarkVersionIndexReadyConflict, CreateNewTargetState, CloneTempToSource, @@ -724,7 +726,7 @@ describe('migrations v2 model', () => { }); }); describe('CREATE_REINDEX_TEMP', () => { - const createReindexTargetState: CreateReindexTempState = { + const state: CreateReindexTempState = { ...baseState, controlState: 'CREATE_REINDEX_TEMP', versionIndexReadyActions: Option.none, @@ -732,80 +734,134 @@ describe('migrations v2 model', () => { targetIndex: '.kibana_7.11.0_001', tempIndexMappings: { properties: {} }, }; - it('CREATE_REINDEX_TEMP -> REINDEX_SOURCE_TO_TEMP if action succeeds', () => { + it('CREATE_REINDEX_TEMP -> REINDEX_SOURCE_TO_TEMP_OPEN_PIT if action succeeds', () => { const res: ResponseType<'CREATE_REINDEX_TEMP'> = Either.right('create_index_succeeded'); - const newState = model(createReindexTargetState, res); - expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP'); + const newState = model(state, res); + expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_OPEN_PIT'); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); }); - describe('REINDEX_SOURCE_TO_TEMP', () => { - const reindexSourceToTargetState: ReindexSourceToTempState = { + + describe('REINDEX_SOURCE_TO_TEMP_OPEN_PIT', () => { + const state: ReindexSourceToTempOpenPit = { ...baseState, - controlState: 'REINDEX_SOURCE_TO_TEMP', + controlState: 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT', versionIndexReadyActions: Option.none, sourceIndex: Option.some('.kibana') as Option.Some, targetIndex: '.kibana_7.11.0_001', + tempIndexMappings: { properties: {} }, }; - test('REINDEX_SOURCE_TO_TEMP -> REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP'> = Either.right({ - taskId: 'reindex-task-id', + it('REINDEX_SOURCE_TO_TEMP_OPEN_PIT -> REINDEX_SOURCE_TO_TEMP_READ if action succeeds', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_OPEN_PIT'> = Either.right({ + pitId: 'pit_id', }); - const newState = model(reindexSourceToTargetState, res); - expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK'); - expect(newState.retryCount).toEqual(0); - expect(newState.retryDelay).toEqual(0); + const newState = model(state, res) as ReindexSourceToTempRead; + expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_READ'); + expect(newState.sourceIndexPitId).toBe('pit_id'); + expect(newState.lastHitSortValue).toBe(undefined); + }); + }); + + describe('REINDEX_SOURCE_TO_TEMP_READ', () => { + const state: ReindexSourceToTempRead = { + ...baseState, + controlState: 'REINDEX_SOURCE_TO_TEMP_READ', + versionIndexReadyActions: Option.none, + sourceIndex: Option.some('.kibana') as Option.Some, + sourceIndexPitId: 'pit_id', + targetIndex: '.kibana_7.11.0_001', + tempIndexMappings: { properties: {} }, + lastHitSortValue: undefined, + }; + + it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_INDEX if the index has outdated documents to reindex', () => { + const outdatedDocuments = [{ _id: '1', _source: { type: 'vis' } }]; + const lastHitSortValue = [123456]; + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({ + outdatedDocuments, + lastHitSortValue, + }); + const newState = model(state, res) as ReindexSourceToTempIndex; + expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_INDEX'); + expect(newState.outdatedDocuments).toBe(outdatedDocuments); + expect(newState.lastHitSortValue).toBe(lastHitSortValue); + }); + + it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_CLOSE_PIT if no outdated documents to reindex', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({ + outdatedDocuments: [], + lastHitSortValue: undefined, + }); + const newState = model(state, res) as ReindexSourceToTempClosePit; + expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT'); + expect(newState.sourceIndexPitId).toBe('pit_id'); }); }); - describe('REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK', () => { - const state: ReindexSourceToTempWaitForTaskState = { + + describe('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT', () => { + const state: ReindexSourceToTempClosePit = { ...baseState, - controlState: 'REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK', + controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT', versionIndexReadyActions: Option.none, sourceIndex: Option.some('.kibana') as Option.Some, + sourceIndexPitId: 'pit_id', targetIndex: '.kibana_7.11.0_001', - reindexSourceToTargetTaskId: 'reindex-task-id', + tempIndexMappings: { properties: {} }, }; - test('REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK -> SET_TEMP_WRITE_BLOCK when response is right', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK'> = Either.right( - 'reindex_succeeded' + + it('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT -> SET_TEMP_WRITE_BLOCK if action succeeded', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT'> = Either.right({}); + const newState = model(state, res) as ReindexSourceToTempIndex; + expect(newState.controlState).toBe('SET_TEMP_WRITE_BLOCK'); + expect(newState.sourceIndex).toEqual(state.sourceIndex); + }); + }); + + describe('REINDEX_SOURCE_TO_TEMP_INDEX', () => { + const state: ReindexSourceToTempIndex = { + ...baseState, + controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX', + outdatedDocuments: [], + versionIndexReadyActions: Option.none, + sourceIndex: Option.some('.kibana') as Option.Some, + sourceIndexPitId: 'pit_id', + targetIndex: '.kibana_7.11.0_001', + lastHitSortValue: undefined, + }; + + it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right( + 'bulk_index_succeeded' ); const newState = model(state, res); - expect(newState.controlState).toEqual('SET_TEMP_WRITE_BLOCK'); + expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ'); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); - test('REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK -> SET_TEMP_WRITE_BLOCK when response is left target_index_had_write_block', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK'> = Either.left({ + + it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ when response is left target_index_had_write_block', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.left({ type: 'target_index_had_write_block', }); - const newState = model(state, res); - expect(newState.controlState).toEqual('SET_TEMP_WRITE_BLOCK'); + const newState = model(state, res) as ReindexSourceToTempRead; + expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ'); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); - test('REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK -> SET_TEMP_WRITE_BLOCK when response is left index_not_found_exception', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK'> = Either.left({ + + it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ when response is left index_not_found_exception for temp index', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.left({ type: 'index_not_found_exception', - index: '.kibana_7.11.0_reindex_temp', + index: state.tempIndex, }); - const newState = model(state, res); - expect(newState.controlState).toEqual('SET_TEMP_WRITE_BLOCK'); + const newState = model(state, res) as ReindexSourceToTempRead; + expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ'); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); - test('REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK -> REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK when response is left wait_for_task_completion_timeout', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK'> = Either.left({ - message: '[timeout_exception] Timeout waiting for ...', - type: 'wait_for_task_completion_timeout', - }); - const newState = model(state, res); - expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK'); - expect(newState.retryCount).toEqual(1); - expect(newState.retryDelay).toEqual(2000); - }); }); + describe('SET_TEMP_WRITE_BLOCK', () => { const state: SetTempWriteBlock = { ...baseState, From 2cf6d41c867ef90bac2a1dafc67db7b4c515cb9a Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 15 Apr 2021 11:33:58 +0200 Subject: [PATCH 04/27] lol --- .../server/saved_objects/migrationsv2/next.ts | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 5cbda741a0ce5..859d81b5f097c 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import * as TaskEither from 'fp-ts/lib/TaskEither'; -import * as Option from 'fp-ts/lib/Option'; -import { UnwrapPromise } from '@kbn/utility-types'; -import { pipe } from 'fp-ts/lib/pipeable'; +import type { UnwrapPromise } from '@kbn/utility-types'; import type { AllActionStates, - ReindexSourceToTempState, + ReindexSourceToTempOpenPit, + ReindexSourceToTempRead, + ReindexSourceToTempClosePit, + ReindexSourceToTempIndex, MarkVersionIndexReady, InitState, LegacyCreateReindexTargetState, @@ -27,18 +27,16 @@ import type { UpdateTargetMappingsState, UpdateTargetMappingsWaitForTaskState, CreateReindexTempState, - ReindexSourceToTempWaitForTaskState, MarkVersionIndexReadyConflict, CreateNewTargetState, CloneTempToSource, SetTempWriteBlock, WaitForYellowSourceState, + TransformRawDocs, } from './types'; import * as Actions from './actions'; import { ElasticsearchClient } from '../../elasticsearch'; -import { SavedObjectsRawDoc } from '..'; -export type TransformRawDocs = (rawDocs: SavedObjectsRawDoc[]) => Promise; type ActionMap = ReturnType; /** @@ -63,19 +61,28 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra Actions.createIndex(client, state.targetIndex, state.targetIndexMappings), CREATE_REINDEX_TEMP: (state: CreateReindexTempState) => Actions.createIndex(client, state.tempIndex, state.tempIndexMappings), - REINDEX_SOURCE_TO_TEMP: (state: ReindexSourceToTempState) => - Actions.reindex( + REINDEX_SOURCE_TO_TEMP_OPEN_PIT: (state: ReindexSourceToTempOpenPit) => + Actions.openPit(client, state.sourceIndex.value), + REINDEX_SOURCE_TO_TEMP_READ: (state: ReindexSourceToTempRead) => + Actions.readWithPit( client, - state.sourceIndex.value, + state.sourceIndexPitId, + state.unusedTypesToExclude, + state.batchSize, + state.lastHitSortValue + ), + REINDEX_SOURCE_TO_TEMP_CLOSE_PIT: (state: ReindexSourceToTempClosePit) => + Actions.closePit(client, state.sourceIndexPitId), + REINDEX_SOURCE_TO_TEMP_INDEX: (state: ReindexSourceToTempIndex) => + Actions.transformDocs( + client, + transformRawDocs, + state.outdatedDocuments, state.tempIndex, - Option.none, - false, - state.unusedTypesToExclude + false ), SET_TEMP_WRITE_BLOCK: (state: SetTempWriteBlock) => Actions.setWriteBlock(client, state.tempIndex), - REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK: (state: ReindexSourceToTempWaitForTaskState) => - Actions.waitForReindexTask(client, state.reindexSourceToTargetTaskId, '60s'), CLONE_TEMP_TO_TARGET: (state: CloneTempToSource) => Actions.cloneIndex(client, state.tempIndex, state.targetIndex), UPDATE_TARGET_MAPPINGS: (state: UpdateTargetMappingsState) => @@ -89,16 +96,20 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra outdatedDocumentsQuery: state.outdatedDocumentsQuery, }), OUTDATED_DOCUMENTS_TRANSFORM: (state: OutdatedDocumentsTransform) => - pipe( - TaskEither.tryCatch( - () => transformRawDocs(state.outdatedDocuments), - (e) => { - throw e; - } - ), - TaskEither.chain((docs) => - Actions.bulkOverwriteTransformedDocuments(client, state.targetIndex, docs) - ) + // Wait for a refresh to happen before returning. This ensures that when + // this Kibana instance searches for outdated documents, it won't find + // documents that were already transformed by itself or another Kibana + // instance. However, this causes each OUTDATED_DOCUMENTS_SEARCH -> + // OUTDATED_DOCUMENTS_TRANSFORM cycle to take 1s so when batches are + // small performance will become a lot worse. + // The alternative is to use a search_after with either a tie_breaker + // field or using a Point In Time as a cursor to go through all documents. + Actions.transformDocs( + client, + transformRawDocs, + state.outdatedDocuments, + state.targetIndex, + 'wait_for' ), MARK_VERSION_INDEX_READY: (state: MarkVersionIndexReady) => Actions.updateAliases(client, state.versionIndexReadyActions.value), From 4afb2854d33be26be24b0d77bbda4ccb839b1d21 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 15 Apr 2021 11:34:43 +0200 Subject: [PATCH 05/27] remove unnecessary param from request generic --- src/core/server/saved_objects/service/lib/repository.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 7c719ac56a835..c64ee49c4edfe 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -1903,10 +1903,7 @@ export class SavedObjectsRepository { ...(preference ? { preference } : {}), }; - const { - body, - statusCode, - } = await this.client.openPointInTime( + const { body, statusCode } = await this.client.openPointInTime( // @ts-expect-error @elastic/elasticsearch OpenPointInTimeRequest.index expected to accept string[] esOptions, { From 68416681fb37f26c2af002d41b37dfd90c013a33 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 15 Apr 2021 11:52:50 +0200 Subject: [PATCH 06/27] remove unused parameter --- .../migrations/core/index_migrator.ts | 3 +-- .../migrations/core/migrate_raw_docs.test.ts | 20 ++++++------------- .../migrations/core/migrate_raw_docs.ts | 4 +--- .../migrations/kibana/kibana_migrator.ts | 8 +------- 4 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index 5bf5ae26f6a0a..472fb4f8d1a39 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -189,8 +189,7 @@ async function migrateSourceToDest(context: Context) { serializer, documentMigrator.migrateAndConvert, // @ts-expect-error @elastic/elasticsearch `Hit._id` may be a string | number in ES, but we always expect strings in the SO index. - docs, - log + docs ) ); } diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index 66750a8abf1db..45e73f7dfae30 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -11,7 +11,6 @@ import _ from 'lodash'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectsSerializer } from '../../serialization'; import { migrateRawDocs } from './migrate_raw_docs'; -import { createSavedObjectsMigrationLoggerMock } from '../../migrations/mocks'; describe('migrateRawDocs', () => { test('converts raw docs to saved objects', async () => { @@ -24,8 +23,7 @@ describe('migrateRawDocs', () => { [ { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, - ], - createSavedObjectsMigrationLoggerMock() + ] ); expect(result).toEqual([ @@ -59,7 +57,6 @@ describe('migrateRawDocs', () => { }); test('throws when encountering a corrupt saved object document', async () => { - const logger = createSavedObjectsMigrationLoggerMock(); const transform = jest.fn((doc: any) => [ set(_.cloneDeep(doc), 'attributes.name', 'TADA'), ]); @@ -69,8 +66,7 @@ describe('migrateRawDocs', () => { [ { _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } }, { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, - ], - logger + ] ); expect(result).rejects.toMatchInlineSnapshot( @@ -88,8 +84,7 @@ describe('migrateRawDocs', () => { const result = await migrateRawDocs( new SavedObjectsSerializer(new SavedObjectTypeRegistry()), transform, - [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }], - createSavedObjectsMigrationLoggerMock() + [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] ); expect(result).toEqual([ @@ -119,12 +114,9 @@ describe('migrateRawDocs', () => { throw new Error('error during transform'); }); await expect( - migrateRawDocs( - new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - transform, - [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }], - createSavedObjectsMigrationLoggerMock() - ) + migrateRawDocs(new SavedObjectsSerializer(new SavedObjectTypeRegistry()), transform, [ + { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, + ]) ).rejects.toThrowErrorMatchingInlineSnapshot(`"error during transform"`); }); }); diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts index e75f29e54c876..102ec81646a92 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts @@ -16,7 +16,6 @@ import { SavedObjectUnsanitizedDoc, } from '../../serialization'; import { MigrateAndConvertFn } from './document_migrator'; -import { SavedObjectsMigrationLogger } from '.'; /** * Error thrown when saved object migrations encounter a corrupt saved object. @@ -46,8 +45,7 @@ export class CorruptSavedObjectError extends Error { export async function migrateRawDocs( serializer: SavedObjectsSerializer, migrateDoc: MigrateAndConvertFn, - rawDocs: SavedObjectsRawDoc[], - log: SavedObjectsMigrationLogger + rawDocs: SavedObjectsRawDoc[] ): Promise { const migrateDocWithoutBlocking = transformNonBlocking(migrateDoc); const processedDocs = []; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index 29852f8ac6445..58dcae7309eea 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -36,7 +36,6 @@ import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectsType } from '../../types'; import { runResilientMigrator } from '../../migrationsv2'; import { migrateRawDocs } from '../core/migrate_raw_docs'; -import { MigrationLogger } from '../core/migration_logger'; export interface KibanaMigratorOptions { client: ElasticsearchClient; @@ -185,12 +184,7 @@ export class KibanaMigrator { logger: this.log, preMigrationScript: indexMap[index].script, transformRawDocs: (rawDocs: SavedObjectsRawDoc[]) => - migrateRawDocs( - this.serializer, - this.documentMigrator.migrateAndConvert, - rawDocs, - new MigrationLogger(this.log) - ), + migrateRawDocs(this.serializer, this.documentMigrator.migrateAndConvert, rawDocs), migrationVersionPerType: this.documentMigrator.migrationVersion, indexPrefix: index, migrationsConfig: this.soMigrationsConfig, From d3a2dd12838c4bedb1f4f2e818d96a027f58c602 Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 16 Apr 2021 11:37:20 +0200 Subject: [PATCH 07/27] optimize search when quierying SO for migration --- .../migrationsv2/actions/index.ts | 10 +++---- .../integration_tests/actions.test.ts | 26 ++++++++----------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index a860419cf8fbb..a0a5fb1f915ae 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -468,18 +468,16 @@ export const readWithPit = ( return client .search({ body: { - // Sort fields are required to use searchAfter, so we set some defaults here + // Sort fields are required to use searchAfter sort: { - updated_at: { order: 'desc' }, + // the most efficient option as order is not important for the migration + _shard_doc: { order: 'desc' }, }, pit: { id: pitId, keep_alive: pitKeepAlive }, size: batchSize, search_after: searchAfter, // Exclude saved object types - query: Option.fold( - () => undefined, - (query) => query - )(unusedTypesQuery), + query: Option.isSome(unusedTypesQuery) ? unusedTypesQuery.value : undefined, }, }) .then((response) => { diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index abea0a513811e..704064ab0681a 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -66,11 +66,7 @@ describe('migration actions', () => { // Create test fixture data: await createIndex(client, 'existing_index_with_docs', { dynamic: true, - properties: { - updated_at: { type: 'date' }, - title: { type: 'text' }, - type: { type: 'text' }, - }, + properties: {}, })(); const sourceDocs = ([ { _source: { title: 'doc 1' } }, @@ -426,13 +422,13 @@ describe('migration actions', () => { targetIndex: 'reindex_target', outdatedDocumentsQuery: undefined, })()) as Either.Right).right.outdatedDocuments; - expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` + expect(results.map((doc) => doc._source.title).sort()).toMatchInlineSnapshot(` Array [ "doc 1", "doc 2", "doc 3", - "saved object 4", "f-agent-event 5", + "saved object 4", ] `); }); @@ -464,7 +460,7 @@ describe('migration actions', () => { targetIndex: 'reindex_target_excluded_docs', outdatedDocumentsQuery: undefined, })()) as Either.Right).right.outdatedDocuments; - expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` + expect(results.map((doc) => doc._source.title).sort()).toMatchInlineSnapshot(` Array [ "doc 1", "doc 2", @@ -494,13 +490,13 @@ describe('migration actions', () => { targetIndex: 'reindex_target_2', outdatedDocumentsQuery: undefined, })()) as Either.Right).right.outdatedDocuments; - expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` + expect(results.map((doc) => doc._source.title).sort()).toMatchInlineSnapshot(` Array [ "doc 1_updated", "doc 2_updated", "doc 3_updated", - "saved object 4_updated", "f-agent-event 5_updated", + "saved object 4_updated", ] `); }); @@ -546,13 +542,13 @@ describe('migration actions', () => { targetIndex: 'reindex_target_3', outdatedDocumentsQuery: undefined, })()) as Either.Right).right.outdatedDocuments; - expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` + expect(results.map((doc) => doc._source.title).sort()).toMatchInlineSnapshot(` Array [ "doc 1_updated", "doc 2_updated", "doc 3_updated", - "saved object 4_updated", "f-agent-event 5_updated", + "saved object 4_updated", ] `); }); @@ -596,13 +592,13 @@ describe('migration actions', () => { targetIndex: 'reindex_target_4', outdatedDocumentsQuery: undefined, })()) as Either.Right).right.outdatedDocuments; - expect(results.map((doc) => doc._source.title)).toMatchInlineSnapshot(` + expect(results.map((doc) => doc._source.title).sort()).toMatchInlineSnapshot(` Array [ "doc 1", "doc 2", "doc 3_updated", - "saved object 4_updated", "f-agent-event 5_updated", + "saved object 4_updated", ] `); }); @@ -901,7 +897,7 @@ describe('migration actions', () => { undefined )()) as Either.Right; - expect(docsResponse.right.outdatedDocuments.map((doc) => doc._source.title)) + expect(docsResponse.right.outdatedDocuments.map((doc) => doc._source.title).sort()) .toMatchInlineSnapshot(` Array [ "doc 1", From be9438e2832539428deb6f0614e03165e3749a3f Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 16 Apr 2021 12:36:03 +0200 Subject: [PATCH 08/27] fix wrong type in fixtures --- test/functional/fixtures/es_archiver/visualize/data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json index 66941e201e9ba..f337bffe80f2c 100644 --- a/test/functional/fixtures/es_archiver/visualize/data.json +++ b/test/functional/fixtures/es_archiver/visualize/data.json @@ -207,7 +207,7 @@ "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]", "title": "test_index*" }, - "type": "test_index*" + "type": "index-pattern" } } } From 4ebf73b269f11702169cb2c146ff7e5893a2c94a Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 16 Apr 2021 14:23:42 +0200 Subject: [PATCH 09/27] try shard_doc asc --- src/core/server/saved_objects/migrationsv2/actions/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index a0a5fb1f915ae..eea4e6b0f3c52 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -471,7 +471,7 @@ export const readWithPit = ( // Sort fields are required to use searchAfter sort: { // the most efficient option as order is not important for the migration - _shard_doc: { order: 'desc' }, + _shard_doc: { order: 'asc' }, }, pit: { id: pitId, keep_alive: pitKeepAlive }, size: batchSize, From e40c82400eb12c98e3f74679e52c066279903b0a Mon Sep 17 00:00:00 2001 From: restrry Date: Mon, 19 Apr 2021 18:01:07 +0200 Subject: [PATCH 10/27] add an integration test --- .../7.13.0_so_with_multiple_namespaces.zip | Bin 0 -> 56841 bytes .../integration_tests/rewriting_id.test.ts | 240 ++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_so_with_multiple_namespaces.zip create mode 100644 src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_so_with_multiple_namespaces.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_so_with_multiple_namespaces.zip new file mode 100644 index 0000000000000000000000000000000000000000..a92211c16c559330c906aefae1768576c912580d GIT binary patch literal 56841 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 literal 0 HcmV?d00001 diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts new file mode 100644 index 0000000000000..a70e8ead7c61d --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts @@ -0,0 +1,240 @@ +/* + * 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 Path from 'path'; +import Fs from 'fs'; +import Util from 'util'; +import uuidv5 from 'uuid/v5'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; +import * as kbnTestServer from '../../../../test_helpers/kbn_server'; +import type { ElasticsearchClient } from '../../../elasticsearch'; +import { Root } from '../../../root'; + +const logFilePath = Path.join(__dirname, 'migration_test_kibana.log'); + +const asyncUnlink = Util.promisify(Fs.unlink); +async function removeLogFile() { + // ignore errors if it doesn't exist + await asyncUnlink(logFilePath).catch(() => void 0); +} + +function sortByTypeAndId(a: { type: string; id: string }, b: { type: string; id: string }) { + return a.type.localeCompare(b.type) || a.id.localeCompare(b.id); +} + +async function fetchDocs(esClient: ElasticsearchClient, index: string) { + const { body } = await esClient.search({ + index, + body: { + query: { + bool: { + should: [ + { + term: { type: 'foo' }, + }, + { + term: { type: 'bar' }, + }, + { + term: { type: 'legacy-url-alias' }, + }, + ], + }, + }, + }, + }); + + return body.hits.hits + .map((h) => ({ + ...h._source, + id: h._id, + })) + .sort(sortByTypeAndId); +} + +function createRoot() { + return kbnTestServer.createRootWithCorePlugins( + { + migrations: { + skip: false, + enableV2: true, + }, + logging: { + appenders: { + file: { + type: 'file', + fileName: logFilePath, + layout: { + type: 'json', + }, + }, + }, + loggers: [ + { + name: 'root', + appenders: ['file'], + }, + ], + }, + }, + { + oss: true, + } + ); +} + +describe('migration v2', () => { + let esServer: kbnTestServer.TestElasticsearchUtils; + let root: Root; + + beforeAll(async () => { + await removeLogFile(); + }); + + afterAll(async () => { + if (root) { + await root.shutdown(); + } + if (esServer) { + await esServer.stop(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }); + + it('rewrites id deterministically for SO with namespaceType: "multiple" and "multiple-isolated"', async () => { + const migratedIndex = `.kibana_${pkg.version}_001`; + const { startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'trial', + // original SO: + // [ + // { id: 'foo:1', type: 'foo', foo: { name: 'Foo 1 default' } }, + // { id: 'spacex:foo:1', type: 'foo', foo: { name: 'Foo 1 spacex' }, namespace: 'spacex' }, + // { + // id: 'bar:1', + // type: 'bar', + // bar: { nomnom: 1 }, + // references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }], + // }, + // { + // id: 'spacex:bar:1', + // type: 'bar', + // bar: { nomnom: 2 }, + // references: [{ type: 'foo', id: '1', name: 'Foo 1 spacex' }], + // namespace: 'spacex', + // }, + // ]; + dataArchive: Path.join(__dirname, 'archives', '7.13.0_so_with_multiple_namespaces.zip'), + }, + }, + }); + + root = createRoot(); + + esServer = await startES(); + const coreSetup = await root.setup(); + + coreSetup.savedObjects.registerType({ + name: 'foo', + hidden: false, + mappings: { properties: { name: { type: 'text' } } }, + namespaceType: 'multiple', + convertToMultiNamespaceTypeVersion: '8.0.0', + }); + + coreSetup.savedObjects.registerType({ + name: 'bar', + hidden: false, + mappings: { properties: { nomnom: { type: 'integer' } } }, + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', + }); + + const coreStart = await root.start(); + const esClient = coreStart.elasticsearch.client.asInternalUser; + + const migratedDocs = await fetchDocs(esClient, migratedIndex); + + // 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 + const newFooId = uuidv5('spacex:foo:1', uuidv5.DNS); + const newBarId = uuidv5('spacex:bar:1', uuidv5.DNS); + + expect(migratedDocs).toEqual( + [ + { + id: 'foo:1', + type: 'foo', + foo: { name: 'Foo 1 default' }, + references: [], + namespaces: ['default'], + migrationVersion: { foo: '8.0.0' }, + coreMigrationVersion: pkg.version, + }, + { + id: `foo:${newFooId}`, + type: 'foo', + foo: { name: 'Foo 1 spacex' }, + references: [], + namespaces: ['spacex'], + originId: '1', + migrationVersion: { foo: '8.0.0' }, + coreMigrationVersion: pkg.version, + }, + { + // new object for spacex:foo:1 + id: 'legacy-url-alias:spacex:foo:1', + type: 'legacy-url-alias', + 'legacy-url-alias': { + targetId: newFooId, + targetNamespace: 'spacex', + targetType: 'foo', + }, + migrationVersion: {}, + references: [], + coreMigrationVersion: pkg.version, + }, + { + id: 'bar:1', + type: 'bar', + bar: { nomnom: 1 }, + references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }], + namespaces: ['default'], + migrationVersion: { bar: '8.0.0' }, + coreMigrationVersion: pkg.version, + }, + { + id: `bar:${newBarId}`, + type: 'bar', + bar: { nomnom: 2 }, + references: [{ type: 'foo', id: newFooId, name: 'Foo 1 spacex' }], + namespaces: ['spacex'], + originId: '1', + migrationVersion: { bar: '8.0.0' }, + coreMigrationVersion: pkg.version, + }, + { + // new object for spacex:bar:1 + id: 'legacy-url-alias:spacex:bar:1', + type: 'legacy-url-alias', + 'legacy-url-alias': { + targetId: newBarId, + targetNamespace: 'spacex', + targetType: 'bar', + }, + migrationVersion: {}, + references: [], + coreMigrationVersion: pkg.version, + }, + ].sort(sortByTypeAndId) + ); + }); +}); From 8c03eafca8ffb6c190463751630540d733401c04 Mon Sep 17 00:00:00 2001 From: restrry Date: Mon, 19 Apr 2021 18:04:54 +0200 Subject: [PATCH 11/27] cleanup --- src/core/server/saved_objects/migrationsv2/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index 76085366a9648..7ce8a95db4996 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -330,8 +330,6 @@ export type State = | ReindexSourceToTempRead | ReindexSourceToTempClosePit | ReindexSourceToTempIndex - // | ReindexSourceToTempState - // | ReindexSourceToTempWaitForTaskState | SetTempWriteBlock | CloneTempToSource | UpdateTargetMappingsState From e522a4b01b1aba7a99b881ef84bc5100b8a39fcd Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 20 Apr 2021 13:55:35 +0200 Subject: [PATCH 12/27] track_total_hits: false to improve perf --- src/core/server/saved_objects/migrationsv2/actions/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index eea4e6b0f3c52..049cdc41b7527 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -476,6 +476,9 @@ export const readWithPit = ( pit: { id: pitId, keep_alive: pitKeepAlive }, size: batchSize, search_after: searchAfter, + // Improve performance by not calculating the total number of hits + // matching the query. + track_total_hits: false, // Exclude saved object types query: Option.isSome(unusedTypesQuery) ? unusedTypesQuery.value : undefined, }, From ea3a1c42d294e84d34e36017e90c87182515ecb5 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 20 Apr 2021 19:06:57 +0200 Subject: [PATCH 13/27] add happy path test for transformDocs action --- .../integration_tests/actions.test.ts | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 704064ab0681a..537fa32f391e3 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -35,6 +35,7 @@ import { UpdateAndPickupMappingsResponse, verifyReindex, removeWriteBlock, + transformDocs, waitForIndexStatusYellow, } from '../actions'; import * as Either from 'fp-ts/lib/Either'; @@ -940,7 +941,44 @@ describe('migration actions', () => { }); describe('transformDocs', () => { - it.todo('all the tests'); // add at least one test for id transformed + it('applies "transformRawDocs" and writes result into an index', async () => { + const index = 'transform_docs_index'; + const originalDocs = [ + { _id: 'foo:1', _source: { type: 'dashboard', value: 1 } }, + { _id: 'foo:2', _source: { type: 'dashboard', value: 2 } }, + ]; + + await createIndex(client, index, { + dynamic: true, + properties: {}, + })(); + + const result = (await transformDocs( + client, + async function (docs) { + for (const doc of docs) { + doc._source.value += 1; + } + return docs; + }, + originalDocs, + index, + 'wait_for' + )()) as Either.Right<'bulk_index_succeeded'>; + + expect(result.right).toBe('bulk_index_succeeded'); + + const { body } = await client.search<{ value: number }>({ + index, + }); + const hits = body.hits.hits; + + const foo1 = hits.find((h) => h._id === 'foo:1'); + expect(foo1?._source?.value).toBe(2); + + const foo2 = hits.find((h) => h._id === 'foo:2'); + expect(foo2?._source?.value).toBe(3); + }); }); describe('searchForOutdatedDocuments', () => { From 3aab1165de7dca265966e0758012b31bedc85e9d Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 20 Apr 2021 19:53:16 +0200 Subject: [PATCH 14/27] remove unused types --- .../server/saved_objects/migrationsv2/types.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index 7ce8a95db4996..093e97d236a5d 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -182,22 +182,6 @@ export interface ReindexSourceToTempIndex extends PostInitState { readonly lastHitSortValue: number[] | undefined; } -export type ReindexSourceToTempState = PostInitState & { - /** Reindex documents from the source index into the target index */ - readonly controlState: 'REINDEX_SOURCE_TO_TEMP'; - readonly sourceIndex: Option.Some; -}; - -export type ReindexSourceToTempWaitForTaskState = PostInitState & { - /** - * Wait until reindexing documents from the source index into the target - * index has completed - */ - readonly controlState: 'REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK'; - readonly sourceIndex: Option.Some; - readonly reindexSourceToTargetTaskId: string; -}; - export type SetTempWriteBlock = PostInitState & { /** * From 01266161a0888b45c162f20fe1ccc75c7ec9eab7 Mon Sep 17 00:00:00 2001 From: restrry Date: Wed, 21 Apr 2021 18:40:46 +0200 Subject: [PATCH 15/27] fix wrong typing --- src/core/test_helpers/kbn_server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 950ab5f4392e1..dbf19f84825be 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Client } from 'elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; import { // @ts-expect-error https://github.com/elastic/kibana/issues/95679 @@ -140,7 +140,7 @@ export interface TestElasticsearchServer { start: (esArgs: string[], esEnvVars: Record) => Promise; stop: () => Promise; cleanup: () => Promise; - getClient: () => Client; + getClient: () => KibanaClient; getCallCluster: () => LegacyAPICaller; getUrl: () => string; } From aeb35cbc572dba4c1d6c2a2ae773d6145db06d25 Mon Sep 17 00:00:00 2001 From: restrry Date: Wed, 21 Apr 2021 18:42:03 +0200 Subject: [PATCH 16/27] add cleanup phase --- .../saved_objects/migrationsv2/index.ts | 1 + .../migrations_state_action_machine.ts | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/core/server/saved_objects/migrationsv2/index.ts b/src/core/server/saved_objects/migrationsv2/index.ts index d4edda48e7728..25816c7fd14c6 100644 --- a/src/core/server/saved_objects/migrationsv2/index.ts +++ b/src/core/server/saved_objects/migrationsv2/index.ts @@ -56,5 +56,6 @@ export async function runResilientMigrator({ logger, next: next(client, transformRawDocs), model, + client, }); } diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts index 7f016f70c927a..f6f13ab8fc738 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts @@ -9,6 +9,8 @@ import { errors as EsErrors } from '@elastic/elasticsearch'; import * as Option from 'fp-ts/lib/Option'; import { Logger, LogMeta } from '../../logging'; +import type { ElasticsearchClient } from '../../elasticsearch'; +import * as Actions from './actions'; import { CorruptSavedObjectError } from '../migrations/core/migrate_raw_docs'; import { Model, Next, stateActionMachine } from './state_action_machine'; import { State } from './types'; @@ -25,6 +27,11 @@ type ExecutionLog = Array< controlState: State['controlState']; res: unknown; } + | { + type: 'cleanup'; + state: State; + message: string; + } >; const logStateTransition = ( @@ -80,11 +87,13 @@ export async function migrationStateActionMachine({ logger, next, model, + client, }: { initialState: State; logger: Logger; next: Next; model: Model; + client: ElasticsearchClient; }) { const executionLog: ExecutionLog = []; const startTime = Date.now(); @@ -93,11 +102,13 @@ export async function migrationStateActionMachine({ // indicate which messages come from which index upgrade. const logMessagePrefix = `[${initialState.indexPrefix}] `; let prevTimestamp = startTime; + let lastState: State | undefined; try { const finalState = await stateActionMachine( initialState, (state) => next(state), (state, res) => { + lastState = state; executionLog.push({ type: 'response', res, @@ -150,6 +161,7 @@ export async function migrationStateActionMachine({ }; } } else if (finalState.controlState === 'FATAL') { + await cleanup(client, executionLog, finalState); dumpExecutionLog(logger, logMessagePrefix, executionLog); return Promise.reject( new Error( @@ -161,6 +173,7 @@ export async function migrationStateActionMachine({ throw new Error('Invalid terminating control state'); } } catch (e) { + await cleanup(client, executionLog, lastState); if (e instanceof EsErrors.ResponseError) { logger.error( logMessagePrefix + `[${e.body?.error?.type}]: ${e.body?.error?.reason ?? e.message}` @@ -193,3 +206,18 @@ export async function migrationStateActionMachine({ } } } + +async function cleanup(client: ElasticsearchClient, executionLog: ExecutionLog, state?: State) { + if (!state) return; + if ('sourceIndexPitId' in state) { + try { + await Actions.closePit(client, state.sourceIndexPitId)(); + } catch (e) { + executionLog.push({ + type: 'cleanup', + state, + message: e.message, + }); + } + } +} From 9c768a90b15acdf77fece06e8e0f52881e172a6b Mon Sep 17 00:00:00 2001 From: restrry Date: Wed, 21 Apr 2021 18:46:14 +0200 Subject: [PATCH 17/27] add an integration test for cleanup phase --- .../migrationsv2/integration_tests/.gitignore | 2 +- .../archives/7.13.0_with_corrupted_so.zip | Bin 0 -> 49885 bytes .../integration_tests/cleanup.test.ts | 131 ++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip create mode 100644 src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/.gitignore b/src/core/server/saved_objects/migrationsv2/integration_tests/.gitignore index 57208badcc680..397b4a7624e35 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/.gitignore +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/.gitignore @@ -1 +1 @@ -migration_test_kibana.log +*.log diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip new file mode 100644 index 0000000000000000000000000000000000000000..c6c89ac2879b2e674c1d613e6de0271632db9ebc GIT binary patch literal 49885 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 MDF}#0BJf}T2dHD44FCWD literal 0 HcmV?d00001 diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts new file mode 100644 index 0000000000000..48bb282da18f6 --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts @@ -0,0 +1,131 @@ +/* + * 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 Path from 'path'; +import Fs from 'fs'; +import Util from 'util'; +import JSON5 from 'json5'; +import * as kbnTestServer from '../../../../test_helpers/kbn_server'; +import type { Root } from '../../../root'; + +const logFilePath = Path.join(__dirname, 'cleanup_test.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 kbnTestServer.createRootWithCorePlugins( + { + migrations: { + skip: false, + enableV2: true, + }, + logging: { + appenders: { + file: { + type: 'file', + fileName: logFilePath, + layout: { + type: 'json', + }, + }, + }, + loggers: [ + { + name: 'root', + appenders: ['file'], + }, + ], + }, + }, + { + oss: true, + } + ); +} + +describe('migration v2', () => { + let esServer: kbnTestServer.TestElasticsearchUtils; + let root: Root; + + beforeAll(async () => { + await removeLogFile(); + }); + + afterAll(async () => { + if (root) { + await root.shutdown(); + } + if (esServer) { + await esServer.stop(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }); + + it('clean ups if migration fails', async () => { + const { startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'trial', + // 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.setup(); + + await expect(root.start()).rejects.toThrow( + /Unable to migrate the corrupt saved object document with _id: 'index-pattern:test_index\*'/ + ); + + const logFileContent = await asyncReadFile(logFilePath, 'utf-8'); + const records = logFileContent + .split('\n') + .filter(Boolean) + .map((str) => JSON5.parse(str)); + + const logRecordWithPit = records.find( + (rec) => rec.message === '[.kibana] REINDEX_SOURCE_TO_TEMP_OPEN_PIT RESPONSE' + ); + + expect(logRecordWithPit).toBeTruthy(); + + const pitId = logRecordWithPit.right.pitId; + expect(pitId).toBeTruthy(); + + const client = esServer.es.getClient(); + await expect( + client.search({ + body: { + pit: { id: pitId }, + }, + }) + // throws an exception that cannot search with closed PIT + ).rejects.toThrow(/search_phase_execution_exception/); + }); +}); From 8e331ce45e5ab97e5e5e5edc74c839cca9ae4b1c Mon Sep 17 00:00:00 2001 From: restrry Date: Thu, 22 Apr 2021 09:47:35 +0200 Subject: [PATCH 18/27] add unit-tests for cleanup function --- .../migrations_state_action_machine.test.ts | 47 ++++++++++++++++++- .../migrations_state_action_machine.ts | 20 ++------ .../migrations_state_machine_cleanup.mocks.ts | 12 +++++ .../migrations_state_machine_cleanup.ts | 31 ++++++++++++ 4 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.mocks.ts create mode 100644 src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts index fa2e65f16bb2d..075a15862aaf5 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts @@ -5,9 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { cleanupMock } from './migrations_state_machine_cleanup.mocks'; import { migrationStateActionMachine } from './migrations_state_action_machine'; -import { loggingSystemMock } from '../../mocks'; +import { loggingSystemMock, elasticsearchServiceMock } from '../../mocks'; import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; import { AllControlStates, State } from './types'; @@ -15,6 +15,7 @@ import { createInitialState } from './model'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; +const esClient = elasticsearchServiceMock.createElasticsearchClient(); describe('migrationsStateActionMachine', () => { beforeAll(() => { jest @@ -74,6 +75,7 @@ describe('migrationsStateActionMachine', () => { logger: mockLogger.get(), model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'LEGACY_DELETE', 'DONE']), next, + client: esClient, }); const logs = loggingSystemMock.collect(mockLogger); const doneLog = logs.info.splice(8, 1)[0][0]; @@ -151,6 +153,7 @@ describe('migrationsStateActionMachine', () => { logger: mockLogger.get(), model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'LEGACY_DELETE', 'DONE']), next, + client: esClient, }) ).resolves.toEqual(expect.anything()); }); @@ -161,6 +164,7 @@ describe('migrationsStateActionMachine', () => { logger: mockLogger.get(), model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'LEGACY_DELETE', 'DONE']), next, + client: esClient, }) ).resolves.toEqual(expect.objectContaining({ status: 'migrated' })); }); @@ -171,6 +175,7 @@ describe('migrationsStateActionMachine', () => { logger: mockLogger.get(), model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'LEGACY_DELETE', 'DONE']), next, + client: esClient, }) ).resolves.toEqual(expect.objectContaining({ status: 'patched' })); }); @@ -181,6 +186,7 @@ describe('migrationsStateActionMachine', () => { logger: mockLogger.get(), model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'FATAL']), next, + client: esClient, }) ).rejects.toMatchInlineSnapshot( `[Error: Unable to complete saved object migrations for the [.my-so-index] index: the fatal reason]` @@ -196,6 +202,7 @@ describe('migrationsStateActionMachine', () => { logger: mockLogger.get(), model: transitionModel(['LEGACY_DELETE', 'FATAL']), next, + client: esClient, }).catch((err) => err); // Ignore the first 4 log entries that come from our model const executionLogLogs = loggingSystemMock.collect(mockLogger).info.slice(4); @@ -410,6 +417,7 @@ describe('migrationsStateActionMachine', () => { }) ); }, + client: esClient, }) ).rejects.toMatchInlineSnapshot( `[Error: Unable to complete saved object migrations for the [.my-so-index] index. Please check the health of your Elasticsearch cluster and try again. Error: [snapshot_in_progress_exception]: Cannot delete indices that are being snapshotted]` @@ -442,6 +450,7 @@ describe('migrationsStateActionMachine', () => { next: () => { throw new Error('this action throws'); }, + client: esClient, }) ).rejects.toMatchInlineSnapshot( `[Error: Unable to complete saved object migrations for the [.my-so-index] index. Error: this action throws]` @@ -475,6 +484,7 @@ describe('migrationsStateActionMachine', () => { if (state.controlState === 'LEGACY_DELETE') throw new Error('this action throws'); return () => Promise.resolve('hello'); }, + client: esClient, }); } catch (e) { /** ignore */ @@ -664,4 +674,37 @@ describe('migrationsStateActionMachine', () => { ] `); }); + describe('cleanup', () => { + beforeEach(() => { + cleanupMock.mockClear(); + }); + it('calls cleanup function when an action throws', async () => { + await expect( + migrationStateActionMachine({ + initialState: { ...initialState, reason: 'the fatal reason' } as State, + logger: mockLogger.get(), + model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'FATAL']), + next: () => { + throw new Error('this action throws'); + }, + client: esClient, + }) + ).rejects.toThrow(); + + expect(cleanupMock).toHaveBeenCalledTimes(1); + }); + it('calls cleanup function when reaching the FATAL state', async () => { + await expect( + migrationStateActionMachine({ + initialState: { ...initialState, reason: 'the fatal reason' } as State, + logger: mockLogger.get(), + model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'FATAL']), + next, + client: esClient, + }) + ).rejects.toThrow(); + + expect(cleanupMock).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts index f6f13ab8fc738..c20bcdb91e98e 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts @@ -10,12 +10,13 @@ import { errors as EsErrors } from '@elastic/elasticsearch'; import * as Option from 'fp-ts/lib/Option'; import { Logger, LogMeta } from '../../logging'; import type { ElasticsearchClient } from '../../elasticsearch'; -import * as Actions from './actions'; import { CorruptSavedObjectError } from '../migrations/core/migrate_raw_docs'; import { Model, Next, stateActionMachine } from './state_action_machine'; +import { cleanup } from './migrations_state_machine_cleanup'; import { State } from './types'; -type ExecutionLog = Array< +/** @internal */ +export type ExecutionLog = Array< | { type: 'transition'; prevControlState: State['controlState']; @@ -206,18 +207,3 @@ export async function migrationStateActionMachine({ } } } - -async function cleanup(client: ElasticsearchClient, executionLog: ExecutionLog, state?: State) { - if (!state) return; - if ('sourceIndexPitId' in state) { - try { - await Actions.closePit(client, state.sourceIndexPitId)(); - } catch (e) { - executionLog.push({ - type: 'cleanup', - state, - message: e.message, - }); - } - } -} diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.mocks.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.mocks.ts new file mode 100644 index 0000000000000..29967a1f75820 --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.mocks.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +export const cleanupMock = jest.fn(); +jest.doMock('./migrations_state_machine_cleanup', () => ({ + cleanup: cleanupMock, +})); diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts new file mode 100644 index 0000000000000..1881f9a712c29 --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ElasticsearchClient } from '../../elasticsearch'; +import * as Actions from './actions'; +import type { State } from './types'; +import type { ExecutionLog } from './migrations_state_action_machine'; + +export async function cleanup( + client: ElasticsearchClient, + executionLog: ExecutionLog, + state?: State +) { + if (!state) return; + if ('sourceIndexPitId' in state) { + try { + await Actions.closePit(client, state.sourceIndexPitId)(); + } catch (e) { + executionLog.push({ + type: 'cleanup', + state, + message: e.message, + }); + } + } +} From d2dfc355d8082d2b730cf2bd5b8abf875964c22e Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 23 Apr 2021 10:07:31 +0200 Subject: [PATCH 19/27] address comments --- .../integration_tests/actions.test.ts | 74 +++++++++---------- .../server/saved_objects/migrationsv2/next.ts | 8 ++ 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 537fa32f391e3..0e1941f9070bd 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -813,10 +813,8 @@ describe('migration actions', () => { describe('openPit', () => { it('opens PointInTime for an index', async () => { - const pitResponse = (await openPit( - client, - 'existing_index_with_docs' - )()) as Either.Right; + const openPitTask = openPit(client, 'existing_index_with_docs'); + const pitResponse = (await openPitTask()) as Either.Right; expect(pitResponse.right.pitId).toEqual(expect.any(String)); @@ -836,46 +834,42 @@ describe('migration actions', () => { describe('readWithPit', () => { it('requests documents from an index using given PIT', async () => { - const pitResponse = (await openPit( - client, - 'existing_index_with_docs' - )()) as Either.Right; + const openPitTask = openPit(client, 'existing_index_with_docs'); + const pitResponse = (await openPitTask()) as Either.Right; - const docsResponse = (await readWithPit( + const readWithPitTask = readWithPit( client, pitResponse.right.pitId, Option.none, 1000, undefined - )()) as Either.Right; + ); + const docsResponse = (await readWithPitTask()) as Either.Right; await expect(docsResponse.right.outdatedDocuments.length).toBe(5); }); it('requests the batchSize of documents from an index', async () => { - const pitResponse = (await openPit( - client, - 'existing_index_with_docs' - )()) as Either.Right; + const openPitTask = openPit(client, 'existing_index_with_docs'); + const pitResponse = (await openPitTask()) as Either.Right; - const docsResponse = (await readWithPit( + const readWithPitTask = readWithPit( client, pitResponse.right.pitId, Option.none, 3, undefined - )()) as Either.Right; + ); + const docsResponse = (await readWithPitTask()) as Either.Right; await expect(docsResponse.right.outdatedDocuments.length).toBe(3); }); it('excludes documents with types listed in unusedTypesToExclude', async () => { - const pitResponse = (await openPit( - client, - 'existing_index_with_docs' - )()) as Either.Right; + const openPitTask = openPit(client, 'existing_index_with_docs'); + const pitResponse = (await openPitTask()) as Either.Right; - const docsResponse = (await readWithPit( + const readWithPitTask = readWithPit( client, pitResponse.right.pitId, Option.some({ @@ -896,7 +890,9 @@ describe('migration actions', () => { }), 1000, undefined - )()) as Either.Right; + ); + + const docsResponse = (await readWithPitTask()) as Either.Right; expect(docsResponse.right.outdatedDocuments.map((doc) => doc._source.title).sort()) .toMatchInlineSnapshot(` @@ -916,13 +912,10 @@ describe('migration actions', () => { describe('closePit', () => { it('closes PointInTime', async () => { - const pitResponse = (await openPit( - client, - 'existing_index_with_docs' - )()) as Either.Right; + const openPitTask = openPit(client, 'existing_index_with_docs'); + const pitResponse = (await openPitTask()) as Either.Right; const pitId = pitResponse.right.pitId; - await closePit(client, pitId)(); const searchTask = client.search({ @@ -948,23 +941,22 @@ describe('migration actions', () => { { _id: 'foo:2', _source: { type: 'dashboard', value: 2 } }, ]; - await createIndex(client, index, { + const creteIndexTask = createIndex(client, index, { dynamic: true, properties: {}, - })(); + }); + await creteIndexTask(); - const result = (await transformDocs( - client, - async function (docs) { - for (const doc of docs) { - doc._source.value += 1; - } - return docs; - }, - originalDocs, - index, - 'wait_for' - )()) as Either.Right<'bulk_index_succeeded'>; + async function tranformRawDocs(docs: SavedObjectsRawDoc[]): Promise { + for (const doc of docs) { + doc._source.value += 1; + } + return docs; + } + + const transformTask = transformDocs(client, tranformRawDocs, originalDocs, index, 'wait_for'); + + const result = (await transformTask()) as Either.Right<'bulk_index_succeeded'>; expect(result.right).toBe('bulk_index_succeeded'); diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index bc42a691f07ec..9654b5bbead7b 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -79,6 +79,14 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra transformRawDocs, state.outdatedDocuments, state.tempIndex, + /** + * Since we don't run a search against the target index, we disable "refresh" to speed up + * the migration process. + * Although any further step must run "refresh" for the target index + * before we reach out to the OUTDATED_DOCUMENTS_SEARCH step. + * Right now, we rely on UPDATE_TARGET_MAPPINGS + UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK + * to perform refresh. + */ false ), SET_TEMP_WRITE_BLOCK: (state: SetTempWriteBlock) => From 8d17ce0719ca487d519a94bef603b54a049fff90 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 23 Apr 2021 15:12:56 +0200 Subject: [PATCH 20/27] Fix functional test --- test/functional/apps/context/_discover_navigation.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index dc5d56271c7fd..1c3862e07e9d7 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -35,7 +35,10 @@ export default function ({ getService, getPageObjects }) { describe('context link in discover', () => { before(async () => { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); - await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); + await kibanaServer.uiSettings.update({ + 'doc_table:legacy': true, + defaultIndex: 'logstash-*', + }); await PageObjects.common.navigateToApp('discover'); for (const columnName of TEST_COLUMN_NAMES) { From a128d7b7c03493a06d76662902877e535a424b9a Mon Sep 17 00:00:00 2001 From: restrry Date: Sun, 25 Apr 2021 12:39:35 +0200 Subject: [PATCH 21/27] set defaultIndex before each test. otherwise it is deleted in the first test file during cleanup phase --- test/functional/apps/context/_discover_navigation.js | 2 +- test/functional/apps/context/index.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index 1c3862e07e9d7..87363210727f3 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -37,7 +37,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update({ 'doc_table:legacy': true, - defaultIndex: 'logstash-*', }); await PageObjects.common.navigateToApp('discover'); @@ -50,6 +49,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.clickFieldListPlusFilter(columnName, value); } }); + after(async () => { await kibanaServer.uiSettings.replace({}); }); diff --git a/test/functional/apps/context/index.js b/test/functional/apps/context/index.js index 245f88a337dce..52ac3e3ba5a35 100644 --- a/test/functional/apps/context/index.js +++ b/test/functional/apps/context/index.js @@ -19,10 +19,13 @@ export default function ({ getService, getPageObjects, loadTestFile }) { await browser.setWindowSize(1200, 800); await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('visualize'); - await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); await PageObjects.common.navigateToApp('discover'); }); + beforeEach(async function () { + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + }); + after(function unloadMakelogs() { return esArchiver.unload('logstash_functional'); }); From 585bdd9a3c94a84b25810d4aefecf04a5c52c445 Mon Sep 17 00:00:00 2001 From: restrry Date: Sun, 25 Apr 2021 14:00:55 +0200 Subject: [PATCH 22/27] sourceIndex: Option.some<> for consistency --- .../saved_objects/migrationsv2/model.test.ts | 46 ++++++++----------- .../saved_objects/migrationsv2/model.ts | 8 ++-- .../server/saved_objects/migrationsv2/next.ts | 2 +- .../saved_objects/migrationsv2/types.ts | 2 +- 4 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 31e3d1b59b1c4..57a7a7f2ea24a 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -301,14 +301,12 @@ describe('migrations v2 model', () => { settings: {}, }, }); - const newState = model(initState, res) as FatalState; + const newState = model(initState, res) as WaitForYellowSourceState; - expect(newState.controlState).toEqual('WAIT_FOR_YELLOW_SOURCE'); - expect(newState).toMatchObject({ - controlState: 'WAIT_FOR_YELLOW_SOURCE', - sourceIndex: '.kibana_7.invalid.0_001', - }); + expect(newState.controlState).toBe('WAIT_FOR_YELLOW_SOURCE'); + expect(newState.sourceIndex.value).toBe('.kibana_7.invalid.0_001'); }); + test('INIT -> WAIT_FOR_YELLOW_SOURCE when migrating from a v2 migrations index (>= 7.11.0)', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana_7.11.0_001': { @@ -332,15 +330,14 @@ describe('migrations v2 model', () => { }, }, res - ); + ) as WaitForYellowSourceState; - expect(newState).toMatchObject({ - controlState: 'WAIT_FOR_YELLOW_SOURCE', - sourceIndex: '.kibana_7.11.0_001', - }); + expect(newState.controlState).toBe('WAIT_FOR_YELLOW_SOURCE'); + expect(newState.sourceIndex.value).toBe('.kibana_7.11.0_001'); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + test('INIT -> WAIT_FOR_YELLOW_SOURCE when migrating from a v1 migrations index (>= 6.5 < 7.11.0)', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana_3': { @@ -351,12 +348,10 @@ describe('migrations v2 model', () => { settings: {}, }, }); - const newState = model(initState, res); + const newState = model(initState, res) as WaitForYellowSourceState; - expect(newState).toMatchObject({ - controlState: 'WAIT_FOR_YELLOW_SOURCE', - sourceIndex: '.kibana_3', - }); + expect(newState.controlState).toBe('WAIT_FOR_YELLOW_SOURCE'); + expect(newState.sourceIndex.value).toBe('.kibana_3'); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -422,12 +417,10 @@ describe('migrations v2 model', () => { versionIndex: 'my-saved-objects_7.11.0_001', }, res - ); + ) as WaitForYellowSourceState; - expect(newState).toMatchObject({ - controlState: 'WAIT_FOR_YELLOW_SOURCE', - sourceIndex: 'my-saved-objects_3', - }); + expect(newState.controlState).toBe('WAIT_FOR_YELLOW_SOURCE'); + expect(newState.sourceIndex.value).toBe('my-saved-objects_3'); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -451,12 +444,11 @@ describe('migrations v2 model', () => { versionIndex: 'my-saved-objects_7.12.0_001', }, res - ); + ) as WaitForYellowSourceState; + + expect(newState.controlState).toBe('WAIT_FOR_YELLOW_SOURCE'); + expect(newState.sourceIndex.value).toBe('my-saved-objects_7.11.0'); - expect(newState).toMatchObject({ - controlState: 'WAIT_FOR_YELLOW_SOURCE', - sourceIndex: 'my-saved-objects_7.11.0', - }); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -664,7 +656,7 @@ describe('migrations v2 model', () => { const waitForYellowSourceState: WaitForYellowSourceState = { ...baseState, controlState: 'WAIT_FOR_YELLOW_SOURCE', - sourceIndex: '.kibana_3', + sourceIndex: Option.some('.kibana_3') as Option.Some, sourceIndexMappings: mappingsWithUnknownType, }; diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 6e04e260aa5c7..1e917b986f93f 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -227,7 +227,7 @@ export const model = (currentState: State, resW: ResponseType): return { ...stateP, controlState: 'WAIT_FOR_YELLOW_SOURCE', - sourceIndex: source, + sourceIndex: Option.some(source) as Option.Some, sourceIndexMappings: indices[source].mappings, }; } else if (indices[stateP.legacyIndex] != null) { @@ -303,7 +303,7 @@ export const model = (currentState: State, resW: ResponseType): } } else if (stateP.controlState === 'LEGACY_SET_WRITE_BLOCK') { const res = resW as ExcludeRetryableEsError>; - // If the write block is sucessfully in place + // If the write block is successfully in place if (Either.isRight(res)) { return { ...stateP, controlState: 'LEGACY_CREATE_REINDEX_TARGET' }; } else if (Either.isLeft(res)) { @@ -431,14 +431,14 @@ export const model = (currentState: State, resW: ResponseType): return { ...stateP, controlState: 'SET_SOURCE_WRITE_BLOCK', - sourceIndex: Option.some(source) as Option.Some, + sourceIndex: source, targetIndex: target, targetIndexMappings: disableUnknownTypeMappingFields( stateP.targetIndexMappings, stateP.sourceIndexMappings ), versionIndexReadyActions: Option.some([ - { remove: { index: source, alias: stateP.currentAlias, must_exist: true } }, + { remove: { index: source.value, alias: stateP.currentAlias, must_exist: true } }, { add: { index: target, alias: stateP.currentAlias } }, { add: { index: target, alias: stateP.versionAlias } }, { remove_index: { index: stateP.tempIndex } }, diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 9654b5bbead7b..6d61634a6948e 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -54,7 +54,7 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra INIT: (state: InitState) => Actions.fetchIndices(client, [state.currentAlias, state.versionAlias]), WAIT_FOR_YELLOW_SOURCE: (state: WaitForYellowSourceState) => - Actions.waitForIndexStatusYellow(client, state.sourceIndex), + Actions.waitForIndexStatusYellow(client, state.sourceIndex.value), SET_SOURCE_WRITE_BLOCK: (state: SetSourceWriteBlockState) => Actions.setWriteBlock(client, state.sourceIndex.value), CREATE_NEW_TARGET: (state: CreateNewTargetState) => diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index 093e97d236a5d..b84d483cf6203 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -132,7 +132,7 @@ export type FatalState = BaseState & { export interface WaitForYellowSourceState extends BaseState { /** Wait for the source index to be yellow before requesting it. */ readonly controlState: 'WAIT_FOR_YELLOW_SOURCE'; - readonly sourceIndex: string; + readonly sourceIndex: Option.Some; readonly sourceIndexMappings: IndexMapping; } From 145244a66b7eb704fd51c97056463dceb1a7cc39 Mon Sep 17 00:00:00 2001 From: restrry Date: Sun, 25 Apr 2021 14:01:16 +0200 Subject: [PATCH 23/27] Revert "set defaultIndex before each test. otherwise it is deleted in the first test file during cleanup phase" This reverts commit a128d7b7c03493a06d76662902877e535a424b9a. --- test/functional/apps/context/_discover_navigation.js | 2 +- test/functional/apps/context/index.js | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index 87363210727f3..1c3862e07e9d7 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -37,6 +37,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update({ 'doc_table:legacy': true, + defaultIndex: 'logstash-*', }); await PageObjects.common.navigateToApp('discover'); @@ -49,7 +50,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.clickFieldListPlusFilter(columnName, value); } }); - after(async () => { await kibanaServer.uiSettings.replace({}); }); diff --git a/test/functional/apps/context/index.js b/test/functional/apps/context/index.js index 52ac3e3ba5a35..245f88a337dce 100644 --- a/test/functional/apps/context/index.js +++ b/test/functional/apps/context/index.js @@ -19,11 +19,8 @@ export default function ({ getService, getPageObjects, loadTestFile }) { await browser.setWindowSize(1200, 800); await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('visualize'); - await PageObjects.common.navigateToApp('discover'); - }); - - beforeEach(async function () { await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await PageObjects.common.navigateToApp('discover'); }); after(function unloadMakelogs() { From 44f797477ad0155f9689dbe3e260133653563f12 Mon Sep 17 00:00:00 2001 From: restrry Date: Sun, 25 Apr 2021 14:34:19 +0200 Subject: [PATCH 24/27] address comments from Pierre --- .../migrations/core/document_migrator.ts | 5 +++-- .../integration_tests/actions.test.ts | 6 +++--- .../integration_tests/rewriting_id.test.ts | 18 +++++++++--------- .../server/saved_objects/migrationsv2/model.ts | 3 ++- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index cccd38bf5cc9e..51376bfbd8a0b 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -67,7 +67,7 @@ import { SavedObjectMigrationFn, SavedObjectMigrationMap } from '../types'; import { DEFAULT_NAMESPACE_STRING } from '../../service/lib/utils'; import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; -const DEFAULT_MINIMUM_CONVERT_VERSION = '8.0.0'; +const DEFAULT_MINIMUM_CONVERT_VERSION = '7.13.0'; export type MigrateFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc; export type MigrateAndConvertFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc[]; @@ -850,7 +850,8 @@ function assertNoDowngrades( * that we can later regenerate any inbound object references to match. * * @note This is only intended to be used when single-namespace object types are converted into multi-namespace object types. + * @internal */ -function deterministicallyRegenerateObjectId(namespace: string, type: string, id: string) { +export function deterministicallyRegenerateObjectId(namespace: string, type: string, id: string) { return uuidv5(`${namespace}:${type}:${id}`, uuidv5.DNS); // the uuidv5 namespace constant (uuidv5.DNS) is arbitrary } diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 0e1941f9070bd..b31f20950ae77 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -865,7 +865,7 @@ describe('migration actions', () => { await expect(docsResponse.right.outdatedDocuments.length).toBe(3); }); - it('excludes documents with types listed in unusedTypesToExclude', async () => { + it('it excludes documents not matching the provided "unusedTypesQuery"', async () => { const openPitTask = openPit(client, 'existing_index_with_docs'); const pitResponse = (await openPitTask()) as Either.Right; @@ -941,11 +941,11 @@ describe('migration actions', () => { { _id: 'foo:2', _source: { type: 'dashboard', value: 2 } }, ]; - const creteIndexTask = createIndex(client, index, { + const createIndexTask = createIndex(client, index, { dynamic: true, properties: {}, }); - await creteIndexTask(); + await createIndexTask(); async function tranformRawDocs(docs: SavedObjectsRawDoc[]): Promise { for (const doc of docs) { diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts index a70e8ead7c61d..1b61394ba05a0 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts @@ -9,11 +9,11 @@ import Path from 'path'; import Fs from 'fs'; import Util from 'util'; -import uuidv5 from 'uuid/v5'; import { kibanaPackageJson as pkg } from '@kbn/utils'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; import type { ElasticsearchClient } from '../../../elasticsearch'; import { Root } from '../../../root'; +import { deterministicallyRegenerateObjectId } from '../../migrations/core/document_migrator'; const logFilePath = Path.join(__dirname, 'migration_test_kibana.log'); @@ -147,7 +147,7 @@ describe('migration v2', () => { hidden: false, mappings: { properties: { name: { type: 'text' } } }, namespaceType: 'multiple', - convertToMultiNamespaceTypeVersion: '8.0.0', + convertToMultiNamespaceTypeVersion: '7.13.0', }); coreSetup.savedObjects.registerType({ @@ -155,7 +155,7 @@ describe('migration v2', () => { hidden: false, mappings: { properties: { nomnom: { type: 'integer' } } }, namespaceType: 'multiple-isolated', - convertToMultiNamespaceTypeVersion: '8.0.0', + convertToMultiNamespaceTypeVersion: '7.13.0', }); const coreStart = await root.start(); @@ -165,8 +165,8 @@ describe('migration v2', () => { // 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 - const newFooId = uuidv5('spacex:foo:1', uuidv5.DNS); - const newBarId = uuidv5('spacex:bar:1', uuidv5.DNS); + const newFooId = deterministicallyRegenerateObjectId('spacex', 'foo', '1'); + const newBarId = deterministicallyRegenerateObjectId('spacex', 'bar', '1'); expect(migratedDocs).toEqual( [ @@ -176,7 +176,7 @@ describe('migration v2', () => { foo: { name: 'Foo 1 default' }, references: [], namespaces: ['default'], - migrationVersion: { foo: '8.0.0' }, + migrationVersion: { foo: '7.13.0' }, coreMigrationVersion: pkg.version, }, { @@ -186,7 +186,7 @@ describe('migration v2', () => { references: [], namespaces: ['spacex'], originId: '1', - migrationVersion: { foo: '8.0.0' }, + migrationVersion: { foo: '7.13.0' }, coreMigrationVersion: pkg.version, }, { @@ -208,7 +208,7 @@ describe('migration v2', () => { bar: { nomnom: 1 }, references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }], namespaces: ['default'], - migrationVersion: { bar: '8.0.0' }, + migrationVersion: { bar: '7.13.0' }, coreMigrationVersion: pkg.version, }, { @@ -218,7 +218,7 @@ describe('migration v2', () => { references: [{ type: 'foo', id: newFooId, name: 'Foo 1 spacex' }], namespaces: ['spacex'], originId: '1', - migrationVersion: { bar: '8.0.0' }, + migrationVersion: { bar: '7.13.0' }, coreMigrationVersion: pkg.version, }, { diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 1e917b986f93f..2097b1de88aab 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -506,8 +506,9 @@ export const model = (currentState: State, resW: ResponseType): } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { + const { sourceIndexPitId, ...state } = stateP; return { - ...stateP, + ...state, controlState: 'SET_TEMP_WRITE_BLOCK', sourceIndex: stateP.sourceIndex as Option.Some, }; From 97315b6dc2bc133bdb55a523cacd35f162eac53b Mon Sep 17 00:00:00 2001 From: restrry Date: Sun, 25 Apr 2021 16:22:56 +0200 Subject: [PATCH 25/27] fix test --- .../saved_objects/migrations/core/document_migrator.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 1cf408ea96a56..6e0f9835ccac2 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -186,7 +186,7 @@ describe('DocumentMigrator', () => { log: mockLogger, }; expect(() => new DocumentMigrator(invalidDefinition)).toThrowError( - `Invalid convertToMultiNamespaceTypeVersion for type foo. Value '3.2.4' cannot be less than '8.0.0'.` + `Invalid convertToMultiNamespaceTypeVersion for type foo. Value '3.2.4' cannot be less than '7.13.0'.` ); }); From f7b4d7df87c96e5159931b0637bec9334b3cbad7 Mon Sep 17 00:00:00 2001 From: restrry Date: Mon, 26 Apr 2021 09:05:40 +0200 Subject: [PATCH 26/27] Revert "fix test" This reverts commit 97315b6dc2bc133bdb55a523cacd35f162eac53b. --- .../saved_objects/migrations/core/document_migrator.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 6e0f9835ccac2..1cf408ea96a56 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -186,7 +186,7 @@ describe('DocumentMigrator', () => { log: mockLogger, }; expect(() => new DocumentMigrator(invalidDefinition)).toThrowError( - `Invalid convertToMultiNamespaceTypeVersion for type foo. Value '3.2.4' cannot be less than '7.13.0'.` + `Invalid convertToMultiNamespaceTypeVersion for type foo. Value '3.2.4' cannot be less than '8.0.0'.` ); }); From c6ff34bd0e68529a85592ccc8a5f585983cbd624 Mon Sep 17 00:00:00 2001 From: restrry Date: Mon, 26 Apr 2021 09:13:27 +0200 Subject: [PATCH 27/27] revert min convert version back to 8.0 --- .../migrations/core/document_migrator.ts | 2 +- .../integration_tests/rewriting_id.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 51376bfbd8a0b..8e538f6e12384 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -67,7 +67,7 @@ import { SavedObjectMigrationFn, SavedObjectMigrationMap } from '../types'; import { DEFAULT_NAMESPACE_STRING } from '../../service/lib/utils'; import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; -const DEFAULT_MINIMUM_CONVERT_VERSION = '7.13.0'; +const DEFAULT_MINIMUM_CONVERT_VERSION = '8.0.0'; export type MigrateFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc; export type MigrateAndConvertFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc[]; diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts index 1b61394ba05a0..9f7e32c49ef15 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts @@ -147,7 +147,7 @@ describe('migration v2', () => { hidden: false, mappings: { properties: { name: { type: 'text' } } }, namespaceType: 'multiple', - convertToMultiNamespaceTypeVersion: '7.13.0', + convertToMultiNamespaceTypeVersion: '8.0.0', }); coreSetup.savedObjects.registerType({ @@ -155,7 +155,7 @@ describe('migration v2', () => { hidden: false, mappings: { properties: { nomnom: { type: 'integer' } } }, namespaceType: 'multiple-isolated', - convertToMultiNamespaceTypeVersion: '7.13.0', + convertToMultiNamespaceTypeVersion: '8.0.0', }); const coreStart = await root.start(); @@ -176,7 +176,7 @@ describe('migration v2', () => { foo: { name: 'Foo 1 default' }, references: [], namespaces: ['default'], - migrationVersion: { foo: '7.13.0' }, + migrationVersion: { foo: '8.0.0' }, coreMigrationVersion: pkg.version, }, { @@ -186,7 +186,7 @@ describe('migration v2', () => { references: [], namespaces: ['spacex'], originId: '1', - migrationVersion: { foo: '7.13.0' }, + migrationVersion: { foo: '8.0.0' }, coreMigrationVersion: pkg.version, }, { @@ -208,7 +208,7 @@ describe('migration v2', () => { bar: { nomnom: 1 }, references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }], namespaces: ['default'], - migrationVersion: { bar: '7.13.0' }, + migrationVersion: { bar: '8.0.0' }, coreMigrationVersion: pkg.version, }, { @@ -218,7 +218,7 @@ describe('migration v2', () => { references: [{ type: 'foo', id: newFooId, name: 'Foo 1 spacex' }], namespaces: ['spacex'], originId: '1', - migrationVersion: { bar: '7.13.0' }, + migrationVersion: { bar: '8.0.0' }, coreMigrationVersion: pkg.version, }, {