Skip to content

Commit

Permalink
[7.x] [Saved object migrations] Collect all documents that fail to tr…
Browse files Browse the repository at this point in the history
…ansform before stopping the migration (#96986) (#99713)

* [Saved object migrations] Collect all documents that fail to transform before stopping the migration (#96986)

Co-authored-by: Kibana Machine <[email protected]>

* Update src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts

Test relies on an archive with saved objects from version 8.0.0

Co-authored-by: Christiane (Tina) Heiligers <[email protected]>
  • Loading branch information
kibanamachine and TinaHeiligers authored May 10, 2021
1 parent 761dc11 commit 2487feb
Show file tree
Hide file tree
Showing 19 changed files with 1,049 additions and 173 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { set } from '@elastic/safer-lodash-set';
import _ from 'lodash';
import { SavedObjectUnsanitizedDoc } from '../../serialization';
import { DocumentMigrator } from './document_migrator';
import { TransformSavedObjectDocumentError } from './transform_saved_object_document_error';
import { loggingSystemMock } from '../../../logging/logging_system.mock';
import { SavedObjectsType } from '../../types';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
Expand Down Expand Up @@ -724,6 +725,12 @@ describe('DocumentMigrator', () => {

it('logs the original error and throws a transform error if a document transform fails', () => {
const log = mockLogger;
const failedDoc = {
id: 'smelly',
type: 'dog',
attributes: {},
migrationVersion: {},
};
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry({
Expand All @@ -737,12 +744,6 @@ describe('DocumentMigrator', () => {
log,
});
migrator.prepareMigrations();
const failedDoc = {
id: 'smelly',
type: 'dog',
attributes: {},
migrationVersion: {},
};
try {
migrator.migrate(_.cloneDeep(failedDoc));
expect('Did not throw').toEqual('But it should have!');
Expand All @@ -751,6 +752,7 @@ describe('DocumentMigrator', () => {
"Failed to transform document smelly. Transform: dog:1.2.3
Doc: {\\"id\\":\\"smelly\\",\\"type\\":\\"dog\\",\\"attributes\\":{},\\"migrationVersion\\":{}}"
`);
expect(error).toBeInstanceOf(TransformSavedObjectDocumentError);
expect(loggingSystemMock.collect(mockLoggerFactory).error[0][0]).toMatchInlineSnapshot(
`[Error: Dang diggity!]`
);
Expand Down
13 changes: 10 additions & 3 deletions src/core/server/saved_objects/migrations/core/document_migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
SavedObjectsType,
} from '../../types';
import { MigrationLogger } from './migration_logger';
import { TransformSavedObjectDocumentError } from '.';
import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { SavedObjectMigrationFn, SavedObjectMigrationMap } from '../types';
import { DEFAULT_NAMESPACE_STRING } from '../../service/lib/utils';
Expand Down Expand Up @@ -679,9 +680,15 @@ function wrapWithTry(
const failedTransform = `${type.name}:${version}`;
const failedDoc = JSON.stringify(doc);
log.error(error);

throw new Error(
`Failed to transform document ${doc?.id}. Transform: ${failedTransform}\nDoc: ${failedDoc}`
// To make debugging failed migrations easier, we add items needed to convert the
// saved object id to the full raw id (the id only contains the uuid part) and the full error itself
throw new TransformSavedObjectDocumentError(
doc.id,
doc.type,
doc.namespace,
failedTransform,
failedDoc,
error
);
}
};
Expand Down
6 changes: 6 additions & 0 deletions src/core/server/saved_objects/migrations/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ export type { MigrationResult, MigrationStatus } from './migration_coordinator';
export { createMigrationEsClient } from './migration_es_client';
export type { MigrationEsClient } from './migration_es_client';
export { excludeUnusedTypesQuery } from './elastic_index';
export { TransformSavedObjectDocumentError } from './transform_saved_object_document_error';
export type {
DocumentsTransformFailed,
DocumentsTransformSuccess,
TransformErrorObjects,
} from './migrate_raw_docs';
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@
*/

import { set } from '@elastic/safer-lodash-set';
import * as Either from 'fp-ts/lib/Either';
import _ from 'lodash';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { SavedObjectsSerializer } from '../../serialization';
import { migrateRawDocs } from './migrate_raw_docs';
import {
DocumentsTransformFailed,
DocumentsTransformSuccess,
migrateRawDocs,
migrateRawDocsSafely,
} from './migrate_raw_docs';
import { TransformSavedObjectDocumentError } from './transform_saved_object_document_error';

describe('migrateRawDocs', () => {
test('converts raw docs to saved objects', async () => {
Expand Down Expand Up @@ -120,3 +127,156 @@ describe('migrateRawDocs', () => {
).rejects.toThrowErrorMatchingInlineSnapshot(`"error during transform"`);
});
});

describe('migrateRawDocsSafely', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('converts raw docs to saved objects', async () => {
const transform = jest.fn<any, any>((doc: any) => [
set(_.cloneDeep(doc), 'attributes.name', 'HOI!'),
]);
const task = migrateRawDocsSafely(
new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
transform,
[
{ _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<DocumentsTransformSuccess>;
expect(result._tag).toEqual('Right');
expect(result.right.processedDocs).toEqual([
{
_id: 'a:b',
_source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] },
},
{
_id: 'c:d',
_source: { type: 'c', c: { name: 'HOI!' }, migrationVersion: {}, references: [] },
},
]);

const obj1 = {
id: 'b',
type: 'a',
attributes: { name: 'AAA' },
migrationVersion: {},
references: [],
};
const obj2 = {
id: 'd',
type: 'c',
attributes: { name: 'DDD' },
migrationVersion: {},
references: [],
};
expect(transform).toHaveBeenCalledTimes(2);
expect(transform).toHaveBeenNthCalledWith(1, obj1);
expect(transform).toHaveBeenNthCalledWith(2, obj2);
});

test('returns a `left` tag when encountering a corrupt saved object document', async () => {
const transform = jest.fn<any, any>((doc: any) => [
set(_.cloneDeep(doc), 'attributes.name', 'TADA'),
]);
const task = migrateRawDocsSafely(
new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
transform,
[
{ _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<DocumentsTransformFailed>;
expect(transform).toHaveBeenCalledTimes(1);
expect(result._tag).toEqual('Left');
expect(Object.keys(result.left)).toEqual(['type', 'corruptDocumentIds', 'transformErrors']);
expect(result.left.corruptDocumentIds.length).toEqual(1);
expect(result.left.transformErrors.length).toEqual(0);
});

test('handles when one document is transformed into multiple documents', async () => {
const transform = jest.fn<any, any>((doc: any) => [
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 result = (await task()) as Either.Right<DocumentsTransformSuccess>;
expect(result._tag).toEqual('Right');
expect(result.right.processedDocs).toEqual([
{
_id: 'a:b',
_source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] },
},
{
_id: 'foo:bar',
_source: { type: 'foo', foo: { name: 'baz' }, references: [] },
},
]);

const obj = {
id: 'b',
type: 'a',
attributes: { name: 'AAA' },
migrationVersion: {},
references: [],
};
expect(transform).toHaveBeenCalledTimes(1);
expect(transform).toHaveBeenCalledWith(obj);
});

test('instance of Either.left containing transform errors when the transform function throws a TransformSavedObjectDocument error', async () => {
const transform = jest.fn<any, any>((doc: any) => {
throw new TransformSavedObjectDocumentError(
`${doc.id}`,
`${doc.type}`,
`${doc.namespace}`,
`${doc.type}1.2.3`,
JSON.stringify(doc),
new Error('error during transform')
);
});
const task = migrateRawDocsSafely(
new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
transform,
[{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] // this is the raw doc
);
const result = (await task()) as Either.Left<DocumentsTransformFailed>;
expect(transform).toHaveBeenCalledTimes(1);
expect(result._tag).toEqual('Left');
expect(result.left.corruptDocumentIds.length).toEqual(0);
expect(result.left.transformErrors.length).toEqual(1);
expect(result.left.transformErrors[0].err.message).toMatchInlineSnapshot(`
"Failed to transform document b. Transform: a1.2.3
Doc: {\\"type\\":\\"a\\",\\"id\\":\\"b\\",\\"attributes\\":{\\"name\\":\\"AAA\\"},\\"references\\":[],\\"migrationVersion\\":{}}"
`);
});

test("instance of Either.left containing errors when the transform function throws an error that isn't a TransformSavedObjectDocument error", async () => {
const transform = jest.fn<any, any>((doc: any) => {
throw new Error('error during transform');
});
const task = migrateRawDocsSafely(
new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
transform,
[{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] // this is the raw doc
);
const result = (await task()) as Either.Left<DocumentsTransformFailed>;
expect(transform).toHaveBeenCalledTimes(1);
expect(result._tag).toEqual('Left');
expect(result.left.corruptDocumentIds.length).toEqual(0);
expect(result.left.transformErrors.length).toEqual(1);
expect(result.left.transformErrors[0]).toMatchInlineSnapshot(`
Object {
"err": [Error: error during transform],
"rawId": "a:b",
}
`);
});
});
Loading

0 comments on commit 2487feb

Please sign in to comment.