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 0481e6118acb0c..c8ff79351aadb7 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 @@ -137,14 +137,15 @@ describe('migrateRawDocsSafely', () => { const transform = jest.fn((doc: any) => [ set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), ]); - const task = migrateRawDocsSafely( - new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - transform, - [ + const task = migrateRawDocsSafely({ + serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + knownTypes: new Set(['a', 'c']), + migrateDoc: transform, + rawDocs: [ { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, - ] - ); + ], + }); const result = (await task()) as Either.Right; expect(result._tag).toEqual('Right'); expect(result.right.processedDocs).toEqual([ @@ -181,14 +182,15 @@ describe('migrateRawDocsSafely', () => { const transform = jest.fn((doc: any) => [ set(_.cloneDeep(doc), 'attributes.name', 'TADA'), ]); - const task = migrateRawDocsSafely( - new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - transform, - [ + const task = migrateRawDocsSafely({ + serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + knownTypes: new Set(['a', 'c']), + migrateDoc: transform, + rawDocs: [ { _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } }, { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, - ] - ); + ], + }); const result = (await task()) as Either.Left; expect(transform).toHaveBeenCalledTimes(1); expect(result._tag).toEqual('Left'); @@ -202,11 +204,12 @@ describe('migrateRawDocsSafely', () => { set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), { id: 'bar', type: 'foo', attributes: { name: 'baz' } }, ]); - const task = migrateRawDocsSafely( - new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - transform, - [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] - ); + const task = migrateRawDocsSafely({ + serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + knownTypes: new Set(['a', 'c']), + migrateDoc: transform, + rawDocs: [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }], + }); const result = (await task()) as Either.Right; expect(result._tag).toEqual('Right'); expect(result.right.processedDocs).toEqual([ @@ -235,11 +238,12 @@ describe('migrateRawDocsSafely', () => { const transform = jest.fn((doc: any) => { throw new TransformSavedObjectDocumentError(new Error('error during transform'), '8.0.0'); }); - const task = migrateRawDocsSafely( - new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - transform, - [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] // this is the raw doc - ); + const task = migrateRawDocsSafely({ + serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + knownTypes: new Set(['a', 'c']), + migrateDoc: transform, + rawDocs: [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }], // this is the raw doc + }); const result = (await task()) as Either.Left; expect(transform).toHaveBeenCalledTimes(1); expect(result._tag).toEqual('Left'); @@ -252,4 +256,43 @@ describe('migrateRawDocsSafely', () => { } `); }); + + test('skips documents of unknown types', async () => { + const transform = jest.fn((doc: any) => [ + set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), + ]); + const task = migrateRawDocsSafely({ + serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + knownTypes: new Set(['a']), + migrateDoc: transform, + rawDocs: [ + { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, + { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, + ], + }); + + const result = (await task()) as Either.Right; + expect(result._tag).toEqual('Right'); + expect(result.right.processedDocs).toEqual([ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + { + _id: 'c:d', + // name field is not migrated on unknown type + _source: { type: 'c', c: { name: 'DDD' } }, + }, + ]); + + const obj1 = { + id: 'b', + type: 'a', + attributes: { name: 'AAA' }, + migrationVersion: {}, + references: [], + }; + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenNthCalledWith(1, obj1); + }); }); 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 461ae1df6bc3d5..65ea21a6778d5b 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 @@ -81,6 +81,13 @@ export async function migrateRawDocs( return processedDocs; } +interface MigrateRawDocsSafelyDeps { + serializer: SavedObjectsSerializer; + knownTypes: ReadonlySet; + migrateDoc: MigrateAndConvertFn; + rawDocs: SavedObjectsRawDoc[]; +} + /** * Applies the specified migration function to every saved object document provided * and converts the saved object to a raw document. @@ -88,11 +95,15 @@ export async function migrateRawDocs( * for which the transformation function failed. * @returns {TaskEither.TaskEither} */ -export function migrateRawDocsSafely( - serializer: SavedObjectsSerializer, - migrateDoc: MigrateAndConvertFn, - rawDocs: SavedObjectsRawDoc[] -): TaskEither.TaskEither { +export function migrateRawDocsSafely({ + serializer, + knownTypes, + migrateDoc, + rawDocs, +}: MigrateRawDocsSafelyDeps): TaskEither.TaskEither< + DocumentsTransformFailed, + DocumentsTransformSuccess +> { return async () => { const migrateDocNonBlocking = transformNonBlocking(migrateDoc); const processedDocs: SavedObjectsRawDoc[] = []; @@ -100,7 +111,10 @@ export function migrateRawDocsSafely( const corruptSavedObjectIds: string[] = []; const options = { namespaceTreatment: 'lax' as const }; for (const raw of rawDocs) { - if (serializer.isRawSavedObject(raw, options)) { + // Do not transform documents of unknown types + if (raw?._source?.type && !knownTypes.has(raw._source.type)) { + processedDocs.push(raw); + } else if (serializer.isRawSavedObject(raw, options)) { try { const savedObject = convertToRawAddMigrationVersion(raw, options, serializer); processedDocs.push( 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 2d0282e6d26324..572b2934e49b82 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -184,11 +184,12 @@ export class KibanaMigrator { logger: this.log, preMigrationScript: indexMap[index].script, transformRawDocs: (rawDocs: SavedObjectsRawDoc[]) => - migrateRawDocsSafely( - this.serializer, - this.documentMigrator.migrateAndConvert, - rawDocs - ), + migrateRawDocsSafely({ + serializer: this.serializer, + knownTypes: new Set(this.typeRegistry.getAllTypes().map((t) => t.name)), + migrateDoc: this.documentMigrator.migrateAndConvert, + rawDocs, + }), migrationVersionPerType: this.documentMigrator.migrationVersion, indexPrefix: index, migrationsConfig: this.soMigrationsConfig, diff --git a/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts b/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts index 62a619ef447fad..a52cb2a9229684 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts @@ -97,9 +97,12 @@ describe('checkForUnknownDocs', () => { const result = await task(); expect(Either.isRight(result)).toBe(true); + expect((result as Either.Right).right).toEqual({ + unknownDocs: [], + }); }); - it('resolves with `Either.left` when unknown docs are found', async () => { + it('resolves with `Either.right` when unknown docs are found', async () => { const client = elasticsearchClientMock.createInternalClient( elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { @@ -120,9 +123,8 @@ describe('checkForUnknownDocs', () => { const result = await task(); - expect(Either.isLeft(result)).toBe(true); - expect((result as Either.Left).left).toEqual({ - type: 'unknown_docs_found', + expect(Either.isRight(result)).toBe(true); + expect((result as Either.Right).right).toEqual({ unknownDocs: [ { id: '12', type: 'foo' }, { id: '14', type: 'bar' }, @@ -148,9 +150,8 @@ describe('checkForUnknownDocs', () => { const result = await task(); - expect(Either.isLeft(result)).toBe(true); - expect((result as Either.Left).left).toEqual({ - type: 'unknown_docs_found', + expect(Either.isRight(result)).toBe(true); + expect((result as Either.Right).right).toEqual({ unknownDocs: [{ id: '12', type: 'unknown' }], }); }); diff --git a/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts b/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts index 7cc1c26a2ea8b1..e3d72fbdf866f8 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts @@ -32,7 +32,6 @@ export interface CheckForUnknownDocsFoundDoc { /** @internal */ export interface UnknownDocsFound { - type: 'unknown_docs_found'; unknownDocs: CheckForUnknownDocsFoundDoc[]; } @@ -42,8 +41,8 @@ export const checkForUnknownDocs = ({ unusedTypesQuery, knownTypes, }: CheckForUnknownDocsParams): TaskEither.TaskEither< - RetryableEsClientError | UnknownDocsFound, - {} + RetryableEsClientError, + UnknownDocsFound > => () => { const query = createUnknownDocQuery(unusedTypesQuery, knownTypes); @@ -56,14 +55,9 @@ export const checkForUnknownDocs = ({ }) .then((response) => { const { hits } = response.body.hits; - if (hits.length) { - return Either.left({ - type: 'unknown_docs_found' as const, - unknownDocs: hits.map((hit) => ({ id: hit._id, type: hit._source?.type ?? 'unknown' })), - }); - } else { - return Either.right({}); - } + return Either.right({ + unknownDocs: hits.map((hit) => ({ id: hit._id, type: hit._source?.type ?? 'unknown' })), + }); }) .catch(catchRetryableEsClientErrors); }; diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index 8e4584970f138d..6bfcddfe1f6dec 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -80,7 +80,6 @@ export type { } from './update_and_pickup_mappings'; export { updateAndPickupMappings } from './update_and_pickup_mappings'; -import type { UnknownDocsFound } from './check_for_unknown_docs'; export type { CheckForUnknownDocsParams, UnknownDocsFound, @@ -131,7 +130,6 @@ export interface ActionErrorTypeMap { alias_not_found_exception: AliasNotFound; remove_index_not_a_concrete_index: RemoveIndexNotAConcreteIndex; documents_transform_failed: DocumentsTransformFailed; - unknown_docs_found: UnknownDocsFound; } /** diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_unknown_types.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_unknown_types.test.ts index c5e302adbe9032..52a40bbd9f8d57 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_unknown_types.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_unknown_types.test.ts @@ -7,42 +7,35 @@ */ import Path from 'path'; -import Fs from 'fs'; -import Util from 'util'; +import fs from 'fs/promises'; import { estypes } from '@elastic/elasticsearch'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; import { Root } from '../../../root'; +import JSON5 from 'json5'; +import { ElasticsearchClient } from '../../../elasticsearch'; +import { Env } from '@kbn/config'; +import { REPO_ROOT } from '@kbn/utils'; +import { getEnvOptions } from '@kbn/config/target/mocks'; +const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; const logFilePath = Path.join(__dirname, '7_13_unknown_types_test.log'); -const asyncUnlink = Util.promisify(Fs.unlink); - async function removeLogFile() { // ignore errors if it doesn't exist - await asyncUnlink(logFilePath).catch(() => void 0); + await fs.unlink(logFilePath).catch(() => void 0); } describe('migration v2', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; + let startES: () => Promise; beforeAll(async () => { await removeLogFile(); }); - afterAll(async () => { - if (root) { - await root.shutdown(); - } - if (esServer) { - await esServer.stop(); - } - - await new Promise((resolve) => setTimeout(resolve, 10000)); - }); - - it('migrates the documents to the highest version', async () => { - const { startES } = kbnTestServer.createTestServers({ + beforeEach(() => { + ({ startES } = kbnTestServer.createTestServers({ adjustTimeout: (t: number) => jest.setTimeout(t), settings: { es: { @@ -53,50 +46,157 @@ describe('migration v2', () => { dataArchive: Path.join(__dirname, 'archives', '7.13.0_with_unknown_so.zip'), }, }, - }); + })); + }); + + afterEach(async () => { + if (root) { + await root.shutdown(); + } + if (esServer) { + await esServer.stop(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }); + it('logs a warning and completes the migration with unknown docs retained', async () => { root = createRoot(); + esServer = await startES(); + await root.setup(); + await root.start(); + + const logFileContent = await fs.readFile(logFilePath, 'utf-8'); + const records = logFileContent + .split('\n') + .filter(Boolean) + .map((str) => JSON5.parse(str)); + + const unknownDocsWarningLog = records.find((rec) => + rec.message.startsWith(`[.kibana] CHECK_UNKNOWN_DOCUMENTS`) + ); + + expect( + unknownDocsWarningLog.message.startsWith( + '[.kibana] CHECK_UNKNOWN_DOCUMENTS Upgrades will fail for 8.0+ because documents were found for unknown saved ' + + 'object types. To ensure that upgrades will succeed in the future, either re-enable plugins or delete ' + + `these documents from the ".kibana_${kibanaVersion}_001" index after the current upgrade completes.` + ) + ).toBeTruthy(); + const unknownDocs = [ + { type: 'space', id: 'space:default' }, + { type: 'space', id: 'space:first' }, + { type: 'space', id: 'space:second' }, + { type: 'space', id: 'space:third' }, + { type: 'space', id: 'space:forth' }, + { type: 'space', id: 'space:fifth' }, + { type: 'space', id: 'space:sixth' }, + { type: 'foo', id: 'P2SQfHkBs3dBRGh--No5' }, + { type: 'foo', id: 'QGSZfHkBs3dBRGh-ANoD' }, + { type: 'foo', id: 'QWSZfHkBs3dBRGh-hNob' }, + ]; + + unknownDocs.forEach(({ id, type }) => { + expect(unknownDocsWarningLog.message).toEqual( + expect.stringContaining(`- "${id}" (type: "${type}")`) + ); + }); + + const client: ElasticsearchClient = esServer.es.getClient(); + const { body: response } = await client.indices.getSettings({ + index: `.kibana_${kibanaVersion}_001`, + }); + const settings = response[`.kibana_${kibanaVersion}_001`] + .settings as estypes.IndicesIndexStatePrefixedSettings; + expect(settings.index).not.toBeUndefined(); + expect(settings.index!.blocks?.write).not.toEqual('true'); + + // Ensure that documents for unknown types were preserved in target index in an unmigrated state + const spaceDocs = await fetchDocs(client, `.kibana_${kibanaVersion}_001`, 'space'); + expect(spaceDocs.map((s) => s.id)).toEqual( + expect.arrayContaining([ + 'space:default', + 'space:first', + 'space:second', + 'space:third', + 'space:forth', + 'space:fifth', + 'space:sixth', + ]) + ); + spaceDocs.forEach((d) => { + expect(d.migrationVersion.space).toEqual('6.6.0'); + expect(d.coreMigrationVersion).toEqual('7.13.0'); + }); + const fooDocs = await fetchDocs(client, `.kibana_${kibanaVersion}_001`, 'foo'); + expect(fooDocs.map((f) => f.id)).toEqual( + expect.arrayContaining([ + 'P2SQfHkBs3dBRGh--No5', + 'QGSZfHkBs3dBRGh-ANoD', + 'QWSZfHkBs3dBRGh-hNob', + ]) + ); + fooDocs.forEach((d) => { + expect(d.migrationVersion.foo).toEqual('7.13.0'); + expect(d.coreMigrationVersion).toEqual('7.13.0'); + }); + }); + + it('migrates outdated documents when types are re-enabled', async () => { + // Start kibana with foo and space types disabled + root = createRoot(); esServer = await startES(); await root.setup(); + await root.start(); - try { - await root.start(); - } catch (err) { - const errorMessage = err.message; - - expect( - errorMessage.startsWith( - 'Unable to complete saved object migrations for the [.kibana] index: Migration failed because documents ' + - 'were found for unknown saved object types. To proceed with the migration, please delete these documents from the ' + - '".kibana_7.13.0_001" index.' - ) - ).toBeTruthy(); - - const unknownDocs = [ - { type: 'space', id: 'space:default' }, - { type: 'space', id: 'space:first' }, - { type: 'space', id: 'space:second' }, - { type: 'space', id: 'space:third' }, - { type: 'space', id: 'space:forth' }, - { type: 'space', id: 'space:fifth' }, - { type: 'space', id: 'space:sixth' }, - { type: 'foo', id: 'P2SQfHkBs3dBRGh--No5' }, - { type: 'foo', id: 'QGSZfHkBs3dBRGh-ANoD' }, - { type: 'foo', id: 'QWSZfHkBs3dBRGh-hNob' }, - ]; - - unknownDocs.forEach(({ id, type }) => { - expect(errorMessage).toEqual(expect.stringContaining(`- "${id}" (type: "${type}")`)); - }); - - const client = esServer.es.getClient(); - const { body: response } = await client.indices.getSettings({ index: '.kibana_7.13.0_001' }); - const settings = response['.kibana_7.13.0_001'] - .settings as estypes.IndicesIndexStatePrefixedSettings; - expect(settings.index).not.toBeUndefined(); - expect(settings.index!.blocks?.write).not.toEqual('true'); - } + // Shutdown and start Kibana again with space type registered to ensure space docs get migrated + await root.shutdown(); + root = createRoot(); + const coreSetup = await root.setup(); + coreSetup.savedObjects.registerType({ + name: 'space', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'agnostic', + migrations: { + '6.6.0': (d) => d, + [kibanaVersion]: (d) => d, + }, + }); + await root.start(); + + const client: ElasticsearchClient = esServer.es.getClient(); + const spacesDocsMigrated = await fetchDocs(client, `.kibana_${kibanaVersion}_001`, 'space'); + expect(spacesDocsMigrated.map((s) => s.id)).toEqual( + expect.arrayContaining([ + 'space:default', + 'space:first', + 'space:second', + 'space:third', + 'space:forth', + 'space:fifth', + 'space:sixth', + ]) + ); + spacesDocsMigrated.forEach((d) => { + expect(d.migrationVersion.space).toEqual(kibanaVersion); // should be migrated + expect(d.coreMigrationVersion).toEqual(kibanaVersion); + }); + + // Make sure unmigrated foo docs are also still there in an unmigrated state + const fooDocsUnmigrated = await fetchDocs(client, `.kibana_${kibanaVersion}_001`, 'foo'); + expect(fooDocsUnmigrated.map((f) => f.id)).toEqual( + expect.arrayContaining([ + 'P2SQfHkBs3dBRGh--No5', + 'QGSZfHkBs3dBRGh-ANoD', + 'QWSZfHkBs3dBRGh-hNob', + ]) + ); + fooDocsUnmigrated.forEach((d) => { + expect(d.migrationVersion.foo).toEqual('7.13.0'); // should still not be migrated + expect(d.coreMigrationVersion).toEqual('7.13.0'); + }); }); }); @@ -131,3 +231,26 @@ function createRoot() { } ); } + +async function fetchDocs(esClient: ElasticsearchClient, index: string, type: string) { + const { body } = await esClient.search({ + index, + size: 10000, + body: { + query: { + bool: { + should: [ + { + term: { type }, + }, + ], + }, + }, + }, + }); + + return body.hits.hits.map((h) => ({ + ...h._source, + id: h._id, + })); +} 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 8443f837a7f1de..cd42d4077695e3 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 @@ -52,6 +52,8 @@ const logStateTransition = ( switch (level) { case 'error': return logger.error(logMessagePrefix + message); + case 'warning': + return logger.warn(logMessagePrefix + message); case 'info': return logger.info(logMessagePrefix + message); default: diff --git a/src/core/server/saved_objects/migrationsv2/model/extract_errors.test.ts b/src/core/server/saved_objects/migrationsv2/model/extract_errors.test.ts index a028c40ca65974..c2daadcd342ac2 100644 --- a/src/core/server/saved_objects/migrationsv2/model/extract_errors.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model/extract_errors.test.ts @@ -25,7 +25,7 @@ describe('extractUnknownDocFailureReason', () => { '.kibana_15' ) ).toMatchInlineSnapshot(` - "Migration failed because documents were found for unknown saved object types. To proceed with the migration, please delete these documents from the \\".kibana_15\\" index. + "Upgrades will fail for 8.0+ because documents were found for unknown saved object types. To ensure that upgrades will succeed in the future, either re-enable plugins or delete these documents from the \\".kibana_15\\" index after the current upgrade completes. The documents with unknown types are: - \\"unknownType:12\\" (type: \\"unknownType\\") - \\"anotherUnknownType:42\\" (type: \\"anotherUnknownType\\") diff --git a/src/core/server/saved_objects/migrationsv2/model/extract_errors.ts b/src/core/server/saved_objects/migrationsv2/model/extract_errors.ts index cc6fe7bad3ca7a..082e6344afffcb 100644 --- a/src/core/server/saved_objects/migrationsv2/model/extract_errors.ts +++ b/src/core/server/saved_objects/migrationsv2/model/extract_errors.ts @@ -38,15 +38,16 @@ export function extractTransformFailuresReason( export function extractUnknownDocFailureReason( unknownDocs: CheckForUnknownDocsFoundDoc[], - sourceIndex: string + targetIndex: string ): string { return ( - `Migration failed because documents were found for unknown saved object types. ` + - `To proceed with the migration, please delete these documents from the "${sourceIndex}" index.\n` + + `Upgrades will fail for 8.0+ because documents were found for unknown saved object types. ` + + `To ensure that upgrades will succeed in the future, either re-enable plugins or delete these documents from the ` + + `"${targetIndex}" index after the current upgrade completes.\n` + `The documents with unknown types are:\n` + unknownDocs.map((doc) => `- "${doc.id}" (type: "${doc.type}")\n`).join('') + `You can delete them using the following command:\n` + - `curl -X POST "{elasticsearch}/${sourceIndex}/_bulk?pretty" -H 'Content-Type: application/json' -d'\n` + + `curl -X POST "{elasticsearch}/${targetIndex}/_bulk?pretty" -H 'Content-Type: application/json' -d'\n` + unknownDocs.map((doc) => `{ "delete" : { "_id" : "${doc.id}" } }\n`).join('') + `'` ); diff --git a/src/core/server/saved_objects/migrationsv2/model/model.test.ts b/src/core/server/saved_objects/migrationsv2/model/model.test.ts index 136709d1b874fe..3058f586efb0cc 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model/model.test.ts @@ -715,7 +715,7 @@ describe('migrations v2 model', () => { }, } as const; - test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK if action succeeds', () => { + test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK if action succeeds and no unknown docs are found', () => { const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = { ...baseState, controlState: 'CHECK_UNKNOWN_DOCUMENTS', @@ -723,7 +723,7 @@ describe('migrations v2 model', () => { sourceIndexMappings: mappingsWithUnknownType, }; - const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({}); + const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({ unknownDocs: [] }); const newState = model(checkUnknownDocumentsSourceState, res); expect(newState.controlState).toEqual('SET_SOURCE_WRITE_BLOCK'); @@ -758,9 +758,12 @@ describe('migrations v2 model', () => { }, } `); + + // No log message gets appended + expect(newState.logs).toEqual([]); }); - test('CHECK_UNKNOWN_DOCUMENTS -> FATAL if action fails and unknown docs were found', () => { + test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK and adds log if action succeeds and unknown docs were found', () => { const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = { ...baseState, controlState: 'CHECK_UNKNOWN_DOCUMENTS', @@ -768,20 +771,51 @@ describe('migrations v2 model', () => { sourceIndexMappings: mappingsWithUnknownType, }; - const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.left({ - type: 'unknown_docs_found', + const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({ unknownDocs: [ { id: 'dashboard:12', type: 'dashboard' }, { id: 'foo:17', type: 'foo' }, ], }); const newState = model(checkUnknownDocumentsSourceState, res); - expect(newState.controlState).toEqual('FATAL'); + expect(newState.controlState).toEqual('SET_SOURCE_WRITE_BLOCK'); expect(newState).toMatchObject({ - controlState: 'FATAL', - reason: expect.stringContaining( - 'Migration failed because documents were found for unknown saved object types' + controlState: 'SET_SOURCE_WRITE_BLOCK', + sourceIndex: Option.some('.kibana_3'), + targetIndex: '.kibana_7.11.0_001', + }); + + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); + + expect(newState.logs[0]).toMatchObject({ + level: 'warning', + message: expect.stringContaining( + 'Upgrades will fail for 8.0+ because documents were found for unknown saved object types' ), }); }); diff --git a/src/core/server/saved_objects/migrationsv2/model/model.ts b/src/core/server/saved_objects/migrationsv2/model/model.ts index b28e4e30243806..a78457fa891f7b 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model/model.ts @@ -10,7 +10,7 @@ import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; import { AliasAction, isLeftTypeof } from '../actions'; -import { AllActionStates, State } from '../types'; +import { AllActionStates, MigrationLog, State } from '../types'; import type { ResponseType } from '../next'; import { disableUnknownTypeMappingFields } from '../../migrations/core/migration_context'; import { @@ -318,6 +318,7 @@ export const model = (currentState: State, resW: ResponseType): } } else if (stateP.controlState === 'CHECK_UNKNOWN_DOCUMENTS') { const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { const source = stateP.sourceIndex; const target = stateP.versionIndex; @@ -336,17 +337,24 @@ export const model = (currentState: State, resW: ResponseType): { add: { index: target, alias: stateP.versionAlias } }, { remove_index: { index: stateP.tempIndex } }, ]), + + logs: [ + ...stateP.logs, + ...(res.right.unknownDocs.length > 0 + ? ([ + { + level: 'warning', + message: `CHECK_UNKNOWN_DOCUMENTS ${extractUnknownDocFailureReason( + res.right.unknownDocs, + target + )}`, + }, + ] as MigrationLog[]) + : []), + ], }; } else { - if (isLeftTypeof(res.left, 'unknown_docs_found')) { - return { - ...stateP, - controlState: 'FATAL', - reason: extractUnknownDocFailureReason(res.left.unknownDocs, stateP.sourceIndex.value), - }; - } else { - return throwBadResponse(stateP, res.left); - } + return throwBadResponse(stateP, res); } } else if (stateP.controlState === 'SET_SOURCE_WRITE_BLOCK') { const res = resW as ExcludeRetryableEsError>; diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index ed361a710ac99f..576e3a44121844 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -19,7 +19,7 @@ import { DocumentsTransformSuccess, } from '../migrations/core/migrate_raw_docs'; -export type MigrationLogLevel = 'error' | 'info'; +export type MigrationLogLevel = 'error' | 'info' | 'warning'; export interface MigrationLog { level: MigrationLogLevel;