-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[SoMigV2] Fail fast if unknown document types are present in the sour…
…ce index (#103341) (#103733) * initial draft * fix some tests * fix additional unit tests * move all the things * create error generation fn * add correct error message * add unknown types to log message * fix types * fix existing test suites * add IT test * review comments * add tests + use unknown instead of undefined for empty types * update RFC with new step # Conflicts: # rfcs/text/0013_saved_object_migrations.md
- Loading branch information
1 parent
4720ea1
commit acf96e7
Showing
26 changed files
with
1,316 additions
and
442 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
157 changes: 157 additions & 0 deletions
157
src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 * as Either from 'fp-ts/lib/Either'; | ||
import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors'; | ||
import { errors as EsErrors, estypes } from '@elastic/elasticsearch'; | ||
import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; | ||
import { checkForUnknownDocs } from './check_for_unknown_docs'; | ||
|
||
jest.mock('./catch_retryable_es_client_errors'); | ||
|
||
describe('checkForUnknownDocs', () => { | ||
const unusedTypesQuery: estypes.QueryDslQueryContainer = { | ||
bool: { must: [{ term: { hello: 'dolly' } }] }, | ||
}; | ||
const knownTypes = ['foo', 'bar']; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('calls catchRetryableEsClientErrors when the promise rejects', async () => { | ||
// Create a mock client that rejects all methods with a 503 status code response. | ||
const retryableError = new EsErrors.ResponseError( | ||
elasticsearchClientMock.createApiResponse({ | ||
statusCode: 503, | ||
body: { error: { type: 'es_type', reason: 'es_reason' } }, | ||
}) | ||
); | ||
const client = elasticsearchClientMock.createInternalClient( | ||
elasticsearchClientMock.createErrorTransportRequestPromise(retryableError) | ||
); | ||
|
||
const task = checkForUnknownDocs({ | ||
client, | ||
indexName: '.kibana_8.0.0', | ||
knownTypes, | ||
unusedTypesQuery, | ||
}); | ||
try { | ||
await task(); | ||
} catch (e) { | ||
/** ignore */ | ||
} | ||
expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); | ||
}); | ||
|
||
it('calls `client.search` with the correct parameters', async () => { | ||
const client = elasticsearchClientMock.createInternalClient( | ||
elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { hits: [] } }) | ||
); | ||
|
||
const task = checkForUnknownDocs({ | ||
client, | ||
indexName: '.kibana_8.0.0', | ||
knownTypes, | ||
unusedTypesQuery, | ||
}); | ||
|
||
await task(); | ||
|
||
expect(client.search).toHaveBeenCalledTimes(1); | ||
expect(client.search).toHaveBeenCalledWith({ | ||
index: '.kibana_8.0.0', | ||
body: { | ||
query: { | ||
bool: { | ||
must: unusedTypesQuery, | ||
must_not: knownTypes.map((type) => ({ | ||
term: { | ||
type, | ||
}, | ||
})), | ||
}, | ||
}, | ||
}, | ||
}); | ||
}); | ||
|
||
it('resolves with `Either.right` when no unknown docs are found', async () => { | ||
const client = elasticsearchClientMock.createInternalClient( | ||
elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { hits: [] } }) | ||
); | ||
|
||
const task = checkForUnknownDocs({ | ||
client, | ||
indexName: '.kibana_8.0.0', | ||
knownTypes, | ||
unusedTypesQuery, | ||
}); | ||
|
||
const result = await task(); | ||
|
||
expect(Either.isRight(result)).toBe(true); | ||
}); | ||
|
||
it('resolves with `Either.left` when unknown docs are found', async () => { | ||
const client = elasticsearchClientMock.createInternalClient( | ||
elasticsearchClientMock.createSuccessTransportRequestPromise({ | ||
hits: { | ||
hits: [ | ||
{ _id: '12', _source: { type: 'foo' } }, | ||
{ _id: '14', _source: { type: 'bar' } }, | ||
], | ||
}, | ||
}) | ||
); | ||
|
||
const task = checkForUnknownDocs({ | ||
client, | ||
indexName: '.kibana_8.0.0', | ||
knownTypes, | ||
unusedTypesQuery, | ||
}); | ||
|
||
const result = await task(); | ||
|
||
expect(Either.isLeft(result)).toBe(true); | ||
expect((result as Either.Left<any>).left).toEqual({ | ||
type: 'unknown_docs_found', | ||
unknownDocs: [ | ||
{ id: '12', type: 'foo' }, | ||
{ id: '14', type: 'bar' }, | ||
], | ||
}); | ||
}); | ||
|
||
it('uses `unknown` as the type when the document does not contain a type field', async () => { | ||
const client = elasticsearchClientMock.createInternalClient( | ||
elasticsearchClientMock.createSuccessTransportRequestPromise({ | ||
hits: { | ||
hits: [{ _id: '12', _source: {} }], | ||
}, | ||
}) | ||
); | ||
|
||
const task = checkForUnknownDocs({ | ||
client, | ||
indexName: '.kibana_8.0.0', | ||
knownTypes, | ||
unusedTypesQuery, | ||
}); | ||
|
||
const result = await task(); | ||
|
||
expect(Either.isLeft(result)).toBe(true); | ||
expect((result as Either.Left<any>).left).toEqual({ | ||
type: 'unknown_docs_found', | ||
unknownDocs: [{ id: '12', type: 'unknown' }], | ||
}); | ||
}); | ||
}); |
85 changes: 85 additions & 0 deletions
85
src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import * as Either from 'fp-ts/lib/Either'; | ||
import * as TaskEither from 'fp-ts/lib/TaskEither'; | ||
import { estypes } from '@elastic/elasticsearch'; | ||
import type { SavedObjectsRawDocSource } from '../../serialization'; | ||
import { ElasticsearchClient } from '../../../elasticsearch'; | ||
import { | ||
catchRetryableEsClientErrors, | ||
RetryableEsClientError, | ||
} from './catch_retryable_es_client_errors'; | ||
|
||
/** @internal */ | ||
export interface CheckForUnknownDocsParams { | ||
client: ElasticsearchClient; | ||
indexName: string; | ||
unusedTypesQuery: estypes.QueryDslQueryContainer; | ||
knownTypes: string[]; | ||
} | ||
|
||
/** @internal */ | ||
export interface CheckForUnknownDocsFoundDoc { | ||
id: string; | ||
type: string; | ||
} | ||
|
||
/** @internal */ | ||
export interface UnknownDocsFound { | ||
type: 'unknown_docs_found'; | ||
unknownDocs: CheckForUnknownDocsFoundDoc[]; | ||
} | ||
|
||
export const checkForUnknownDocs = ({ | ||
client, | ||
indexName, | ||
unusedTypesQuery, | ||
knownTypes, | ||
}: CheckForUnknownDocsParams): TaskEither.TaskEither< | ||
RetryableEsClientError | UnknownDocsFound, | ||
{} | ||
> => () => { | ||
const query = createUnknownDocQuery(unusedTypesQuery, knownTypes); | ||
|
||
return client | ||
.search<SavedObjectsRawDocSource>({ | ||
index: indexName, | ||
body: { | ||
query, | ||
}, | ||
}) | ||
.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({}); | ||
} | ||
}) | ||
.catch(catchRetryableEsClientErrors); | ||
}; | ||
|
||
const createUnknownDocQuery = ( | ||
unusedTypesQuery: estypes.QueryDslQueryContainer, | ||
knownTypes: string[] | ||
): estypes.QueryDslQueryContainer => { | ||
return { | ||
bool: { | ||
must: unusedTypesQuery, | ||
must_not: knownTypes.map((type) => ({ | ||
term: { | ||
type, | ||
}, | ||
})), | ||
}, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.