Skip to content

Commit

Permalink
SO migrations: Improves transformation error creation and testing (el…
Browse files Browse the repository at this point in the history
  • Loading branch information
TinaHeiligers authored and yctercero committed May 25, 2021
1 parent 65e466c commit 01ab915
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 143 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -748,14 +748,8 @@ describe('DocumentMigrator', () => {
migrator.migrate(_.cloneDeep(failedDoc));
expect('Did not throw').toEqual('But it should have!');
} catch (error) {
expect(error.message).toMatchInlineSnapshot(`
"Failed to transform document smelly. Transform: dog:1.2.3
Doc: {\\"id\\":\\"smelly\\",\\"type\\":\\"dog\\",\\"attributes\\":{},\\"migrationVersion\\":{}}"
`);
expect(error.message).toBe('Dang diggity!');
expect(error).toBeInstanceOf(TransformSavedObjectDocumentError);
expect(loggingSystemMock.collect(mockLoggerFactory).error[0][0]).toMatchInlineSnapshot(
`[Error: Dang diggity!]`
);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -679,19 +679,8 @@ function wrapWithTry(

return { transformedDoc: result, additionalDocs: [] };
} catch (error) {
const failedTransform = `${type.name}:${version}`;
const failedDoc = JSON.stringify(doc);
log.error(error);
// 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
);
throw new TransformSavedObjectDocumentError(error);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,34 +233,7 @@ describe('migrateRawDocsSafely', () => {

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');
throw new TransformSavedObjectDocumentError(new Error('error during transform'));
});
const task = migrateRawDocsSafely(
new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,10 @@ import { TransformSavedObjectDocumentError } from './transform_saved_object_docu
describe('TransformSavedObjectDocumentError', () => {
it('is a special error', () => {
const originalError = new Error('Dang diggity!');
const err = new TransformSavedObjectDocumentError(
'id',
'type',
'namespace',
'failedTransform',
'failedDoc',
originalError
);
const err = new TransformSavedObjectDocumentError(originalError);
expect(err).toBeInstanceOf(TransformSavedObjectDocumentError);
expect(err.id).toEqual('id');
expect(err.namespace).toEqual('namespace');
expect(err.stack).not.toBeNull();
});
it('constructs an special error message', () => {
const originalError = new Error('Dang diggity!');
const err = new TransformSavedObjectDocumentError(
'id',
'type',
'namespace',
'failedTransform',
'failedDoc',
originalError
);
expect(err.message).toMatchInlineSnapshot(
`
"Failed to transform document id. Transform: failedTransform
Doc: failedDoc"
`
);
});
it('handles undefined namespace', () => {
const originalError = new Error('Dang diggity!');
const err = new TransformSavedObjectDocumentError(
'id',
'type',
undefined,
'failedTransform',
'failedDoc',
originalError
);
expect(err.message).toMatchInlineSnapshot(
`
"Failed to transform document id. Transform: failedTransform
Doc: failedDoc"
`
);
expect(err.originalError).toBe(originalError);
expect(err.message).toMatchInlineSnapshot(`"Dang diggity!"`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,10 @@
/**
* Error thrown when saved object migrations encounter a transformation error.
* Transformation errors happen when a transform function throws an error for an unsanitized saved object
* The id (doc.id) reported in this error class is just the uuid part and doesn't tell users what the full elasticsearch id is.
* in order to convert the id to the serialized version further upstream using serializer.generateRawId, we need to provide the following items:
* - namespace: doc.namespace,
* - type: doc.type,
* - id: doc.id,
* The new error class helps with v2 migrations.
* For backward compatibility with v1 migrations, the error message is the same as what was previously thrown as a plain error
*/

export class TransformSavedObjectDocumentError extends Error {
constructor(
public readonly id: string,
public readonly type: string,
public readonly namespace: string | undefined,
public readonly failedTransform: string, // created by document_migrator wrapWithTry as `${type.name}:${version}`;
public readonly failedDoc: string,
public readonly originalError: Error
) {
super(`Failed to transform document ${id}. Transform: ${failedTransform}\nDoc: ${failedDoc}`);
constructor(public readonly originalError: Error) {
super(`${originalError.message}`);
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('migration v2 with corrupt saved object documents', () => {
// },
// original corrupt SO example:
// {
// id: 'bar:123'
// id: 'bar:123' // '123' etc
// type: 'foo',
// foo: {},
// migrationVersion: {
Expand Down Expand Up @@ -107,16 +107,39 @@ describe('migration v2 with corrupt saved object documents', () => {
try {
await root.start();
} catch (err) {
const corruptFooSOs = /foo:/g;
const corruptBarSOs = /bar:/g;
const corruptBazSOs = /baz:/g;
const errorMessage = err.message;
expect(
[
...err.message.matchAll(corruptFooSOs),
...err.message.matchAll(corruptBarSOs),
...err.message.matchAll(corruptBazSOs),
].length
).toEqual(16);
errorMessage.startsWith(
'Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: Corrupt saved object documents: '
)
).toBeTruthy();
expect(
errorMessage.endsWith(' To allow migrations to proceed, please delete these documents.')
).toBeTruthy();
const expectedCorruptDocIds = [
'"foo:my_name"',
'"123"',
'"456"',
'"789"',
'"foo:other_name"',
'"bar:123"',
'"baz:123"',
'"bar:345"',
'"bar:890"',
'"baz:456"',
'"baz:789"',
'"bar:other_name"',
'"baz:other_name"',
'"bar:my_name"',
'"baz:my_name"',
'"foo:123"',
'"foo:456"',
'"foo:789"',
'"foo:other"',
];
for (const corruptDocId of expectedCorruptDocIds) {
expect(errorMessage.includes(corruptDocId)).toBeTruthy();
}
}
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* 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 * as kbnTestServer from '../../../../test_helpers/kbn_server';
import { Root } from '../../../root';

const logFilePath = Path.join(__dirname, '7_13_corrupt_transform_failures_test.log');

const asyncUnlink = Util.promisify(Fs.unlink);
async function removeLogFile() {
// ignore errors if it doesn't exist
await asyncUnlink(logFilePath).catch(() => void 0);
}

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('migrates the documents to the highest version', async () => {
const { startES } = kbnTestServer.createTestServers({
adjustTimeout: (t: number) => jest.setTimeout(t),
settings: {
es: {
license: 'basic',
// example of original 'foo' SO with corrupt id:
// _id: one
// {
// foo: {
// name: 'one',
// },
// type: 'foo',
// references: [],
// migrationVersion: {
// foo: '7.13.0',
// },
// "coreMigrationVersion": "7.13.0",
// "updated_at": "2021-05-16T18:16:45.450Z"
// },

// SO that will fail transformation:
// {
// type: 'space',
// space: {},
// },
//
//
dataArchive: Path.join(
__dirname,
'archives',
'7_13_corrupt_and_transform_failures_docs.zip'
),
},
},
});

root = createRoot();

esServer = await startES();
const coreSetup = await root.setup();

coreSetup.savedObjects.registerType({
name: 'foo',
hidden: false,
mappings: {
properties: {},
},
namespaceType: 'agnostic',
migrations: {
'7.14.0': (doc) => doc,
},
});
try {
await root.start();
} catch (err) {
const errorMessage = err.message;
expect(
errorMessage.startsWith(
'Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: Corrupt saved object documents: '
)
).toBeTruthy();
expect(
errorMessage.endsWith(' To allow migrations to proceed, please delete these documents.')
).toBeTruthy();

const expectedCorruptDocIds = [
'P2SQfHkBs3dBRGh--No5',
'QGSZfHkBs3dBRGh-ANoD',
'QWSZfHkBs3dBRGh-hNob',
'QmSZfHkBs3dBRGh-w9qH',
'one',
'two',
'Q2SZfHkBs3dBRGh-9dp2',
];
for (const corruptDocId of expectedCorruptDocIds) {
expect(errorMessage.includes(corruptDocId)).toBeTruthy();
}
const expectedTransformErrorMessage =
'Transformation errors: space:default: Document "default" has property "space" which belongs to a more recent version of Kibana [6.6.0]. The last known version is [undefined]';
expect(errorMessage.includes(expectedTransformErrorMessage)).toBeTruthy();
}
});
});

function createRoot() {
return kbnTestServer.createRootWithCorePlugins(
{
migrations: {
skip: false,
enableV2: true,
batchSize: 5,
},
logging: {
appenders: {
file: {
type: 'file',
fileName: logFilePath,
layout: {
type: 'json',
},
},
},
loggers: [
{
name: 'root',
appenders: ['file'],
},
],
},
},
{
oss: true,
}
);
}
Loading

0 comments on commit 01ab915

Please sign in to comment.