mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Add origin conflict checks when resolving import errors (#125241)
This commit is contained in:
parent
4fe96b799e
commit
5629decd9e
12 changed files with 420 additions and 67 deletions
|
@ -13,6 +13,7 @@ import {
|
|||
SavedObjectReference,
|
||||
SavedObject,
|
||||
SavedObjectsImportFailure,
|
||||
SavedObjectsImportRetry,
|
||||
} from '../../types';
|
||||
import { checkOriginConflicts } from './check_origin_conflicts';
|
||||
import { savedObjectsClientMock } from '../../../mocks';
|
||||
|
@ -65,6 +66,7 @@ describe('#checkOriginConflicts', () => {
|
|||
ignoreRegularConflicts?: boolean;
|
||||
importStateMap?: ImportStateMap;
|
||||
pendingOverwrites?: Set<string>;
|
||||
retries?: SavedObjectsImportRetry[];
|
||||
}): CheckOriginConflictsParams => {
|
||||
savedObjectsClient = savedObjectsClientMock.create();
|
||||
find = savedObjectsClient.find;
|
||||
|
@ -122,6 +124,24 @@ describe('#checkOriginConflicts', () => {
|
|||
expect(find).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not execute searches for multi-namespace objects that have a retry with a destinationId specified', async () => {
|
||||
const objects = [multiNsObj, multiNsObjWithOriginId];
|
||||
const params = setupParams({
|
||||
objects,
|
||||
retries: [
|
||||
{ type: multiNsObj.type, id: multiNsObj.id, destinationId: 'doesnt-matter' },
|
||||
{
|
||||
type: multiNsObjWithOriginId.type,
|
||||
id: multiNsObjWithOriginId.id,
|
||||
destinationId: 'doesnt-matter',
|
||||
},
|
||||
] as SavedObjectsImportRetry[],
|
||||
});
|
||||
|
||||
await checkOriginConflicts(params);
|
||||
expect(find).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('executes searches for multi-namespace objects', async () => {
|
||||
const objects = [multiNsObj, otherObj, multiNsObjWithOriginId, otherObjWithOriginId];
|
||||
const params1 = setupParams({ objects });
|
||||
|
@ -205,6 +225,65 @@ describe('#checkOriginConflicts', () => {
|
|||
expect(checkOriginConflictsResult).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
describe('retries', () => {
|
||||
// retries are only defined when called from resolveSavedObjectsImportErrors
|
||||
test('filters inexact matches of other retries ("retryDestinations" check)', async () => {
|
||||
// obj1 and obj2 exist in this space
|
||||
// try to import obj3 and obj4; simulating a scenario where they both share an origin, but obj3 is being retried with the
|
||||
// destinationId of obj1, and obj2 is being retried without a destinationId
|
||||
const obj1 = createObject(MULTI_NS_TYPE, 'id-1', 'some-originId');
|
||||
const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'some-originId');
|
||||
const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'some-originId');
|
||||
const obj4 = createObject(MULTI_NS_TYPE, 'id-4', 'some-originId');
|
||||
const objects = [obj3, obj4];
|
||||
const params = setupParams({
|
||||
objects,
|
||||
importStateMap: new Map([
|
||||
[`${obj3.type}:${obj3.id}`, {}],
|
||||
[`${obj4.type}:${obj4.id}`, {}],
|
||||
]),
|
||||
pendingOverwrites: new Set([`${obj3.type}:${obj3.id}`]),
|
||||
retries: [
|
||||
{ type: obj3.type, id: obj3.id, destinationId: obj1.id, overwrite: true }, // if obj1 already exists, this would have had to have overwrite=true to pass the earlier call to checkConflicts without an error
|
||||
{ type: obj4.type, id: obj4.id },
|
||||
] as SavedObjectsImportRetry[],
|
||||
});
|
||||
// find is skipped for obj1 because it has a retry with a destinationId
|
||||
mockFindResult(obj1, obj2); // find for obj4: the result is an inexact match with two destinations, but obj1 is matched by obj3 -- accordingly, obj4 has an inexact match to obj2
|
||||
|
||||
const checkOriginConflictsResult = await checkOriginConflicts(params);
|
||||
const expectedResult = {
|
||||
importStateMap: new Map(),
|
||||
errors: [createConflictError(obj4, obj2.id)],
|
||||
pendingOverwrites: new Set(), // does not capture obj3 because that would have been captured in pendingOverwrites for the checkConflicts function
|
||||
};
|
||||
expect(checkOriginConflictsResult).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('does not return a conflict error when a retry has overwrite=true', async () => {
|
||||
// obj1 exists in this space
|
||||
// try to import 2; simulating a scenario where they both share an origin
|
||||
const obj1 = createObject(MULTI_NS_TYPE, 'id-1', 'some-originId');
|
||||
const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'some-originId');
|
||||
const objects = [obj2];
|
||||
const params = setupParams({
|
||||
objects,
|
||||
importStateMap: new Map([[`${obj2.type}:${obj2.id}`, {}]]),
|
||||
pendingOverwrites: new Set(), // obj2 wouldn't be included in pendingOverwrites from the earlier call to checkConflicts because obj2 doesn't exist
|
||||
retries: [{ type: obj2.type, id: obj2.id, overwrite: true }] as SavedObjectsImportRetry[],
|
||||
});
|
||||
mockFindResult(obj1); // find for obj2: the result is an inexact match with one destination -- accordingly, obj2 has an inexact match to obj1
|
||||
|
||||
const checkOriginConflictsResult = await checkOriginConflicts(params);
|
||||
const expectedResult = {
|
||||
importStateMap: new Map([[`${obj2.type}:${obj2.id}`, { destinationId: obj1.id }]]),
|
||||
errors: [],
|
||||
pendingOverwrites: new Set([`${obj2.type}:${obj2.id}`]),
|
||||
};
|
||||
expect(checkOriginConflictsResult).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('object result without a `importStateMap` entry (no match or exact match)', () => {
|
||||
test('returns object when no match is detected (0 hits)', async () => {
|
||||
// no objects exist in this space
|
||||
|
|
|
@ -8,8 +8,14 @@
|
|||
|
||||
import pMap from 'p-map';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { SavedObject, SavedObjectsClientContract, SavedObjectsImportFailure } from '../../types';
|
||||
import type {
|
||||
SavedObject,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsImportFailure,
|
||||
SavedObjectsImportRetry,
|
||||
} from '../../types';
|
||||
import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
|
||||
import { getObjectKey } from '../../service/lib/internal_utils';
|
||||
import type { ImportStateMap } from './types';
|
||||
import { createOriginQuery } from './utils';
|
||||
|
||||
|
@ -21,11 +27,17 @@ interface CheckOriginConflictsParams {
|
|||
ignoreRegularConflicts?: boolean;
|
||||
importStateMap: ImportStateMap;
|
||||
pendingOverwrites: Set<string>;
|
||||
retries?: SavedObjectsImportRetry[];
|
||||
}
|
||||
|
||||
type CheckOriginConflictParams = Omit<CheckOriginConflictsParams, 'objects' | 'importIdMap'> & {
|
||||
type CheckOriginConflictParams = Omit<
|
||||
CheckOriginConflictsParams,
|
||||
'objects' | 'importIdMap' | 'retries'
|
||||
> & {
|
||||
object: SavedObject<{ title?: string }>;
|
||||
objectIdsBeingImported: Set<string>;
|
||||
retryMap: Map<string, SavedObjectsImportRetry>;
|
||||
retryDestinations: Set<string>;
|
||||
};
|
||||
|
||||
interface InexactMatch<T> {
|
||||
|
@ -64,7 +76,7 @@ const transformObjectsToAmbiguousConflictFields = (
|
|||
return a.id < b.id ? -1 : 1; // ascending
|
||||
});
|
||||
const getAmbiguousConflictSourceKey = <T>({ object }: InexactMatch<T>) =>
|
||||
`${object.type}:${object.originId || object.id}`;
|
||||
getObjectKey({ type: object.type, id: object.originId || object.id });
|
||||
|
||||
/**
|
||||
* Make a search request for an import object to check if any objects of this type that match this object's `originId` or `id` exist in the
|
||||
|
@ -85,12 +97,27 @@ const checkOriginConflict = async (
|
|||
namespace,
|
||||
objectIdsBeingImported,
|
||||
pendingOverwrites,
|
||||
retryMap,
|
||||
retryDestinations,
|
||||
} = params;
|
||||
const { type, originId, id } = object;
|
||||
|
||||
if (!typeRegistry.isMultiNamespace(type) || pendingOverwrites.has(`${type}:${id}`)) {
|
||||
const key = getObjectKey(object);
|
||||
const retry = retryMap.get(key);
|
||||
if (
|
||||
!typeRegistry.isMultiNamespace(type) ||
|
||||
pendingOverwrites.has(key) ||
|
||||
!!retry?.destinationId
|
||||
) {
|
||||
// Skip the search request for non-multi-namespace types, since by definition they cannot have inexact matches or ambiguous conflicts.
|
||||
// Also skip the search request for objects that we've already determined have an "exact match" conflict.
|
||||
// Finally, skip the search request for objects that have specified a destinationId for a retry.
|
||||
|
||||
// The checkConflicts function is always called before this one. There are three situations where a retry would have a destinationId:
|
||||
// 1. retry with overwrite=false, where the object already exists -> checkConflicts would return a conflict error
|
||||
// 2. retry with overwrite=true, where the object already exists -> checkConflicts would add an entry to pendingOverwrites
|
||||
// 3. retry where the object *doesn't* exist -> checkConflicts wouldn't return an error _or_ add an entry to pendingOverwrites
|
||||
// Scenario (3) is why we check to see if there is a retry destinationId and skip the origin check in that case.
|
||||
return { tag: 'right', value: object };
|
||||
}
|
||||
|
||||
|
@ -111,9 +138,10 @@ const checkOriginConflict = async (
|
|||
return { tag: 'right', value: object };
|
||||
}
|
||||
// This is an "inexact match" so far; filter the conflict destination(s) to exclude any that exactly match other objects we are importing.
|
||||
const objects = savedObjects.filter(
|
||||
(obj) => !objectIdsBeingImported.has(`${obj.type}:${obj.id}`)
|
||||
);
|
||||
const objects = savedObjects.filter((obj) => {
|
||||
const destKey = getObjectKey(obj);
|
||||
return !objectIdsBeingImported.has(destKey) && !retryDestinations.has(destKey);
|
||||
});
|
||||
const destinations = transformObjectsToAmbiguousConflictFields(objects);
|
||||
if (destinations.length === 0) {
|
||||
// No conflict destinations remain after filtering, so this is a "no match" result.
|
||||
|
@ -140,16 +168,30 @@ const checkOriginConflict = async (
|
|||
* will allow `createSavedObjects` to modify the ID before creating the object (thus ensuring a conflict during).
|
||||
* B. Otherwise, this is an "ambiguous conflict" result; return an error.
|
||||
*/
|
||||
export async function checkOriginConflicts({ objects, ...params }: CheckOriginConflictsParams) {
|
||||
export async function checkOriginConflicts({
|
||||
objects,
|
||||
retries = [],
|
||||
...params
|
||||
}: CheckOriginConflictsParams) {
|
||||
const objectIdsBeingImported = new Set<string>();
|
||||
for (const [key, { isOnlyReference }] of params.importStateMap.entries()) {
|
||||
if (!isOnlyReference) {
|
||||
objectIdsBeingImported.add(key);
|
||||
}
|
||||
}
|
||||
const retryMap = retries.reduce(
|
||||
(acc, cur) => acc.set(getObjectKey(cur), cur),
|
||||
new Map<string, SavedObjectsImportRetry>()
|
||||
);
|
||||
const retryDestinations = retries.reduce((acc, cur) => {
|
||||
if (cur.destinationId) {
|
||||
acc.add(getObjectKey({ type: cur.type, id: cur.destinationId }));
|
||||
}
|
||||
return acc;
|
||||
}, new Set<string>());
|
||||
// Check each object for possible destination conflicts, ensuring we don't too many concurrent searches running.
|
||||
const mapper = async (object: SavedObject<{ title?: string }>) =>
|
||||
checkOriginConflict({ object, objectIdsBeingImported, ...params });
|
||||
checkOriginConflict({ object, objectIdsBeingImported, retryMap, retryDestinations, ...params });
|
||||
const checkOriginConflictResults = await pMap(objects, mapper, {
|
||||
concurrency: MAX_CONCURRENT_SEARCHES,
|
||||
});
|
||||
|
@ -170,17 +212,18 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo
|
|||
if (!isLeft(result)) {
|
||||
return;
|
||||
}
|
||||
const key = getAmbiguousConflictSourceKey(result.value);
|
||||
const ambiguousConflictsSourceKey = getAmbiguousConflictSourceKey(result.value);
|
||||
const sources = transformObjectsToAmbiguousConflictFields(
|
||||
ambiguousConflictSourcesMap.get(key)!
|
||||
ambiguousConflictSourcesMap.get(ambiguousConflictsSourceKey)!
|
||||
);
|
||||
const { object, destinations } = result.value;
|
||||
const { type, id, attributes } = object;
|
||||
if (sources.length === 1 && destinations.length === 1) {
|
||||
// This is a simple "inexact match" result -- a single import object has a single destination conflict.
|
||||
if (params.ignoreRegularConflicts) {
|
||||
importStateMap.set(`${type}:${id}`, { destinationId: destinations[0].id });
|
||||
pendingOverwrites.add(`${type}:${id}`);
|
||||
const key = getObjectKey(object);
|
||||
if (params.ignoreRegularConflicts || retryMap.get(key)?.overwrite) {
|
||||
importStateMap.set(key, { destinationId: destinations[0].id });
|
||||
pendingOverwrites.add(key);
|
||||
} else {
|
||||
const { title } = attributes;
|
||||
errors.push({
|
||||
|
@ -203,7 +246,7 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo
|
|||
if (sources.length > 1) {
|
||||
// In the case of ambiguous source conflicts, don't treat them as errors; instead, regenerate the object ID and reset its origin
|
||||
// (e.g., the same outcome as if `createNewCopies` was enabled for the entire import operation).
|
||||
importStateMap.set(`${type}:${id}`, { destinationId: uuidv4(), omitOriginId: true });
|
||||
importStateMap.set(getObjectKey(object), { destinationId: uuidv4(), omitOriginId: true });
|
||||
return;
|
||||
}
|
||||
const { title } = attributes;
|
||||
|
|
|
@ -13,6 +13,7 @@ import type { collectSavedObjects } from './lib/collect_saved_objects';
|
|||
import type { regenerateIds } from './lib/regenerate_ids';
|
||||
import type { validateReferences } from './lib/validate_references';
|
||||
import type { checkConflicts } from './lib/check_conflicts';
|
||||
import type { checkOriginConflicts } from './lib/check_origin_conflicts';
|
||||
import type { getImportStateMapForRetries } from './lib/get_import_state_map_for_retries';
|
||||
import type { splitOverwrites } from './lib/split_overwrites';
|
||||
import type { createSavedObjects } from './lib/create_saved_objects';
|
||||
|
@ -55,6 +56,13 @@ jest.mock('./lib/check_conflicts', () => ({
|
|||
checkConflicts: mockCheckConflicts,
|
||||
}));
|
||||
|
||||
export const mockCheckOriginConflicts = jest.fn() as jest.MockedFunction<
|
||||
typeof checkOriginConflicts
|
||||
>;
|
||||
jest.mock('./lib/check_origin_conflicts', () => ({
|
||||
checkOriginConflicts: mockCheckOriginConflicts,
|
||||
}));
|
||||
|
||||
export const mockGetImportStateMapForRetries = jest.fn() as jest.MockedFunction<
|
||||
typeof getImportStateMapForRetries
|
||||
>;
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
mockRegenerateIds,
|
||||
mockValidateReferences,
|
||||
mockCheckConflicts,
|
||||
mockCheckOriginConflicts,
|
||||
mockGetImportStateMapForRetries,
|
||||
mockSplitOverwrites,
|
||||
mockCreateSavedObjects,
|
||||
|
@ -56,7 +57,12 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
errors: [],
|
||||
filteredObjects: [],
|
||||
importStateMap: new Map(),
|
||||
pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type
|
||||
pendingOverwrites: new Set(),
|
||||
});
|
||||
mockCheckOriginConflicts.mockResolvedValue({
|
||||
errors: [],
|
||||
importStateMap: new Map(),
|
||||
pendingOverwrites: new Set(),
|
||||
});
|
||||
mockGetImportStateMapForRetries.mockReturnValue(new Map());
|
||||
mockSplitOverwrites.mockReturnValue({
|
||||
|
@ -304,6 +310,32 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
expect(mockCheckConflicts).toHaveBeenCalledWith(checkConflictsParams);
|
||||
});
|
||||
|
||||
test('checks origin conflicts', async () => {
|
||||
const retries = [createRetry()];
|
||||
const options = setupOptions({ retries });
|
||||
const filteredObjects = [createObject()];
|
||||
const importStateMap = new Map();
|
||||
const pendingOverwrites = new Set<string>();
|
||||
mockCheckConflicts.mockResolvedValue({
|
||||
errors: [],
|
||||
filteredObjects,
|
||||
importStateMap,
|
||||
pendingOverwrites,
|
||||
});
|
||||
|
||||
await resolveSavedObjectsImportErrors(options);
|
||||
const checkOriginConflictsParams = {
|
||||
objects: filteredObjects,
|
||||
savedObjectsClient,
|
||||
typeRegistry,
|
||||
namespace,
|
||||
importStateMap,
|
||||
pendingOverwrites,
|
||||
retries,
|
||||
};
|
||||
expect(mockCheckOriginConflicts).toHaveBeenCalledWith(checkOriginConflictsParams);
|
||||
});
|
||||
|
||||
test('gets import ID map for retries', async () => {
|
||||
const retries = [createRetry()];
|
||||
const createNewCopies = Symbol() as unknown as boolean;
|
||||
|
@ -313,7 +345,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
errors: [],
|
||||
filteredObjects,
|
||||
importStateMap: new Map(),
|
||||
pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type
|
||||
pendingOverwrites: new Set(),
|
||||
});
|
||||
|
||||
await resolveSavedObjectsImportErrors(options);
|
||||
|
@ -357,28 +389,49 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
|
||||
test('creates saved objects', async () => {
|
||||
const options = setupOptions();
|
||||
const errors = [createError(), createError(), createError()];
|
||||
const errors = [createError(), createError(), createError(), createError()];
|
||||
mockCollectSavedObjects.mockResolvedValue({
|
||||
errors: [errors[0]],
|
||||
collectedObjects: [], // doesn't matter
|
||||
importStateMap: new Map(), // doesn't matter
|
||||
importStateMap: new Map([
|
||||
['a', {}],
|
||||
['b', {}],
|
||||
['c', {}],
|
||||
['d', { isOnlyReference: true }],
|
||||
]),
|
||||
});
|
||||
mockCheckReferenceOrigins.mockResolvedValue({
|
||||
importStateMap: new Map([['d', { isOnlyReference: true, destinationId: 'newId-d' }]]),
|
||||
});
|
||||
mockValidateReferences.mockResolvedValue([errors[1]]);
|
||||
mockCheckConflicts.mockResolvedValue({
|
||||
errors: [errors[2]],
|
||||
filteredObjects: [],
|
||||
importStateMap: new Map([['foo', { destinationId: 'someId' }]]),
|
||||
pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type
|
||||
importStateMap: new Map([
|
||||
['b', { destinationId: 'newId-b2' }],
|
||||
['c', { destinationId: 'newId-c2' }],
|
||||
]),
|
||||
pendingOverwrites: new Set(),
|
||||
});
|
||||
mockCheckOriginConflicts.mockResolvedValue({
|
||||
errors: [errors[3]],
|
||||
importStateMap: new Map([['c', { destinationId: 'newId-c3' }]]),
|
||||
pendingOverwrites: new Set(),
|
||||
});
|
||||
mockGetImportStateMapForRetries.mockReturnValue(
|
||||
new Map([
|
||||
['foo', { destinationId: 'newId' }],
|
||||
['bar', { destinationId: 'anotherNewId' }],
|
||||
['a', { destinationId: 'newId-a1' }],
|
||||
['b', { destinationId: 'newId-b1' }],
|
||||
['c', { destinationId: 'newId-c1' }],
|
||||
])
|
||||
);
|
||||
|
||||
// assert that the importStateMap is correctly composed of the results from the four modules
|
||||
const importStateMap = new Map([
|
||||
['foo', { destinationId: 'someId' }],
|
||||
['bar', { destinationId: 'anotherNewId' }],
|
||||
['a', { destinationId: 'newId-a1' }],
|
||||
['b', { destinationId: 'newId-b2' }],
|
||||
['c', { destinationId: 'newId-c3' }],
|
||||
['d', { isOnlyReference: true, destinationId: 'newId-d' }],
|
||||
]);
|
||||
const objectsToOverwrite = [createObject()];
|
||||
const objectsToNotOverwrite = [createObject()];
|
||||
|
@ -421,6 +474,19 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
expect(mockRegenerateIds).toHaveBeenCalledWith(collectedObjects);
|
||||
});
|
||||
|
||||
test('does not check origin conflicts', async () => {
|
||||
const options = setupOptions({ createNewCopies: true });
|
||||
const collectedObjects = [createObject()];
|
||||
mockCollectSavedObjects.mockResolvedValue({
|
||||
errors: [],
|
||||
collectedObjects,
|
||||
importStateMap: new Map(), // doesn't matter
|
||||
});
|
||||
|
||||
await resolveSavedObjectsImportErrors(options);
|
||||
expect(mockCheckOriginConflicts).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('creates saved objects', async () => {
|
||||
const options = setupOptions({ createNewCopies: true });
|
||||
const errors = [createError(), createError(), createError()];
|
||||
|
@ -428,42 +494,42 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
errors: [errors[0]],
|
||||
collectedObjects: [], // doesn't matter
|
||||
importStateMap: new Map([
|
||||
['foo', {}],
|
||||
['bar', {}],
|
||||
['baz', {}],
|
||||
['qux', { isOnlyReference: true }],
|
||||
['a', {}],
|
||||
['b', {}],
|
||||
['c', {}],
|
||||
['d', { isOnlyReference: true }],
|
||||
]),
|
||||
});
|
||||
mockCheckReferenceOrigins.mockResolvedValue({
|
||||
importStateMap: new Map([['qux', { isOnlyReference: true, destinationId: 'newId1' }]]),
|
||||
importStateMap: new Map([['d', { isOnlyReference: true, destinationId: 'newId-d' }]]),
|
||||
});
|
||||
mockValidateReferences.mockResolvedValue([errors[1]]);
|
||||
mockRegenerateIds.mockReturnValue(
|
||||
new Map([
|
||||
['foo', { destinationId: 'randomId1' }],
|
||||
['bar', { destinationId: 'randomId2' }],
|
||||
['baz', { destinationId: 'randomId3' }],
|
||||
['a', { destinationId: 'randomId-a' }],
|
||||
['b', { destinationId: 'randomId-b' }],
|
||||
['c', { destinationId: 'randomId-c' }],
|
||||
])
|
||||
);
|
||||
mockCheckConflicts.mockResolvedValue({
|
||||
errors: [errors[2]],
|
||||
filteredObjects: [],
|
||||
importStateMap: new Map([['bar', { destinationId: 'someId' }]]),
|
||||
pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type
|
||||
importStateMap: new Map([['c', { destinationId: 'newId-c2' }]]),
|
||||
pendingOverwrites: new Set(),
|
||||
});
|
||||
mockGetImportStateMapForRetries.mockReturnValue(
|
||||
new Map([
|
||||
['bar', { destinationId: 'newId2' }], // this is overridden by the checkConflicts result
|
||||
['baz', { destinationId: 'newId3' }],
|
||||
['b', { destinationId: 'newId-b1' }],
|
||||
['c', { destinationId: 'newId-c1' }],
|
||||
])
|
||||
);
|
||||
|
||||
// assert that the importStateMap is correctly composed of the results from the five modules
|
||||
const importStateMap = new Map([
|
||||
['foo', { destinationId: 'randomId1' }],
|
||||
['bar', { destinationId: 'someId' }],
|
||||
['baz', { destinationId: 'newId3' }],
|
||||
['qux', { isOnlyReference: true, destinationId: 'newId1' }],
|
||||
['a', { destinationId: 'randomId-a' }],
|
||||
['b', { destinationId: 'newId-b1' }],
|
||||
['c', { destinationId: 'newId-c2' }],
|
||||
['d', { isOnlyReference: true, destinationId: 'newId-d' }],
|
||||
]);
|
||||
const objectsToOverwrite = [createObject()];
|
||||
const objectsToNotOverwrite = [createObject()];
|
||||
|
|
|
@ -27,6 +27,8 @@ import {
|
|||
getImportStateMapForRetries,
|
||||
checkConflicts,
|
||||
executeImportHooks,
|
||||
checkOriginConflicts,
|
||||
ImportStateMap,
|
||||
} from './lib';
|
||||
|
||||
/**
|
||||
|
@ -150,6 +152,24 @@ export async function resolveSavedObjectsImportErrors({
|
|||
};
|
||||
const checkConflictsResult = await checkConflicts(checkConflictsParams);
|
||||
errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors];
|
||||
importStateMap = new Map([...importStateMap, ...checkConflictsResult.importStateMap]);
|
||||
|
||||
let originConflictsImportStateMap: ImportStateMap = new Map();
|
||||
if (!createNewCopies) {
|
||||
// If createNewCopies is *not* enabled, check multi-namespace object types for origin conflicts in this namespace
|
||||
const checkOriginConflictsParams = {
|
||||
objects: checkConflictsResult.filteredObjects,
|
||||
savedObjectsClient,
|
||||
typeRegistry,
|
||||
namespace,
|
||||
importStateMap,
|
||||
pendingOverwrites: checkConflictsResult.pendingOverwrites,
|
||||
retries,
|
||||
};
|
||||
const checkOriginConflictsResult = await checkOriginConflicts(checkOriginConflictsParams);
|
||||
errorAccumulator = [...errorAccumulator, ...checkOriginConflictsResult.errors];
|
||||
originConflictsImportStateMap = checkOriginConflictsResult.importStateMap;
|
||||
}
|
||||
|
||||
// Check multi-namespace object types for regular conflicts and ambiguous conflicts
|
||||
const getImportStateMapForRetriesParams = {
|
||||
|
@ -161,7 +181,9 @@ export async function resolveSavedObjectsImportErrors({
|
|||
importStateMap = new Map([
|
||||
...importStateMap,
|
||||
...importStateMapForRetries,
|
||||
...checkConflictsResult.importStateMap, // this importStateMap takes precedence over the others
|
||||
// the importStateMap entries from checkConflicts and checkOriginConflicts take precedence over the others
|
||||
...checkConflictsResult.importStateMap,
|
||||
...originConflictsImportStateMap,
|
||||
]);
|
||||
|
||||
// Bulk create in two batches, overwrites and non-overwrites
|
||||
|
|
|
@ -767,3 +767,38 @@
|
|||
"type": "doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "sharedtype:outbound-missing-reference-conflict-1",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"sharedtype": {
|
||||
"title": "This is used to test if an imported object with a missing reference will have an exact match conflict when the missing reference is replaced"
|
||||
},
|
||||
"type": "sharedtype",
|
||||
"namespaces": ["*"],
|
||||
"updated_at": "2017-09-21T18:59:16.270Z"
|
||||
},
|
||||
"type": "doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "sharedtype:outbound-missing-reference-conflict-2a",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"originId": "outbound-missing-reference-conflict-2",
|
||||
"sharedtype": {
|
||||
"title": "This is used to test if an imported object with a missing reference will have an inexact match conflict when the missing reference is replaced"
|
||||
},
|
||||
"type": "sharedtype",
|
||||
"namespaces": ["*"],
|
||||
"updated_at": "2017-09-21T18:59:16.270Z"
|
||||
},
|
||||
"type": "doc"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,3 +106,20 @@ export const CONFLICT_TEST_CASES: Record<string, CommonTestCase> = Object.freeze
|
|||
expectedNamespaces: EACH_SPACE,
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* These objects exist in the test data for all saved object test suites, but they are only used to test specific scenarios.
|
||||
*/
|
||||
export const OTHER_TEST_CASES: Record<string, CommonTestCase> = Object.freeze({
|
||||
OUTBOUND_MISSING_REFERENCE_CONFLICT_1_OBJ: Object.freeze({
|
||||
type: 'sharedtype',
|
||||
id: 'outbound-missing-reference-conflict-1',
|
||||
expectedNamespaces: [ALL_SPACES_ID],
|
||||
}),
|
||||
OUTBOUND_MISSING_REFERENCE_CONFLICT_2A_OBJ: Object.freeze({
|
||||
type: 'sharedtype',
|
||||
id: 'outbound-missing-reference-conflict-2a',
|
||||
originId: 'outbound-missing-reference-conflict-2',
|
||||
expectedNamespaces: [ALL_SPACES_ID],
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { SuperTest } from 'supertest';
|
||||
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
|
||||
import {
|
||||
SAVED_OBJECT_TEST_CASES,
|
||||
CONFLICT_TEST_CASES,
|
||||
OTHER_TEST_CASES,
|
||||
} from '../lib/saved_object_test_cases';
|
||||
import { SPACES } from '../lib/spaces';
|
||||
import { expectResponses, getUrlPrefix } from '../lib/saved_object_test_utils';
|
||||
import { ExpectResponseBody, TestDefinition, TestSuite } from '../lib/types';
|
||||
|
@ -41,13 +45,11 @@ export interface ExportTestCase {
|
|||
};
|
||||
}
|
||||
|
||||
// additional sharedtype objects that exist but do not have common test cases defined
|
||||
const CID = 'conflict_';
|
||||
const CONFLICT_1_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}1` });
|
||||
const CONFLICT_2A_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}2a`, originId: `${CID}2` });
|
||||
const CONFLICT_2B_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}2b`, originId: `${CID}2` });
|
||||
const CONFLICT_3_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}3` });
|
||||
const CONFLICT_4A_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}4a`, originId: `${CID}4` });
|
||||
const CASES = {
|
||||
...SAVED_OBJECT_TEST_CASES,
|
||||
...CONFLICT_TEST_CASES,
|
||||
...OTHER_TEST_CASES,
|
||||
};
|
||||
|
||||
export const getTestCases = (spaceId?: string): { [key: string]: ExportTestCase } => ({
|
||||
singleNamespaceObject: {
|
||||
|
@ -86,10 +88,16 @@ export const getTestCases = (spaceId?: string): { [key: string]: ExportTestCase
|
|||
? [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, CASES.MULTI_NAMESPACE_ONLY_SPACE_1]
|
||||
: spaceId === SPACE_2_ID
|
||||
? [CASES.MULTI_NAMESPACE_ONLY_SPACE_2]
|
||||
: [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1]
|
||||
)
|
||||
.concat([CONFLICT_1_OBJ, CONFLICT_2A_OBJ, CONFLICT_2B_OBJ, CONFLICT_3_OBJ, CONFLICT_4A_OBJ])
|
||||
.flat(),
|
||||
: [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1]),
|
||||
...[
|
||||
CASES.CONFLICT_1_OBJ,
|
||||
CASES.CONFLICT_2A_OBJ,
|
||||
CASES.CONFLICT_2B_OBJ,
|
||||
CASES.CONFLICT_3_OBJ,
|
||||
CASES.CONFLICT_4A_OBJ,
|
||||
CASES.OUTBOUND_MISSING_REFERENCE_CONFLICT_1_OBJ,
|
||||
CASES.OUTBOUND_MISSING_REFERENCE_CONFLICT_2A_OBJ,
|
||||
],
|
||||
],
|
||||
},
|
||||
...(spaceId !== SPACE_2_ID && {
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
import expect from '@kbn/expect';
|
||||
import { SuperTest } from 'supertest';
|
||||
import querystring from 'querystring';
|
||||
import { SAVED_OBJECT_TEST_CASES, CONFLICT_TEST_CASES } from '../lib/saved_object_test_cases';
|
||||
import {
|
||||
SAVED_OBJECT_TEST_CASES,
|
||||
CONFLICT_TEST_CASES,
|
||||
OTHER_TEST_CASES,
|
||||
} from '../lib/saved_object_test_cases';
|
||||
import { SPACES, ALL_SPACES_ID } from '../lib/spaces';
|
||||
import {
|
||||
getUrlPrefix,
|
||||
|
@ -44,6 +48,7 @@ export interface FindTestCase {
|
|||
const TEST_CASES = [
|
||||
...Object.values(SAVED_OBJECT_TEST_CASES),
|
||||
...Object.values(CONFLICT_TEST_CASES),
|
||||
...Object.values(OTHER_TEST_CASES),
|
||||
];
|
||||
|
||||
export const getTestCases = (
|
||||
|
|
|
@ -37,6 +37,7 @@ export type ResolveImportErrorsTestSuite = TestSuite<ResolveImportErrorsTestDefi
|
|||
export type FailureType = 'unsupported_type' | 'conflict';
|
||||
export interface ResolveImportErrorsTestCase extends Omit<TestCase, 'failure'> {
|
||||
originId?: string;
|
||||
destinationId?: string;
|
||||
expectedNewId?: string;
|
||||
references?: SavedObjectReference[];
|
||||
replaceReferences?: SavedObjectsImportRetry['replaceReferences'];
|
||||
|
@ -61,34 +62,75 @@ export const TEST_CASES: Record<string, ResolveImportErrorsTestCase> = Object.fr
|
|||
type: 'sharedtype',
|
||||
id: `conflict_1a`,
|
||||
originId: `conflict_1`,
|
||||
destinationId: 'some-random-id',
|
||||
expectedNewId: 'some-random-id',
|
||||
}),
|
||||
CONFLICT_1B_OBJ: Object.freeze({
|
||||
type: 'sharedtype',
|
||||
id: `conflict_1b`,
|
||||
originId: `conflict_1`,
|
||||
destinationId: 'another-random-id',
|
||||
expectedNewId: 'another-random-id',
|
||||
}),
|
||||
CONFLICT_2C_OBJ: Object.freeze({
|
||||
type: 'sharedtype',
|
||||
id: `conflict_2c`,
|
||||
originId: `conflict_2`,
|
||||
destinationId: `conflict_2a`,
|
||||
expectedNewId: `conflict_2a`,
|
||||
}),
|
||||
CONFLICT_2D_OBJ: Object.freeze({
|
||||
type: 'sharedtype',
|
||||
id: `conflict_2d`,
|
||||
originId: `conflict_2`,
|
||||
// destinationId is undefined on purpose
|
||||
expectedNewId: `conflict_2b`, // since conflict_2c was matched with conflict_2a, this (conflict_2d) will result in a regular inexact match conflict with conflict_2b
|
||||
}),
|
||||
CONFLICT_3A_OBJ: Object.freeze({
|
||||
type: 'sharedtype',
|
||||
id: `conflict_3a`,
|
||||
originId: `conflict_3`,
|
||||
destinationId: `conflict_3`,
|
||||
expectedNewId: `conflict_3`,
|
||||
}),
|
||||
CONFLICT_4_OBJ: Object.freeze({
|
||||
type: 'sharedtype',
|
||||
id: `conflict_4`,
|
||||
destinationId: `conflict_4a`,
|
||||
expectedNewId: `conflict_4a`,
|
||||
}),
|
||||
});
|
||||
export const SPECIAL_TEST_CASES: Record<string, ResolveImportErrorsTestCase> = Object.freeze({
|
||||
HIDDEN,
|
||||
OUTBOUND_MISSING_REFERENCE_CONFLICT_1_OBJ: Object.freeze({
|
||||
// This object has an exact match that already exists, *and* it has a reference to an index pattern that doesn't exist.
|
||||
// We are choosing to replace the reference here, so Kibana should detect if there is a conflict and respond appropriately.
|
||||
type: 'sharedtype',
|
||||
id: 'outbound-missing-reference-conflict-1',
|
||||
references: [{ name: '1', type: 'index-pattern', id: 'missing' }],
|
||||
replaceReferences: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
from: 'missing',
|
||||
to: 'inbound-reference-origin-match-2b', // specific ID doesn't matter, just needs to be an index pattern that exists in all spaces
|
||||
},
|
||||
],
|
||||
}),
|
||||
OUTBOUND_MISSING_REFERENCE_CONFLICT_2_OBJ: Object.freeze({
|
||||
// This object has an inexact match that already exists, *and* it has a reference to an index pattern that doesn't exist.
|
||||
// We are choosing to replace the reference here, so Kibana should detect if there is a conflict and respond appropriately.
|
||||
type: 'sharedtype',
|
||||
id: 'outbound-missing-reference-conflict-2',
|
||||
expectedNewId: `outbound-missing-reference-conflict-2a`,
|
||||
references: [{ name: '1', type: 'index-pattern', id: 'missing' }],
|
||||
replaceReferences: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
from: 'missing',
|
||||
to: 'inbound-reference-origin-match-2b', // specific ID doesn't matter, just needs to be an index pattern that exists in all spaces
|
||||
},
|
||||
],
|
||||
}),
|
||||
OUTBOUND_REFERENCE_ORIGIN_MATCH_1_OBJ: Object.freeze({
|
||||
// This object does not already exist, but it has a reference to the originId of an index pattern that does exist.
|
||||
// We use index patterns because they are one of the few reference types that are validated, so the import will fail if the reference
|
||||
|
@ -125,7 +167,7 @@ const createRequest = (
|
|||
type,
|
||||
id,
|
||||
originId,
|
||||
expectedNewId,
|
||||
destinationId,
|
||||
references,
|
||||
replaceReferences,
|
||||
successParam,
|
||||
|
@ -138,7 +180,7 @@ const createRequest = (
|
|||
type,
|
||||
id,
|
||||
overwrite,
|
||||
...(expectedNewId && { destinationId: expectedNewId }),
|
||||
...(destinationId && { destinationId }),
|
||||
...(replaceReferences && { replaceReferences }),
|
||||
...(successParam === 'createNewCopy' && { createNewCopy: true }),
|
||||
},
|
||||
|
|
|
@ -31,11 +31,15 @@ const newCopy = () => ({ successParam: 'createNewCopy' });
|
|||
const createNewCopiesTestCases = () => {
|
||||
// for each outcome, if failure !== undefined then we expect to receive
|
||||
// an error; otherwise, we expect to receive a success result
|
||||
const importable = Object.entries(CASES).map(([, val]) => ({
|
||||
...val,
|
||||
successParam: 'createNewCopies',
|
||||
expectedNewId: uuidv4(),
|
||||
}));
|
||||
const importable = Object.values(CASES).map((testCase) => {
|
||||
const newId = uuidv4();
|
||||
return {
|
||||
...testCase,
|
||||
successParam: 'createNewCopies',
|
||||
destinationId: newId,
|
||||
expectedNewId: newId,
|
||||
};
|
||||
});
|
||||
const nonImportable = [{ ...SPECIAL_TEST_CASES.HIDDEN, ...failUnsupportedType() }]; // unsupported_type is an "unresolvable" error
|
||||
const all = [...importable, ...nonImportable];
|
||||
return { importable, nonImportable, all };
|
||||
|
@ -89,11 +93,21 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
|
|||
// if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that
|
||||
// `expectedDestinationId` already exists
|
||||
{ ...CASES.CONFLICT_2C_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a'
|
||||
{ ...CASES.CONFLICT_2D_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2b'
|
||||
{ ...CASES.CONFLICT_3A_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3'
|
||||
{ ...CASES.CONFLICT_4_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a'
|
||||
];
|
||||
const refOrigins = [
|
||||
// These are in a separate group because they will result in a different 403 error for users who are unauthorized to read
|
||||
{
|
||||
...SPECIAL_TEST_CASES.OUTBOUND_MISSING_REFERENCE_CONFLICT_1_OBJ,
|
||||
...failConflict(!overwrite),
|
||||
},
|
||||
{
|
||||
...SPECIAL_TEST_CASES.OUTBOUND_MISSING_REFERENCE_CONFLICT_2_OBJ,
|
||||
...failConflict(!overwrite),
|
||||
...destinationId(),
|
||||
},
|
||||
{ ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_1_OBJ },
|
||||
{ ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_2_OBJ },
|
||||
];
|
||||
|
|
|
@ -30,11 +30,15 @@ const createNewCopiesTestCases = () => {
|
|||
// for each outcome, if failureType !== undefined then we expect to receive
|
||||
// an error; otherwise, we expect to receive a success result
|
||||
return [
|
||||
...Object.entries(CASES).map(([, val]) => ({
|
||||
...val,
|
||||
successParam: 'createNewCopies',
|
||||
expectedNewId: uuidv4(),
|
||||
})),
|
||||
...Object.values(CASES).map((testCase) => {
|
||||
const newId = uuidv4();
|
||||
return {
|
||||
...testCase,
|
||||
successParam: 'createNewCopies',
|
||||
destinationId: newId,
|
||||
expectedNewId: newId,
|
||||
};
|
||||
}),
|
||||
{ ...SPECIAL_TEST_CASES.HIDDEN, ...failUnsupportedType() }, // unsupported_type is an "unresolvable" error
|
||||
// Other special test cases are excluded here for simplicity and consistency with the resolveImportErrors "spaces_and_security" test
|
||||
// suite and the import test suites.
|
||||
|
@ -86,8 +90,18 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
|
|||
// if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that
|
||||
// `expectedDestinationId` already exists
|
||||
{ ...CASES.CONFLICT_2C_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a'
|
||||
{ ...CASES.CONFLICT_2D_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2b'
|
||||
{ ...CASES.CONFLICT_3A_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3'
|
||||
{ ...CASES.CONFLICT_4_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a'
|
||||
{
|
||||
...SPECIAL_TEST_CASES.OUTBOUND_MISSING_REFERENCE_CONFLICT_1_OBJ,
|
||||
...failConflict(!overwrite),
|
||||
},
|
||||
{
|
||||
...SPECIAL_TEST_CASES.OUTBOUND_MISSING_REFERENCE_CONFLICT_2_OBJ,
|
||||
...failConflict(!overwrite),
|
||||
...destinationId(),
|
||||
},
|
||||
{ ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_1_OBJ },
|
||||
{ ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_2_OBJ },
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue