[Saved Objects] Adds managed to import options (#155677)

Co-authored-by: Alejandro Fernández Haro <afharo@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christiane (Tina) Heiligers 2023-04-27 11:17:42 -07:00 committed by GitHub
parent 483edea966
commit 0858b388f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1265 additions and 86 deletions

View file

@ -293,11 +293,5 @@ export function setManaged({
optionsManaged?: boolean;
objectManaged?: boolean;
}): boolean {
if (optionsManaged !== undefined) {
return optionsManaged;
} else if (optionsManaged === undefined && objectManaged !== undefined) {
return objectManaged;
} else {
return false;
}
return optionsManaged ?? objectManaged ?? false;
}

View file

@ -59,4 +59,10 @@ export interface LegacyUrlAlias {
* created because of saved object conversion, then we will display a toast telling the user that the object has a new URL.
*/
purpose?: 'savedObjectConversion' | 'savedObjectImport';
/**
* Flag indicating if a saved object is managed by Kibana (default=false).
* Only used when upserting a saved object. If the saved object already
* exist this option has no effect.
*/
managed?: boolean;
}

View file

@ -91,6 +91,7 @@ export interface SavedObjectsImportFailure {
* If `overwrite` is specified, an attempt was made to overwrite an existing object.
*/
overwrite?: boolean;
managed?: boolean;
error:
| SavedObjectsImportConflictError
| SavedObjectsImportAmbiguousConflictError
@ -125,6 +126,14 @@ export interface SavedObjectsImportSuccess {
* If `overwrite` is specified, this object overwrote an existing one (or will do so, in the case of a pending resolution).
*/
overwrite?: boolean;
/**
* Flag indicating if a saved object is managed by Kibana (default=false)
*
* This can be leveraged by applications to e.g. prevent edits to a managed
* saved object. Instead, users can be guided to create a copy first and
* make their edits to the copy.
*/
managed?: boolean;
}
/**

View file

@ -80,10 +80,12 @@ describe('#importSavedObjectsFromStream', () => {
management: { icon: `${type}-icon` },
} as any),
importHooks = {},
managed,
}: {
createNewCopies?: boolean;
getTypeImpl?: (name: string) => any;
importHooks?: Record<string, SavedObjectsImportHook[]>;
managed?: boolean;
} = {}): ImportSavedObjectsOptions => {
readStream = new Readable();
savedObjectsClient = savedObjectsClientMock.create();
@ -98,19 +100,23 @@ describe('#importSavedObjectsFromStream', () => {
namespace,
createNewCopies,
importHooks,
managed,
};
};
const createObject = ({
type = 'foo-type',
title = 'some-title',
}: { type?: string; title?: string } = {}): SavedObject<{
managed = undefined, // explicitly declare undefined so as not set to test against existing objects
}: { type?: string; title?: string; managed?: boolean } = {}): SavedObject<{
title: string;
managed?: boolean;
}> => {
return {
type,
id: uuidv4(),
references: [],
attributes: { title },
managed,
};
};
const createError = (): SavedObjectsImportFailure => {
@ -320,6 +326,55 @@ describe('#importSavedObjectsFromStream', () => {
importStateMap,
overwrite,
namespace,
managed: options.managed,
};
expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams);
});
test('creates managed saved objects', async () => {
const options = setupOptions({ managed: true });
const collectedObjects = [createObject({ managed: true })];
const filteredObjects = [createObject({ managed: false })];
const errors = [createError(), createError(), createError(), createError()];
mockCollectSavedObjects.mockResolvedValue({
errors: [errors[0]],
collectedObjects,
importStateMap: new Map([
['foo', {}],
['bar', {}],
['baz', { isOnlyReference: true }],
]),
});
mockCheckReferenceOrigins.mockResolvedValue({
importStateMap: new Map([['baz', { isOnlyReference: true, destinationId: 'newId1' }]]),
});
mockValidateReferences.mockResolvedValue([errors[1]]);
mockCheckConflicts.mockResolvedValue({
errors: [errors[2]],
filteredObjects,
importStateMap: new Map([['foo', { destinationId: 'newId2' }]]),
pendingOverwrites: new Set(),
});
mockCheckOriginConflicts.mockResolvedValue({
errors: [errors[3]],
importStateMap: new Map([['bar', { destinationId: 'newId3' }]]),
pendingOverwrites: new Set(),
});
await importSavedObjectsFromStream(options);
const importStateMap = new Map([
['foo', { destinationId: 'newId2' }],
['bar', { destinationId: 'newId3' }],
['baz', { isOnlyReference: true, destinationId: 'newId1' }],
]);
const createSavedObjectsParams = {
objects: collectedObjects,
accumulatedErrors: errors,
savedObjectsClient,
importStateMap,
overwrite,
namespace,
managed: options.managed,
};
expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams);
});
@ -383,6 +438,118 @@ describe('#importSavedObjectsFromStream', () => {
expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams);
});
});
describe('managed option', () => {
test('if not provided, calls create without an override', async () => {
const options = setupOptions({ createNewCopies: true }); // weithout `managed` set
const collectedObjects = [
createObject({ type: 'foo', managed: true }),
createObject({ type: 'bar', title: 'bar-title', managed: false }),
];
const errors = [createError(), createError()];
mockCollectSavedObjects.mockResolvedValue({
errors: [errors[0]],
collectedObjects,
importStateMap: new Map([
['foo', {}],
['bar', { isOnlyReference: true }],
]),
});
mockCheckReferenceOrigins.mockResolvedValue({
importStateMap: new Map([['bar', { isOnlyReference: true, destinationId: 'newId' }]]),
});
mockValidateReferences.mockResolvedValue([errors[1]]);
mockRegenerateIds.mockReturnValue(new Map([['foo', { destinationId: `randomId1` }]]));
await importSavedObjectsFromStream(options);
const importStateMap: ImportStateMap = new Map([
['foo', { destinationId: `randomId1` }],
['bar', { isOnlyReference: true, destinationId: 'newId' }],
]);
const createSavedObjectsParams = {
objects: collectedObjects,
accumulatedErrors: errors,
savedObjectsClient,
importStateMap,
overwrite,
namespace,
managed: undefined,
};
expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams);
}); // assert that the call to create will not override the object props.
test('creates managed saved objects, overriding existing `managed` value', async () => {
const options = setupOptions({ createNewCopies: true, managed: true });
const collectedObjects = [createObject({ managed: false })];
const errors = [createError(), createError()];
mockCollectSavedObjects.mockResolvedValue({
errors: [errors[0]],
collectedObjects,
importStateMap: new Map([
['foo', {}],
['bar', { isOnlyReference: true }],
]),
});
mockCheckReferenceOrigins.mockResolvedValue({
importStateMap: new Map([['bar', { isOnlyReference: true, destinationId: 'newId' }]]),
});
mockValidateReferences.mockResolvedValue([errors[1]]);
mockRegenerateIds.mockReturnValue(new Map([['foo', { destinationId: `randomId1` }]]));
await importSavedObjectsFromStream(options);
// assert that the importStateMap is correctly composed of the results from the three modules
const importStateMap: ImportStateMap = new Map([
['foo', { destinationId: `randomId1` }],
['bar', { isOnlyReference: true, destinationId: 'newId' }],
]);
const createSavedObjectsParams = {
objects: collectedObjects,
accumulatedErrors: errors,
savedObjectsClient,
importStateMap,
overwrite,
namespace,
managed: true,
};
expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams);
});
test('creates and converts objects from managed to unmanaged', async () => {
const options = setupOptions({ createNewCopies: true, managed: false });
const collectedObjects = [createObject({ managed: true })];
const errors = [createError(), createError()];
mockCollectSavedObjects.mockResolvedValue({
errors: [errors[0]],
collectedObjects,
importStateMap: new Map([
['foo', {}],
['bar', { isOnlyReference: true }],
]),
});
mockCheckReferenceOrigins.mockResolvedValue({
importStateMap: new Map([['bar', { isOnlyReference: true, destinationId: 'newId' }]]),
});
mockValidateReferences.mockResolvedValue([errors[1]]);
mockRegenerateIds.mockReturnValue(new Map([['foo', { destinationId: `randomId1` }]]));
await importSavedObjectsFromStream(options);
// assert that the importStateMap is correctly composed of the results from the three modules
const importStateMap: ImportStateMap = new Map([
['foo', { destinationId: `randomId1` }],
['bar', { isOnlyReference: true, destinationId: 'newId' }],
]);
const createSavedObjectsParams = {
objects: collectedObjects,
accumulatedErrors: errors,
savedObjectsClient,
importStateMap,
overwrite,
namespace,
managed: false,
};
expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams);
});
});
});
describe('results', () => {

View file

@ -54,6 +54,10 @@ export interface ImportSavedObjectsOptions {
* different Kibana versions (e.g. generate legacy URL aliases for all imported objects that have to change IDs).
*/
compatibilityMode?: boolean;
/**
* If provided, Kibana will apply the given option to the `managed` property.
*/
managed?: boolean;
}
/**
@ -73,6 +77,7 @@ export async function importSavedObjectsFromStream({
namespace,
refresh,
compatibilityMode,
managed,
}: ImportSavedObjectsOptions): Promise<SavedObjectsImportResponse> {
let errorAccumulator: SavedObjectsImportFailure[] = [];
const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name);
@ -82,6 +87,7 @@ export async function importSavedObjectsFromStream({
readStream,
objectLimit,
supportedTypes,
managed,
});
errorAccumulator = [...errorAccumulator, ...collectSavedObjectsResult.errors];
// Map of all IDs for objects that we are attempting to import, and any references that are not included in the read stream;
@ -154,12 +160,13 @@ export async function importSavedObjectsFromStream({
namespace,
refresh,
compatibilityMode,
managed,
};
const createSavedObjectsResult = await createSavedObjects(createSavedObjectsParams);
errorAccumulator = [...errorAccumulator, ...createSavedObjectsResult.errors];
const successResults = createSavedObjectsResult.createdObjects.map((createdObject) => {
const { type, id, destinationId, originId } = createdObject;
const { type, id, destinationId, originId, managed: createdObjectManaged } = createdObject;
const getTitle = typeRegistry.getType(type)?.management?.getTitle;
const meta = {
title: getTitle ? getTitle(createdObject) : createdObject.attributes.title,
@ -170,6 +177,7 @@ export async function importSavedObjectsFromStream({
type,
id,
meta,
managed: createdObjectManaged ?? managed,
...(attemptedOverwrite && { overwrite: true }),
...(destinationId && { destinationId }),
...(destinationId && !originId && !createNewCopies && { createNewCopy: true }),

View file

@ -51,6 +51,13 @@ describe('collectSavedObjects()', () => {
attributes: { title: 'my title 2' },
references: [{ type: 'c', id: '3', name: 'c3' }],
};
const obj3 = {
type: 'bz',
id: '33',
attributes: { title: 'my title z' },
references: [{ type: 'b', id: '2', name: 'b2' }],
managed: true,
};
describe('module calls', () => {
test('limit stream with empty input stream is called with null', async () => {
@ -63,14 +70,15 @@ describe('collectSavedObjects()', () => {
});
test('limit stream with non-empty input stream is called with all objects', async () => {
const readStream = createReadStream(obj1, obj2);
const readStream = createReadStream(obj1, obj2, obj3);
const supportedTypes = [obj2.type];
await collectSavedObjects({ readStream, supportedTypes, objectLimit });
expect(createLimitStream).toHaveBeenCalledWith(objectLimit);
expect(limitStreamPush).toHaveBeenCalledTimes(3);
expect(limitStreamPush).toHaveBeenCalledTimes(4);
expect(limitStreamPush).toHaveBeenNthCalledWith(1, obj1);
expect(limitStreamPush).toHaveBeenNthCalledWith(2, obj2);
expect(limitStreamPush).toHaveBeenNthCalledWith(3, obj3);
expect(limitStreamPush).toHaveBeenLastCalledWith(null);
});
@ -82,13 +90,14 @@ describe('collectSavedObjects()', () => {
});
test('get non-unique entries with non-empty input stream is called with all entries', async () => {
const readStream = createReadStream(obj1, obj2);
const readStream = createReadStream(obj1, obj2, obj3);
const supportedTypes = [obj2.type];
await collectSavedObjects({ readStream, supportedTypes, objectLimit });
expect(getNonUniqueEntries).toHaveBeenCalledWith([
{ type: obj1.type, id: obj1.id },
{ type: obj2.type, id: obj2.id },
{ type: obj3.type, id: obj3.id },
]);
});
@ -101,7 +110,7 @@ describe('collectSavedObjects()', () => {
});
test('filter with non-empty input stream is called with all objects of supported types', async () => {
const readStream = createReadStream(obj1, obj2);
const readStream = createReadStream(obj1, obj2, obj3);
const filter = jest.fn();
const supportedTypes = [obj2.type];
await collectSavedObjects({ readStream, supportedTypes, objectLimit, filter });
@ -139,8 +148,8 @@ describe('collectSavedObjects()', () => {
const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit });
const collectedObjects = [
{ ...obj1, typeMigrationVersion: '' },
{ ...obj2, typeMigrationVersion: '' },
{ ...obj1, typeMigrationVersion: '', managed: false },
{ ...obj2, typeMigrationVersion: '', managed: false },
];
const importStateMap = new Map([
[`a:1`, {}], // a:1 is included because it is present in the collected objects
@ -166,7 +175,7 @@ describe('collectSavedObjects()', () => {
const supportedTypes = [obj1.type];
const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit });
const collectedObjects = [{ ...obj1, typeMigrationVersion: '' }];
const collectedObjects = [{ ...obj1, typeMigrationVersion: '', managed: false }];
const importStateMap = new Map([
[`a:1`, {}], // a:1 is included because it is present in the collected objects
[`b:2`, { isOnlyReference: true }], // b:2 was filtered out due to an unsupported type; b:2 is included because a:1 has a reference to b:2, but this is marked as `isOnlyReference` because b:2 is not present in the collected objects
@ -180,8 +189,8 @@ describe('collectSavedObjects()', () => {
test('keeps the original migration versions', async () => {
const collectedObjects = [
{ ...obj1, migrationVersion: { a: '1.0.0' } },
{ ...obj2, typeMigrationVersion: '2.0.0' },
{ ...obj1, migrationVersion: { a: '1.0.0' }, managed: false },
{ ...obj2, typeMigrationVersion: '2.0.0', managed: false },
];
const readStream = createReadStream(...collectedObjects);
@ -218,9 +227,10 @@ describe('collectSavedObjects()', () => {
supportedTypes,
objectLimit,
filter,
managed: false,
});
const collectedObjects = [{ ...obj2, typeMigrationVersion: '' }];
const collectedObjects = [{ ...obj2, typeMigrationVersion: '', managed: false }];
const importStateMap = new Map([
// a:1 was filtered out due to an unsupported type; a:1 is not included because there are no other references to a:1
[`b:2`, {}], // b:2 is included because it is present in the collected objects

View file

@ -25,6 +25,7 @@ interface CollectSavedObjectsOptions {
objectLimit: number;
filter?: (obj: SavedObject) => boolean;
supportedTypes: string[];
managed?: boolean;
}
export async function collectSavedObjects({
@ -32,6 +33,7 @@ export async function collectSavedObjects({
objectLimit,
filter,
supportedTypes,
managed,
}: CollectSavedObjectsOptions) {
const errors: SavedObjectsImportFailure[] = [];
const entries: Array<{ type: string; id: string }> = [];
@ -68,6 +70,9 @@ export async function collectSavedObjects({
return {
...obj,
...(!obj.migrationVersion && !obj.typeMigrationVersion ? { typeMigrationVersion: '' } : {}),
// override any managed flag on an object with that given as an option otherwise set the default to avoid having to do that with a core migration transform
// this is a bulk operation, applied to all objects being imported
...{ managed: managed ?? obj.managed ?? false },
};
}),
createConcatStream([]),

View file

@ -19,31 +19,53 @@ import {
type CreateSavedObjectsParams = Parameters<typeof createSavedObjects>[0];
interface CreateOptions {
type: string;
id: string;
originId?: string;
managed?: boolean;
}
/** Utility function to add default `managed` flag to objects that don't have one declared. */
const addManagedDefault = (objs: SavedObject[]) =>
objs.map((obj) => ({ ...obj, managed: obj.managed ?? false }));
/**
* Function to create a realistic-looking import object given a type, ID, and optional originId
*/
const createObject = (type: string, id: string, originId?: string): SavedObject => ({
const createObject = (createOptions: CreateOptions): SavedObject => {
const { type, id, originId, managed } = createOptions;
return {
type,
id,
attributes: {},
references: [
{ name: 'name-1', type: 'other-type', id: 'other-id' }, // object that is not present
{ name: 'name-2', type: MULTI_NS_TYPE, id: 'id-1' }, // object that is present, but does not have an importStateMap entry
{ name: 'name-3', type: MULTI_NS_TYPE, id: 'id-3' }, // object that is present and has an importStateMap entry
],
...(originId && { originId }),
...(managed && { managed }),
};
};
const createOptionsFrom = (type: string, id: string, originId?: string, managed?: boolean) => ({
type,
id,
attributes: {},
references: [
{ name: 'name-1', type: 'other-type', id: 'other-id' }, // object that is not present
{ name: 'name-2', type: MULTI_NS_TYPE, id: 'id-1' }, // object that is present, but does not have an importStateMap entry
{ name: 'name-3', type: MULTI_NS_TYPE, id: 'id-3' }, // object that is present and has an importStateMap entry
],
...(originId && { originId }),
originId,
managed,
});
const createLegacyUrlAliasObject = (
sourceId: string,
targetId: string,
targetType: string,
targetNamespace: string = 'default'
targetNamespace: string = 'default',
managed?: boolean
): SavedObject<LegacyUrlAlias> => ({
type: LEGACY_URL_ALIAS_TYPE,
id: `${targetNamespace}:${targetType}:${sourceId}`,
attributes: { sourceId, targetNamespace, targetType, targetId, purpose: 'savedObjectImport' },
references: [],
managed: managed ?? false,
});
const MULTI_NS_TYPE = 'multi';
@ -51,19 +73,19 @@ const OTHER_TYPE = 'other';
/**
* Create a variety of different objects to exercise different import / result scenarios
*/
const obj1 = createObject(MULTI_NS_TYPE, 'id-1', 'originId-a'); // -> success
const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-b'); // -> conflict
const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'originId-c'); // -> conflict (with known importId and omitOriginId=true)
const obj4 = createObject(MULTI_NS_TYPE, 'id-4', 'originId-d'); // -> conflict (with known importId)
const obj5 = createObject(MULTI_NS_TYPE, 'id-5', 'originId-e'); // -> unresolvable conflict
const obj6 = createObject(MULTI_NS_TYPE, 'id-6'); // -> success
const obj7 = createObject(MULTI_NS_TYPE, 'id-7'); // -> conflict
const obj8 = createObject(MULTI_NS_TYPE, 'id-8'); // -> conflict (with known importId)
const obj9 = createObject(MULTI_NS_TYPE, 'id-9'); // -> unresolvable conflict
const obj10 = createObject(OTHER_TYPE, 'id-10', 'originId-f'); // -> success
const obj11 = createObject(OTHER_TYPE, 'id-11', 'originId-g'); // -> conflict
const obj12 = createObject(OTHER_TYPE, 'id-12'); // -> success
const obj13 = createObject(OTHER_TYPE, 'id-13'); // -> conflict
const obj1 = createObject(createOptionsFrom(MULTI_NS_TYPE, 'id-1', 'originId-a', true)); // -> success
const obj2 = createObject(createOptionsFrom(MULTI_NS_TYPE, 'id-2', 'originId-b')); // -> conflict
const obj3 = createObject(createOptionsFrom(MULTI_NS_TYPE, 'id-3', 'originId-c')); // -> conflict (with known importId and omitOriginId=true)
const obj4 = createObject(createOptionsFrom(MULTI_NS_TYPE, 'id-4', 'originId-d')); // -> conflict (with known importId)
const obj5 = createObject(createOptionsFrom(MULTI_NS_TYPE, 'id-5', 'originId-e')); // -> unresolvable conflict
const obj6 = createObject(createOptionsFrom(MULTI_NS_TYPE, 'id-6', undefined, true)); // -> success
const obj7 = createObject(createOptionsFrom(MULTI_NS_TYPE, 'id-7')); // -> conflict
const obj8 = createObject(createOptionsFrom(MULTI_NS_TYPE, 'id-8')); // -> conflict (with known importId)
const obj9 = createObject(createOptionsFrom(MULTI_NS_TYPE, 'id-9')); // -> unresolvable conflict
const obj10 = createObject(createOptionsFrom(OTHER_TYPE, 'id-10', 'originId-f')); // -> success
const obj11 = createObject(createOptionsFrom(OTHER_TYPE, 'id-11', 'originId-g')); // -> conflict
const obj12 = createObject(createOptionsFrom(OTHER_TYPE, 'id-12')); // -> success
const obj13 = createObject(createOptionsFrom(OTHER_TYPE, 'id-13')); // -> conflict
// non-multi-namespace types shouldn't have origin IDs, but we include test cases to ensure it's handled gracefully
// non-multi-namespace types by definition cannot result in an unresolvable conflict, so we don't include test cases for those
const importId3 = 'id-foo';
@ -71,7 +93,7 @@ const importId4 = 'id-bar';
const importId8 = 'id-baz';
const importStateMap = new Map([
[`${obj3.type}:${obj3.id}`, { destinationId: importId3, omitOriginId: true }],
[`${obj4.type}:${obj4.id}`, { destinationId: importId4 }],
[`${obj4.type}:${obj4.id}`, { destinationId: importId4, managed: true }],
[`${obj8.type}:${obj8.id}`, { destinationId: importId8 }],
]);
@ -92,6 +114,7 @@ describe('#createSavedObjects', () => {
namespace?: string;
overwrite?: boolean;
compatibilityMode?: boolean;
managed?: boolean;
}): CreateSavedObjectsParams => {
savedObjectsClient = savedObjectsClientMock.create();
bulkCreate = savedObjectsClient.bulkCreate;
@ -99,7 +122,7 @@ describe('#createSavedObjects', () => {
};
const getExpectedBulkCreateArgsObjects = (objects: SavedObject[], retry?: boolean) =>
objects.map(({ type, id, attributes, originId }) => ({
objects.map(({ type, id, attributes, originId, managed }) => ({
type,
id: retry ? `new-id-for-${id}` : id, // if this was a retry, we regenerated the id -- this is mocked below
attributes,
@ -110,13 +133,19 @@ describe('#createSavedObjects', () => {
],
// if the import object had an originId, and/or if we regenerated the id, expect an originId to be included in the create args
...((originId || retry) && { originId: originId || id }),
...(managed && { managed }),
}));
const expectBulkCreateArgs = {
objects: (n: number, objects: SavedObject[], retry?: boolean) => {
const expectedObjects = getExpectedBulkCreateArgsObjects(objects, retry);
const expectedOptions = expect.any(Object);
expect(bulkCreate).toHaveBeenNthCalledWith(n, expectedObjects, expectedOptions);
const expectedObjectsWithManagedDefault = addManagedDefault(expectedObjects);
expect(bulkCreate).toHaveBeenNthCalledWith(
n,
expectedObjectsWithManagedDefault,
expectedOptions
);
},
legacyUrlAliases: (n: number, expectedAliasObjects: SavedObject[]) => {
const expectedOptions = expect.any(Object);
@ -131,14 +160,16 @@ describe('#createSavedObjects', () => {
const getResultMock = {
success: (
{ type, id, attributes, references, originId }: SavedObject,
{ namespace }: CreateSavedObjectsParams
{ type, id, attributes, references, originId, managed: objectManaged }: SavedObject,
{ namespace, managed }: CreateSavedObjectsParams
): SavedObject => ({
type,
id,
attributes,
references,
...(originId && { originId }),
...((managed && { managed }) ??
(objectManaged && { managed: objectManaged }) ?? { managed: false }),
version: 'some-version',
updated_at: 'some-date',
namespaces: [namespace ?? 'default'],
@ -253,7 +284,7 @@ describe('#createSavedObjects', () => {
}
});
test('calls bulkCreate when unresolvable errors or no errors are present', async () => {
test('calls bulkCreate when unresolvable errors or no errors are present with docs that have managed set', async () => {
for (const error of unresolvableErrors) {
const options = setupParams({ objects: objs, accumulatedErrors: [error] });
setupMockResults(options);
@ -269,6 +300,7 @@ describe('#createSavedObjects', () => {
test('when in compatibility mode, calls bulkCreate for legacy URL aliases when unresolvable errors or no errors are present', async () => {
for (const error of unresolvableErrors) {
// options are ok, they return objects as declared
const options = setupParams({
objects: objs,
accumulatedErrors: [error],
@ -288,8 +320,8 @@ describe('#createSavedObjects', () => {
});
});
it('filters out version from objects before create', async () => {
const options = setupParams({ objects: [{ ...obj1, version: 'foo' }] });
it('filters out version from objects before create and accepts managed', async () => {
const options = setupParams({ objects: [{ ...obj1, version: 'foo' }] }); // here optionsManaged is undefined
bulkCreate.mockResolvedValue({ saved_objects: [getResultMock.success(obj1, options)] });
await createSavedObjects(options);
@ -299,8 +331,15 @@ describe('#createSavedObjects', () => {
const testBulkCreateObjects = async ({
namespace,
compatibilityMode,
}: { namespace?: string; compatibilityMode?: boolean } = {}) => {
const options = setupParams({ objects: objs, namespace, compatibilityMode });
managed,
}: { namespace?: string; compatibilityMode?: boolean; managed?: boolean } = {}) => {
const objsWithMissingManaged = addManagedDefault(objs);
const options = setupParams({
objects: objsWithMissingManaged,
namespace,
compatibilityMode,
managed,
});
setupMockResults(options);
await createSavedObjects(options);
@ -310,7 +349,8 @@ describe('#createSavedObjects', () => {
const x4 = { ...obj4, id: importId4 }; // this import object already has an originId
const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create
const argObjs = [obj1, obj2, x3, x4, obj5, obj6, obj7, x8, obj9, obj10, obj11, obj12, obj13];
expectBulkCreateArgs.objects(1, argObjs);
const argObjsWithMissingManaged = addManagedDefault(argObjs);
expectBulkCreateArgs.objects(1, argObjsWithMissingManaged);
if (compatibilityMode) {
// Rewrite namespace in the legacy URL alias.

View file

@ -30,6 +30,14 @@ export interface CreateSavedObjectsParams<T> {
* different Kibana versions (e.g. generate legacy URL aliases for all imported objects that have to change IDs).
*/
compatibilityMode?: boolean;
/**
* If true, create the object as managed.
*
* This can be leveraged by applications to e.g. prevent edits to a managed
* saved object. Instead, users can be guided to create a copy first and
* make their edits to the copy.
*/
managed?: boolean;
}
export interface CreateSavedObjectsResult<T> {
@ -50,6 +58,7 @@ export const createSavedObjects = async <T>({
overwrite,
refresh,
compatibilityMode,
managed,
}: CreateSavedObjectsParams<T>): Promise<CreateSavedObjectsResult<T>> => {
// filter out any objects that resulted in errors
const errorSet = accumulatedErrors.reduce(
@ -96,7 +105,12 @@ export const createSavedObjects = async <T>({
...(!importStateValue.omitOriginId && { originId: originId ?? object.id }),
};
}
return { ...object, ...(references && { references }), ...(originId && { originId }) };
return {
...object,
...(references && { references }),
...(originId && { originId }),
...{ managed: managed ?? object.managed ?? false },
};
});
const resolvableErrors = ['conflict', 'ambiguous_conflict', 'missing_references'];
@ -150,6 +164,7 @@ export const createSavedObjects = async <T>({
targetId: result.id,
purpose: 'savedObjectImport',
},
...{ managed: managed ?? false }, // we can safey create each doc with the given managed flag, even if it's set as the default, bulkCreate would "override" this otherwise.
});
}
}
@ -165,7 +180,6 @@ export const createSavedObjects = async <T>({
})
).saved_objects
: [];
return {
createdObjects: remappedResults.filter((obj) => !obj.error),
errors: extractErrors(remappedResults, objects, legacyUrlAliasResults, legacyUrlAliases),

View file

@ -31,6 +31,7 @@ describe('extractErrors()', () => {
type: 'dashboard',
attributes: { title: 'My Dashboard 1' },
references: [],
managed: false,
},
{
id: '2',
@ -38,6 +39,7 @@ describe('extractErrors()', () => {
attributes: { title: 'My Dashboard 2' },
references: [],
error: SavedObjectsErrorHelpers.createConflictError('dashboard', '2').output.payload,
managed: false,
},
{
id: '3',
@ -45,6 +47,7 @@ describe('extractErrors()', () => {
attributes: { title: 'My Dashboard 3' },
references: [],
error: SavedObjectsErrorHelpers.createBadRequestError().output.payload,
managed: false,
},
{
id: '4',
@ -53,6 +56,7 @@ describe('extractErrors()', () => {
references: [],
error: SavedObjectsErrorHelpers.createConflictError('dashboard', '4').output.payload,
destinationId: 'foo',
managed: false,
},
];
const result = extractErrors(savedObjects, savedObjects, [], new Map());
@ -63,6 +67,7 @@ describe('extractErrors()', () => {
"type": "conflict",
},
"id": "2",
"managed": false,
"meta": Object {
"title": "My Dashboard 2",
},
@ -76,6 +81,7 @@ describe('extractErrors()', () => {
"type": "unknown",
},
"id": "3",
"managed": false,
"meta": Object {
"title": "My Dashboard 3",
},
@ -87,6 +93,7 @@ describe('extractErrors()', () => {
"type": "conflict",
},
"id": "4",
"managed": false,
"meta": Object {
"title": "My Dashboard 4",
},
@ -104,6 +111,7 @@ describe('extractErrors()', () => {
attributes: { title: 'My Dashboard 1' },
references: [],
destinationId: 'one',
managed: false,
},
{
id: '2',
@ -111,6 +119,7 @@ describe('extractErrors()', () => {
attributes: { title: 'My Dashboard 2' },
references: [],
error: SavedObjectsErrorHelpers.createConflictError('dashboard', '2').output.payload,
managed: false,
},
{
id: '3',
@ -118,6 +127,7 @@ describe('extractErrors()', () => {
attributes: { title: 'My Dashboard 3' },
references: [],
destinationId: 'three',
managed: false,
},
];
@ -135,6 +145,7 @@ describe('extractErrors()', () => {
purpose: 'savedObjectImport',
},
references: [],
managed: false,
},
],
[
@ -150,6 +161,7 @@ describe('extractErrors()', () => {
purpose: 'savedObjectImport',
},
references: [],
managed: false,
},
],
]);
@ -176,6 +188,7 @@ describe('extractErrors()', () => {
"type": "conflict",
},
"id": "2",
"managed": false,
"meta": Object {
"title": "My Dashboard 2",
},
@ -189,6 +202,7 @@ describe('extractErrors()', () => {
"type": "unknown",
},
"id": "default:dashboard:3",
"managed": false,
"meta": Object {
"title": "Legacy URL alias (3 -> three)",
},

View file

@ -38,6 +38,7 @@ export function extractErrors(
type: 'conflict',
...(destinationId && { destinationId }),
},
managed: savedObject.managed,
});
continue;
}
@ -49,6 +50,7 @@ export function extractErrors(
...savedObject.error,
type: 'unknown',
},
managed: savedObject.managed,
});
}
}
@ -70,6 +72,7 @@ export function extractErrors(
...legacyUrlAliasResult.error,
type: 'unknown',
},
managed: legacyUrlAlias.managed,
});
}
}

View file

@ -92,12 +92,14 @@ describe('#importSavedObjectsFromStream', () => {
management: { icon: `${type}-icon` },
} as any),
importHooks = {},
managed,
}: {
retries?: SavedObjectsImportRetry[];
createNewCopies?: boolean;
compatibilityMode?: boolean;
getTypeImpl?: (name: string) => any;
importHooks?: Record<string, SavedObjectsImportHook[]>;
managed?: boolean;
} = {}): ResolveSavedObjectsImportErrorsOptions => {
readStream = new Readable();
savedObjectsClient = savedObjectsClientMock.create();
@ -115,6 +117,7 @@ describe('#importSavedObjectsFromStream', () => {
namespace,
createNewCopies,
compatibilityMode,
managed,
};
};
@ -128,7 +131,8 @@ describe('#importSavedObjectsFromStream', () => {
};
const createObject = (
references?: SavedObjectReference[],
{ type = 'foo-type', title = 'some-title' }: { type?: string; title?: string } = {}
{ type = 'foo-type', title = 'some-title' }: { type?: string; title?: string } = {},
managed?: boolean
): SavedObject<{
title: string;
}> => {
@ -137,6 +141,7 @@ describe('#importSavedObjectsFromStream', () => {
id: uuidv4(),
references: references || [],
attributes: { title },
managed: managed ?? false, // apply the default that real createSavedObjects applies
};
};
const createError = (): SavedObjectsImportFailure => {
@ -590,6 +595,62 @@ describe('#importSavedObjectsFromStream', () => {
});
});
});
describe('with managed option', () => {
test('applies managed option to overwritten objects if specified', async () => {
const objectCreated = createObject();
const objectsToOverwrite = [{ ...objectCreated, managed: true }];
const objectsToNotOverwrite = [createObject()];
mockSplitOverwrites.mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite });
mockCreateSavedObjects.mockResolvedValueOnce({
errors: [createError()], // this error will NOT be passed to the second `mockCreateSavedObjects` call
createdObjects: [],
});
await resolveSavedObjectsImportErrors(setupOptions({ managed: true }));
const partialCreateSavedObjectsParams = {
accumulatedErrors: [],
savedObjectsClient,
importStateMap: new Map(),
namespace,
managed: true,
};
expect(mockCreateSavedObjects).toHaveBeenNthCalledWith(1, {
...partialCreateSavedObjectsParams,
objects: objectsToOverwrite,
overwrite: true,
});
expect(mockCreateSavedObjects).toHaveBeenNthCalledWith(2, {
...partialCreateSavedObjectsParams,
objects: objectsToNotOverwrite,
});
});
test('if not specified, sets a default for objects that do not have managed specified', async () => {
const objectsToNotOverwrite = [{ ...createObject(), managed: false }];
const objectsToOverwrite = [createObject()];
mockSplitOverwrites.mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite });
mockCreateSavedObjects.mockResolvedValueOnce({
errors: [createError()], // this error will NOT be passed to the second `mockCreateSavedObjects` call
createdObjects: [],
});
await resolveSavedObjectsImportErrors(setupOptions());
const partialCreateSavedObjectsParams = {
accumulatedErrors: [],
savedObjectsClient,
importStateMap: new Map(),
namespace,
};
expect(mockCreateSavedObjects).toHaveBeenNthCalledWith(1, {
...partialCreateSavedObjectsParams,
objects: objectsToOverwrite,
overwrite: true,
});
expect(mockCreateSavedObjects).toHaveBeenNthCalledWith(2, {
...partialCreateSavedObjectsParams,
objects: objectsToNotOverwrite,
});
});
});
});
describe('results', () => {
@ -664,12 +725,14 @@ describe('#importSavedObjectsFromStream', () => {
id: obj1.id,
meta: { title: obj1.attributes.title, icon: `${obj1.type}-icon` },
overwrite: true,
managed: false,
},
{
type: obj2.type,
id: obj2.id,
meta: { title: obj2.attributes.title, icon: `${obj2.type}-icon` },
destinationId: obj2.destinationId,
managed: false,
},
{
type: obj3.type,
@ -677,6 +740,7 @@ describe('#importSavedObjectsFromStream', () => {
meta: { title: obj3.attributes.title, icon: `${obj3.type}-icon` },
destinationId: obj3.destinationId,
createNewCopy: true,
managed: false,
},
];
const errors = [
@ -727,12 +791,14 @@ describe('#importSavedObjectsFromStream', () => {
id: obj1.id,
overwrite: true,
meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` },
managed: false,
},
{
type: obj2.type,
id: obj2.id,
overwrite: true,
meta: { title: 'bar-title', icon: `${obj2.type}-icon` },
managed: false,
},
];
@ -771,5 +837,120 @@ describe('#importSavedObjectsFromStream', () => {
warnings: [],
});
});
test('does not apply a default for `managed` when not specified', async () => {
const obj1 = createObject([], { type: 'foo' }, true);
const obj2 = createObject([], { type: 'bar', title: 'bar-title' });
const options = setupOptions({
getTypeImpl: (type) => {
if (type === 'foo') {
return {
management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` },
};
}
return {
management: { icon: `${type}-icon` },
};
},
});
mockCheckConflicts.mockResolvedValue({
errors: [],
filteredObjects: [],
importStateMap: new Map(),
pendingOverwrites: new Set(),
});
mockCreateSavedObjects
.mockResolvedValueOnce({
errors: [],
createdObjects: [obj1, { ...obj2, managed: false }],
}) // default applied in createSavedObjects
.mockResolvedValueOnce({ errors: [], createdObjects: [] });
const result = await resolveSavedObjectsImportErrors(options);
// successResults only includes the imported object's type, id, and destinationId (if a new one was generated)
const successResults = [
{
type: obj1.type,
id: obj1.id,
overwrite: true,
meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` },
managed: true,
},
{
type: obj2.type,
id: obj2.id,
overwrite: true,
meta: { title: 'bar-title', icon: `${obj2.type}-icon` },
managed: false,
},
];
expect(result).toEqual({
success: true,
successCount: 2,
successResults,
warnings: [],
});
}); // assert that the documents being imported retain their prop or have the default applied
test('applies `managed` to objects', async () => {
const obj1 = createObject([], { type: 'foo' }, true);
const obj2 = createObject([], { type: 'bar', title: 'bar-title' });
const options = setupOptions({
getTypeImpl: (type) => {
if (type === 'foo') {
return {
management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` },
};
}
return {
management: { icon: `${type}-icon` },
};
},
managed: true,
});
mockCheckConflicts.mockResolvedValue({
errors: [],
filteredObjects: [],
importStateMap: new Map(),
pendingOverwrites: new Set(),
});
mockCreateSavedObjects
.mockResolvedValueOnce({
errors: [],
createdObjects: [
{ ...obj1, managed: true },
{ ...obj2, managed: true },
],
}) // default applied in createSavedObjects
.mockResolvedValueOnce({ errors: [], createdObjects: [] });
const result = await resolveSavedObjectsImportErrors(options);
// successResults only includes the imported object's type, id, and destinationId (if a new one was generated)
const successResults = [
{
type: obj1.type,
id: obj1.id,
overwrite: true,
meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` },
managed: true,
},
{
type: obj2.type,
id: obj2.id,
overwrite: true,
meta: { title: 'bar-title', icon: `${obj2.type}-icon` },
managed: true,
},
];
expect(result).toEqual({
success: true,
successCount: 2,
successResults,
warnings: [],
});
}); // assert that the documents being imported retain their prop or have the default applied
});
});

View file

@ -60,6 +60,10 @@ export interface ResolveSavedObjectsImportErrorsOptions {
* different Kibana versions (e.g. generate legacy URL aliases for all imported objects that have to change IDs).
*/
compatibilityMode?: boolean;
/** If true, will create objects as managed.
* This property allows plugin authors to implement read-only UI's
*/
managed?: boolean;
}
/**
@ -78,6 +82,7 @@ export async function resolveSavedObjectsImportErrors({
namespace,
createNewCopies,
compatibilityMode,
managed,
}: ResolveSavedObjectsImportErrorsOptions): Promise<SavedObjectsImportResponse> {
// throw a BadRequest error if we see invalid retries
validateRetries(retries);
@ -93,6 +98,7 @@ export async function resolveSavedObjectsImportErrors({
objectLimit,
filter,
supportedTypes,
managed,
});
// Map of all IDs for objects that we are attempting to import, and any references that are not included in the read stream;
// each value is empty by default
@ -112,6 +118,7 @@ export async function resolveSavedObjectsImportErrors({
// Replace references
for (const savedObject of collectSavedObjectsResult.collectedObjects) {
// collectedObjects already have managed flag set
const refMap = retriesReferencesMap.get(`${savedObject.type}:${savedObject.id}`);
if (!refMap) {
continue;
@ -205,13 +212,14 @@ export async function resolveSavedObjectsImportErrors({
overwrite?: boolean
) => {
const createSavedObjectsParams = {
objects,
objects, // these objects only have a title, no other properties
accumulatedErrors,
savedObjectsClient,
importStateMap,
namespace,
overwrite,
compatibilityMode,
managed,
};
const { createdObjects, errors: bulkCreateErrors } = await createSavedObjects(
createSavedObjectsParams
@ -235,6 +243,7 @@ export async function resolveSavedObjectsImportErrors({
...(overwrite && { overwrite }),
...(destinationId && { destinationId }),
...(destinationId && !originId && !createNewCopies && { createNewCopy: true }),
...{ managed: createdObject.managed ?? managed ?? false }, // double sure that this already exists but doing a check just in case
};
}),
];

View file

@ -57,6 +57,7 @@ export class SavedObjectsImporter implements ISavedObjectsImporter {
overwrite,
refresh,
compatibilityMode,
managed,
}: SavedObjectsImportOptions): Promise<SavedObjectsImportResponse> {
return importSavedObjectsFromStream({
readStream,
@ -69,6 +70,7 @@ export class SavedObjectsImporter implements ISavedObjectsImporter {
savedObjectsClient: this.#savedObjectsClient,
typeRegistry: this.#typeRegistry,
importHooks: this.#importHooks,
managed,
});
}
@ -78,6 +80,7 @@ export class SavedObjectsImporter implements ISavedObjectsImporter {
compatibilityMode,
namespace,
retries,
managed,
}: SavedObjectsResolveImportErrorsOptions): Promise<SavedObjectsImportResponse> {
return resolveSavedObjectsImportErrors({
readStream,
@ -89,6 +92,7 @@ export class SavedObjectsImporter implements ISavedObjectsImporter {
savedObjectsClient: this.#savedObjectsClient,
typeRegistry: this.#typeRegistry,
importHooks: this.#importHooks,
managed,
});
}
}

View file

@ -31,11 +31,12 @@ describe('importDashboards(req)', () => {
type: 'visualization',
attributes: { visState: '{}' },
references: [],
managed: true,
},
];
});
test('should call bulkCreate with each asset, filtering out any version if present', async () => {
test('should call bulkCreate with each asset, filtering out any version and managed if present', async () => {
await importDashboards(savedObjectClient, importedObjects, { overwrite: false, exclude: [] });
expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1);

View file

@ -69,6 +69,14 @@ export interface SavedObjectsImportOptions {
* different Kibana versions (e.g. generate legacy URL aliases for all imported objects that have to change IDs).
*/
compatibilityMode?: boolean;
/**
* If true, will import as a managed object, else will import as not managed.
*
* This can be leveraged by applications to e.g. prevent edits to a managed
* saved object. Instead, users can be guided to create a copy first and
* make their edits to the copy.
*/
managed?: boolean;
}
/**
@ -89,6 +97,14 @@ export interface SavedObjectsResolveImportErrorsOptions {
* different Kibana versions (e.g. generate legacy URL aliases for all imported objects that have to change IDs).
*/
compatibilityMode?: boolean;
/**
* If true, will import as a managed object, else will import as not managed.
*
* This can be leveraged by applications to e.g. prevent edits to a managed
* saved object. Instead, users can be guided to create a copy first and
* make their edits to the copy.
*/
managed?: boolean;
}
export type CreatedObject<T> = SavedObject<T> & { destinationId?: string };

View file

@ -46,12 +46,14 @@ describe(`POST ${URL}`, () => {
id: 'my-pattern',
attributes: { title: 'my-pattern-*' },
references: [],
managed: false,
};
const mockDashboard = {
type: 'dashboard',
id: 'my-dashboard',
attributes: { title: 'Look at my dashboard' },
references: [],
managed: false,
};
beforeEach(async () => {
@ -145,6 +147,7 @@ describe(`POST ${URL}`, () => {
type: 'index-pattern',
id: 'my-pattern',
meta: { title: 'my-pattern-*', icon: 'index-pattern-icon' },
managed: false,
},
],
warnings: [],
@ -156,11 +159,49 @@ describe(`POST ${URL}`, () => {
);
});
it('imports an index pattern and dashboard, ignoring empty lines in the file', async () => {
it('returns the default for managed as part of the successResults', async () => {
savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [mockIndexPattern] });
const result = await supertest(httpSetup.server.listener)
.post(URL)
.set('content-Type', 'multipart/form-data; boundary=EXAMPLE')
.send(
[
'--EXAMPLE',
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
'Content-Type: application/ndjson',
'',
'{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}',
'--EXAMPLE--',
].join('\r\n')
)
.expect(200);
expect(result.body).toEqual({
success: true,
successCount: 1,
successResults: [
{
type: 'index-pattern',
id: 'my-pattern',
meta: { title: 'my-pattern-*', icon: 'index-pattern-icon' },
managed: false,
},
],
warnings: [],
});
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
[expect.objectContaining({ typeMigrationVersion: '', managed: false })],
expect.any(Object) // options
);
});
it('imports an index pattern, dashboard and visualization, ignoring empty lines in the file', async () => {
// NOTE: changes to this scenario should be reflected in the docs
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [mockIndexPattern, mockDashboard],
saved_objects: [mockIndexPattern, { ...mockDashboard, managed: false }],
});
const result = await supertest(httpSetup.server.listener)
@ -190,11 +231,13 @@ describe(`POST ${URL}`, () => {
type: mockIndexPattern.type,
id: mockIndexPattern.id,
meta: { title: mockIndexPattern.attributes.title, icon: 'index-pattern-icon' },
managed: false,
},
{
type: mockDashboard.type,
id: mockDashboard.id,
meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' },
managed: false,
},
],
warnings: [],
@ -235,6 +278,7 @@ describe(`POST ${URL}`, () => {
type: mockDashboard.type,
id: mockDashboard.id,
meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' },
managed: false,
},
],
errors: [
@ -287,11 +331,13 @@ describe(`POST ${URL}`, () => {
id: mockIndexPattern.id,
meta: { title: mockIndexPattern.attributes.title, icon: 'index-pattern-icon' },
overwrite: true,
managed: false,
},
{
type: mockDashboard.type,
id: mockDashboard.id,
meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' },
managed: false,
},
],
warnings: [],
@ -345,6 +391,7 @@ describe(`POST ${URL}`, () => {
type: mockDashboard.type,
id: mockDashboard.id,
meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' },
managed: false,
},
],
warnings: [],
@ -414,6 +461,7 @@ describe(`POST ${URL}`, () => {
type: mockDashboard.type,
id: mockDashboard.id,
meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' },
managed: false,
},
],
warnings: [],
@ -478,6 +526,7 @@ describe(`POST ${URL}`, () => {
type: mockDashboard.type,
id: mockDashboard.id,
meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' },
managed: false,
},
],
warnings: [],
@ -505,12 +554,14 @@ describe(`POST ${URL}`, () => {
id: 'new-id-1',
attributes: { title: 'Look at my visualization' },
references: [],
managed: false,
};
const obj2 = {
type: 'dashboard',
id: 'new-id-2',
attributes: { title: 'Look at my dashboard' },
references: [],
managed: false,
};
savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [obj1, obj2] });
@ -539,12 +590,14 @@ describe(`POST ${URL}`, () => {
id: 'my-vis',
meta: { title: obj1.attributes.title, icon: 'visualization-icon' },
destinationId: obj1.id,
managed: false,
},
{
type: obj2.type,
id: 'my-dashboard',
meta: { title: obj2.attributes.title, icon: 'dashboard-icon' },
destinationId: obj2.id,
managed: false,
},
],
warnings: [],
@ -556,11 +609,13 @@ describe(`POST ${URL}`, () => {
type: 'visualization',
id: 'new-id-1',
references: [{ name: 'ref_0', type: 'index-pattern', id: 'my-pattern' }],
managed: false,
}),
expect.objectContaining({
type: 'dashboard',
id: 'new-id-2',
references: [{ name: 'ref_0', type: 'visualization', id: 'new-id-1' }],
managed: false,
}),
],
expect.any(Object) // options
@ -769,6 +824,7 @@ describe(`POST ${URL}`, () => {
originId: 'my-vis',
attributes: { title: 'Look at my visualization' },
references: [],
managed: false,
};
const obj2 = {
type: 'dashboard',
@ -776,6 +832,7 @@ describe(`POST ${URL}`, () => {
originId: 'my-dashboard',
attributes: { title: 'Look at my dashboard' },
references: [],
managed: false,
};
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [
@ -785,6 +842,7 @@ describe(`POST ${URL}`, () => {
id: obj2.id,
attributes: {},
references: [],
managed: false,
error: { error: 'some-error', message: 'Why not?', statusCode: 503 },
},
],
@ -830,6 +888,7 @@ describe(`POST ${URL}`, () => {
id: obj1.originId,
meta: { title: obj1.attributes.title, icon: 'visualization-icon' },
destinationId: obj1.id,
managed: false,
},
],
errors: [
@ -843,6 +902,7 @@ describe(`POST ${URL}`, () => {
statusCode: 503,
type: 'unknown',
},
managed: false,
},
],
warnings: [],
@ -855,12 +915,14 @@ describe(`POST ${URL}`, () => {
id: 'new-id-1',
originId: 'my-vis',
references: [{ name: 'ref_0', type: 'index-pattern', id: 'my-pattern' }],
managed: false,
}),
expect.objectContaining({
type: 'dashboard',
id: 'new-id-2',
originId: 'my-dashboard',
references: [{ name: 'ref_0', type: 'visualization', id: 'new-id-1' }],
managed: false,
}),
],
expect.any(Object) // options
@ -905,7 +967,9 @@ describe(`POST ${URL}`, () => {
attributes: { title: 'Look at my visualization' },
references: [],
};
savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [obj1] });
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [{ ...obj1, managed: false }],
});
// Prepare mock results for the created legacy URL alias (for obj1 only).
const legacyUrlAliasObj1 = {
@ -956,6 +1020,7 @@ describe(`POST ${URL}`, () => {
id: obj1.originId,
meta: { title: obj1.attributes.title, icon: 'visualization-icon' },
destinationId: obj1.id,
managed: false,
},
],
errors: [
@ -969,6 +1034,7 @@ describe(`POST ${URL}`, () => {
type: 'unknown',
},
meta: { title: 'Legacy URL alias (my-vis -> new-id-1)', icon: 'legacy-url-alias-icon' },
managed: false,
},
],
warnings: [],
@ -981,6 +1047,7 @@ describe(`POST ${URL}`, () => {
id: 'new-id-1',
originId: 'my-vis',
references: [{ name: 'ref_0', type: 'index-pattern', id: 'my-pattern' }],
managed: false,
}),
],
expect.any(Object) // options

View file

@ -44,18 +44,21 @@ describe(`POST ${URL}`, () => {
id: 'my-dashboard',
attributes: { title: 'Look at my dashboard' },
references: [],
managed: false,
};
const mockVisualization = {
type: 'visualization',
id: 'my-vis',
attributes: { title: 'Look at my visualization' },
references: [{ name: 'ref_0', type: 'index-pattern', id: 'existing' }],
managed: false,
};
const mockIndexPattern = {
type: 'index-pattern',
id: 'existing',
attributes: {},
references: [],
managed: false,
};
beforeEach(async () => {
@ -155,12 +158,13 @@ describe(`POST ${URL}`, () => {
type,
id,
attributes: { title },
managed,
} = mockDashboard;
const meta = { title, icon: 'dashboard-icon' };
expect(result.body).toEqual({
success: true,
successCount: 1,
successResults: [{ type, id, meta }],
successResults: [{ type, id, meta, managed }],
warnings: [],
});
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
@ -193,17 +197,17 @@ describe(`POST ${URL}`, () => {
)
.expect(200);
const { type, id, attributes } = mockDashboard;
const { type, id, attributes, managed } = mockDashboard;
const meta = { title: attributes.title, icon: 'dashboard-icon' };
expect(result.body).toEqual({
success: true,
successCount: 1,
successResults: [{ type, id, meta }],
successResults: [{ type, id, meta, managed }],
warnings: [],
});
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
[{ type, id, attributes, typeMigrationVersion: '' }],
[{ type, id, attributes, typeMigrationVersion: '', managed }],
expect.objectContaining({ overwrite: undefined })
);
});
@ -232,17 +236,17 @@ describe(`POST ${URL}`, () => {
)
.expect(200);
const { type, id, attributes } = mockDashboard;
const { type, id, attributes, managed } = mockDashboard;
const meta = { title: attributes.title, icon: 'dashboard-icon' };
expect(result.body).toEqual({
success: true,
successCount: 1,
successResults: [{ type, id, meta, overwrite: true }],
successResults: [{ type, id, meta, overwrite: true, managed }],
warnings: [],
});
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
[{ type, id, attributes, typeMigrationVersion: '' }],
[{ type, id, attributes, typeMigrationVersion: '', managed }],
expect.objectContaining({ overwrite: true })
);
});
@ -271,7 +275,7 @@ describe(`POST ${URL}`, () => {
)
.expect(200);
const { type, id, attributes, references } = mockVisualization;
const { type, id, attributes, references, managed } = mockVisualization;
expect(result.body).toEqual({
success: true,
successCount: 1,
@ -280,13 +284,14 @@ describe(`POST ${URL}`, () => {
type: 'visualization',
id: 'my-vis',
meta: { title: 'Look at my visualization', icon: 'visualization-icon' },
managed,
},
],
warnings: [],
});
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
[{ type, id, attributes, references, typeMigrationVersion: '' }],
[{ type, id, attributes, references, typeMigrationVersion: '', managed }],
expect.objectContaining({ overwrite: undefined })
);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
@ -319,7 +324,7 @@ describe(`POST ${URL}`, () => {
)
.expect(200);
const { type, id, attributes } = mockVisualization;
const { type, id, attributes, managed } = mockVisualization;
const references = [{ name: 'ref_0', type: 'index-pattern', id: 'missing' }];
expect(result.body).toEqual({
success: true,
@ -329,13 +334,14 @@ describe(`POST ${URL}`, () => {
type: 'visualization',
id: 'my-vis',
meta: { title: 'Look at my visualization', icon: 'visualization-icon' },
managed,
},
],
warnings: [],
});
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
[{ type, id, attributes, references, typeMigrationVersion: '' }],
[{ type, id, attributes, references, typeMigrationVersion: '', managed }],
expect.objectContaining({ overwrite: undefined })
);
expect(savedObjectsClient.bulkGet).not.toHaveBeenCalled();
@ -351,12 +357,14 @@ describe(`POST ${URL}`, () => {
id: 'new-id-1',
attributes: { title: 'Look at my visualization' },
references: [],
managed: false,
};
const obj2 = {
type: 'dashboard',
id: 'new-id-2',
attributes: { title: 'Look at my dashboard' },
references: [],
managed: false,
};
savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [obj1, obj2] });
@ -389,12 +397,14 @@ describe(`POST ${URL}`, () => {
id: 'my-vis',
meta: { title: obj1.attributes.title, icon: 'visualization-icon' },
destinationId: obj1.id,
managed: obj1.managed,
},
{
type: obj2.type,
id: 'my-dashboard',
meta: { title: obj2.attributes.title, icon: 'dashboard-icon' },
destinationId: obj2.id,
managed: obj2.managed,
},
],
warnings: [],
@ -406,11 +416,13 @@ describe(`POST ${URL}`, () => {
type: 'visualization',
id: 'new-id-1',
references: [{ name: 'ref_0', type: 'index-pattern', id: 'existing' }],
managed: false,
}),
expect.objectContaining({
type: 'dashboard',
id: 'new-id-2',
references: [{ name: 'ref_0', type: 'visualization', id: 'new-id-1' }],
managed: false,
}),
],
expect.any(Object) // options
@ -429,6 +441,7 @@ describe(`POST ${URL}`, () => {
id: 'my-vis',
attributes: { title: 'Look at my visualization' },
references: [],
managed: false,
};
const obj2 = {
type: 'dashboard',
@ -436,6 +449,7 @@ describe(`POST ${URL}`, () => {
originId: 'my-dashboard',
attributes: { title: 'Look at my dashboard' },
references: [],
managed: false,
};
savedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [obj1, obj2] });
@ -451,6 +465,7 @@ describe(`POST ${URL}`, () => {
targetId: obj2.id,
purpose: 'savedObjectImport',
},
managed: false,
};
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [legacyUrlAliasObj2],
@ -484,12 +499,14 @@ describe(`POST ${URL}`, () => {
type: obj1.type,
id: 'my-vis',
meta: { title: obj1.attributes.title, icon: 'visualization-icon' },
managed: obj1.managed,
},
{
type: obj2.type,
id: 'my-dashboard',
meta: { title: obj2.attributes.title, icon: 'dashboard-icon' },
destinationId: obj2.id,
managed: obj2.managed,
},
],
warnings: [],
@ -502,12 +519,14 @@ describe(`POST ${URL}`, () => {
type: 'visualization',
id: 'my-vis',
references: [{ name: 'ref_0', type: 'index-pattern', id: 'existing' }],
managed: false,
}),
expect.objectContaining({
type: 'dashboard',
id: 'new-id-2',
originId: 'my-dashboard',
references: [{ name: 'ref_0', type: 'visualization', id: 'my-vis' }],
managed: false,
}),
],
expect.any(Object) // options

View file

@ -75,7 +75,7 @@ export default function ({ getService }: FtrProviderContext) {
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: resp.body.saved_objects[1].typeMigrationVersion,
managed: resp.body.saved_objects[1].managed,
managed: false,
references: [],
namespaces: [SPACE_ID],
},

View file

@ -28,17 +28,42 @@ export default function ({ getService }: FtrProviderContext) {
},
];
const BULK_REQUESTS_MANAGED = [
{
type: 'visualization',
id: '3fdaa535-5baf-46bc-8265-705eda43b181',
},
{
type: 'tag',
id: '0ed60f29-2021-4fd2-ba4e-943c61e2738c',
},
{
type: 'tag',
id: '00ad6a46-6ac3-4f6c-892c-2f72c54a5e7d',
},
{
type: 'dashboard',
id: '11fb046d-0e50-48a0-a410-a744b82cbffd',
},
];
describe('_bulk_get', () => {
before(async () => {
await kibanaServer.importExport.load(
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
await kibanaServer.importExport.load(
'test/api_integration/fixtures/kbn_archiver/saved_objects/managed_basic.json'
);
});
after(async () => {
await kibanaServer.importExport.unload(
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
await kibanaServer.importExport.unload(
'test/api_integration/fixtures/kbn_archiver/saved_objects/managed_basic.json'
);
});
it('should return 200 with individual responses', async () =>
@ -115,5 +140,130 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body.saved_objects[0].migrationVersion).to.be.ok();
expect(resp.body.saved_objects[0].typeMigrationVersion).to.be.ok();
}));
it('should return 200 with individual responses that include the managed property of each object', async () =>
await supertest
.post(`/api/saved_objects/_bulk_get`)
.send(BULK_REQUESTS_MANAGED)
.expect(200)
.then((resp) => {
const mockDate = '2015-01-01T00:00:00.000Z';
resp.body.saved_objects[0].updated_at = mockDate;
resp.body.saved_objects[1].updated_at = mockDate;
resp.body.saved_objects[2].updated_at = mockDate;
resp.body.saved_objects[3].updated_at = mockDate;
resp.body.saved_objects[0].created_at = mockDate;
resp.body.saved_objects[1].created_at = mockDate;
resp.body.saved_objects[2].created_at = mockDate;
resp.body.saved_objects[3].created_at = mockDate;
expect(resp.body.saved_objects.length).to.eql(4);
expect(resp.body).to.eql({
saved_objects: [
{
id: '3fdaa535-5baf-46bc-8265-705eda43b181',
type: 'visualization',
namespaces: ['default'],
migrationVersion: {
visualization: '8.5.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '8.5.0',
updated_at: '2015-01-01T00:00:00.000Z',
created_at: '2015-01-01T00:00:00.000Z',
version: resp.body.saved_objects[0].version,
attributes: {
description: '',
kibanaSavedObjectMeta: {
searchSourceJSON:
resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
},
title: 'Managed Count of requests',
uiStateJSON: '{"spy":{"mode":{"name":null,"fill":false}}}',
version: resp.body.saved_objects[0].attributes.version,
visState: resp.body.saved_objects[0].attributes.visState,
},
references: [
{
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
type: 'index-pattern',
},
],
managed: true,
},
{
id: '0ed60f29-2021-4fd2-ba4e-943c61e2738c',
type: 'tag',
updated_at: '2015-01-01T00:00:00.000Z',
created_at: '2015-01-01T00:00:00.000Z',
namespaces: ['default'],
migrationVersion: {
tag: '8.0.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '8.0.0',
version: resp.body.saved_objects[1].version,
attributes: { color: '#E7664C', description: 'read-only', name: 'managed' },
references: [],
managed: true,
},
{
id: '00ad6a46-6ac3-4f6c-892c-2f72c54a5e7d',
type: 'tag',
namespaces: ['default'],
migrationVersion: {
tag: '8.0.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '8.0.0',
updated_at: '2015-01-01T00:00:00.000Z',
created_at: '2015-01-01T00:00:00.000Z',
version: resp.body.saved_objects[2].version,
attributes: { color: '#173a58', description: 'Editable', name: 'unmanaged' },
references: [],
managed: false,
},
{
id: '11fb046d-0e50-48a0-a410-a744b82cbffd',
type: 'dashboard',
namespaces: ['default'],
migrationVersion: {
dashboard: '8.7.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '8.7.0',
updated_at: '2015-01-01T00:00:00.000Z',
created_at: '2015-01-01T00:00:00.000Z',
version: resp.body.saved_objects[3].version,
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta:
resp.body.saved_objects[3].attributes.kibanaSavedObjectMeta,
optionsJSON: '{"darkTheme":false}',
panelsJSON: resp.body.saved_objects[3].attributes.panelsJSON,
refreshInterval: resp.body.saved_objects[3].attributes.refreshInterval,
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Managed Requests',
version: resp.body.saved_objects[3].attributes.version,
},
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: '1:panel_1',
type: 'visualization',
},
],
managed: true,
},
],
});
expect(resp.body.saved_objects[0].managed).to.be.ok();
expect(resp.body.saved_objects[1].managed).to.be.ok();
expect(resp.body.saved_objects[2].managed).not.to.be.ok();
expect(resp.body.saved_objects[3].managed).to.be.ok();
}));
});
}

View file

@ -26,7 +26,9 @@ export default function ({ getService }: FtrProviderContext) {
);
});
after(() => kibanaServer.spaces.delete(SPACE_ID));
after(async () => {
await kibanaServer.spaces.delete(SPACE_ID);
});
describe('basic amount of saved objects', () => {
it('should return objects in dependency order', async () => {
@ -578,5 +580,46 @@ export default function ({ getService }: FtrProviderContext) {
.expect(200);
});
});
describe('should retain the managed property value of exported saved objects', () => {
before(async () => {
await kibanaServer.importExport.unload(
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json',
{ space: SPACE_ID }
);
await kibanaServer.importExport.load(
'test/api_integration/fixtures/kbn_archiver/saved_objects/managed_objects.json',
{ space: SPACE_ID }
);
});
after(async () => {
await kibanaServer.importExport.unload(
'test/api_integration/fixtures/kbn_archiver/saved_objects/managed_objects.json',
{ space: SPACE_ID }
);
});
it('should retain all existing saved object properties', async () => {
// we're specifically asserting that the `managed` property isn't overwritten during export.
await supertest
.post(`/s/${SPACE_ID}/api/saved_objects/_export`)
.send({
type: ['config', 'index-pattern'],
})
.expect(200)
.then((resp) => {
const objects = ndjsonToObject(resp.text);
expect(objects).to.have.length(3);
expect(objects[0]).to.have.property('id', '6cda943f-a70e-43d4-b0cb-feb1b624cb62');
expect(objects[0]).to.have.property('type', 'index-pattern');
expect(objects[0]).to.have.property('managed', true);
expect(objects[1]).to.have.property('id', 'c1818992-bb2c-4a9a-b276-83ada7cce03e');
expect(objects[1]).to.have.property('type', 'config');
expect(objects[1]).to.have.property('managed', false);
expect(objects[2]).to.have.property('exportedCount', 2);
expect(objects[2]).to.have.property('missingRefCount', 0);
expect(objects[2].missingReferences).to.have.length(0);
});
});
});
});
}

View file

@ -18,11 +18,17 @@ export default function ({ getService }: FtrProviderContext) {
await kibanaServer.importExport.load(
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
await kibanaServer.importExport.load(
'test/api_integration/fixtures/kbn_archiver/saved_objects/managed_basic.json'
);
});
after(async () => {
await kibanaServer.importExport.unload(
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
await kibanaServer.importExport.unload(
'test/api_integration/fixtures/kbn_archiver/saved_objects/managed_basic.json'
);
});
it('should return 200', async () =>
@ -60,8 +66,52 @@ export default function ({ getService }: FtrProviderContext) {
});
expect(resp.body.migrationVersion).to.be.ok();
expect(resp.body.typeMigrationVersion).to.be.ok();
expect(resp.body.managed).to.not.be.ok();
expect(resp.body.managed).not.to.be.ok();
}));
it("should return an object's managed property", async () => {
await supertest
.get(`/api/saved_objects/dashboard/11fb046d-0e50-48a0-a410-a744b82cbffd`)
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
id: '11fb046d-0e50-48a0-a410-a744b82cbffd',
type: 'dashboard',
namespaces: ['default'],
migrationVersion: {
dashboard: '8.7.0',
},
coreMigrationVersion: '8.8.0',
typeMigrationVersion: '8.7.0',
updated_at: resp.body.updated_at,
created_at: resp.body.created_at,
version: resp.body.version,
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta,
optionsJSON: '{"darkTheme":false}',
panelsJSON: resp.body.attributes.panelsJSON,
refreshInterval: resp.body.attributes.refreshInterval,
timeFrom: resp.body.attributes.timeFrom,
timeRestore: true,
timeTo: resp.body.attributes.timeTo,
title: 'Managed Requests',
version: resp.body.attributes.version,
},
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: '1:panel_1',
type: 'visualization',
},
],
managed: true,
});
expect(resp.body.migrationVersion).to.be.ok();
expect(resp.body.typeMigrationVersion).to.be.ok();
expect(resp.body.managed).to.be.ok();
});
});
describe('doc does not exist', () => {
it('should return same generic error as when index does not exist', async () =>

View file

@ -40,6 +40,42 @@ export default function ({ getService }: FtrProviderContext) {
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
meta: { title: 'Requests', icon: 'dashboardApp' },
};
const managedVis = {
id: '3fdaa535-5baf-46bc-8265-705eda43b181',
type: 'visualization',
meta: {
icon: 'visualizeApp',
title: 'Managed Count of requests',
},
managed: true,
};
const managedTag = {
id: '0ed60f29-2021-4fd2-ba4e-943c61e2738c',
type: 'tag',
meta: {
icon: 'tag',
title: 'managed',
},
managed: true,
};
const unmanagedTag = {
id: '00ad6a46-6ac3-4f6c-892c-2f72c54a5e7d',
type: 'tag',
meta: {
icon: 'tag',
title: 'unmanaged',
},
managed: false,
};
const managedDB = {
id: '11fb046d-0e50-48a0-a410-a744b82cbffd',
type: 'dashboard',
meta: {
icon: 'dashboardApp',
title: 'Managed Requests',
},
managed: true,
};
describe('with basic data existing', () => {
before(async () => {
@ -96,9 +132,9 @@ export default function ({ getService }: FtrProviderContext) {
success: true,
successCount: 3,
successResults: [
{ ...indexPattern, overwrite: true },
{ ...visualization, overwrite: true },
{ ...dashboard, overwrite: true },
{ ...indexPattern, overwrite: true, managed: false },
{ ...visualization, overwrite: true, managed: false },
{ ...dashboard, overwrite: true, managed: false },
],
warnings: [],
});
@ -155,6 +191,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'dashboard-b',
},
type: 'dashboard',
managed: false,
},
{
id: 'dashboard-a',
@ -163,6 +200,7 @@ export default function ({ getService }: FtrProviderContext) {
title: 'dashboard-a',
},
type: 'dashboard',
managed: false,
},
],
warnings: [],
@ -239,6 +277,74 @@ export default function ({ getService }: FtrProviderContext) {
});
});
});
it('should retain existing saved object managed property', async () => {
const objectsToImport = [
JSON.stringify({
type: 'config',
id: '1234',
attributes: {},
references: [],
managed: true,
}),
];
await supertest
.post('/api/saved_objects/_import')
.attach('file', Buffer.from(objectsToImport.join('\n'), 'utf8'), 'export.ndjson')
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: true,
successCount: 1,
successResults: [
{
id: '1234',
meta: {
title: 'Advanced Settings [1234]',
},
type: 'config',
managed: true,
},
],
warnings: [],
});
});
});
it('should not overwrite managed if set on objects beging imported', async () => {
await supertest
.post('/api/saved_objects/_import')
.attach('file', join(__dirname, '../../fixtures/import_managed.ndjson'))
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: true,
successCount: 4,
successResults: [managedVis, unmanagedTag, managedTag, managedDB],
warnings: [],
});
});
});
it('should return 200 when conflicts exist but overwrite is passed in, without changing managed property on the object', async () => {
await supertest
.post('/api/saved_objects/_import')
.query({ overwrite: true })
.attach('file', join(__dirname, '../../fixtures/import_managed.ndjson'))
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: true,
successCount: 4,
successResults: [
{ ...managedVis, overwrite: true },
{ ...unmanagedTag, overwrite: true },
{ ...managedTag, overwrite: true },
{ ...managedDB, overwrite: true },
],
warnings: [],
});
});
});
});
});
}

View file

@ -84,9 +84,9 @@ export default function ({ getService }: FtrProviderContext) {
success: true,
successCount: 3,
successResults: [
{ ...indexPattern, overwrite: true },
{ ...visualization, overwrite: true },
{ ...dashboard, overwrite: true },
{ ...indexPattern, overwrite: true, managed: false },
{ ...visualization, overwrite: true, managed: false },
{ ...dashboard, overwrite: true, managed: false },
],
warnings: [],
});
@ -112,7 +112,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body).to.eql({
success: true,
successCount: 1,
successResults: [{ ...visualization, overwrite: true }],
successResults: [{ ...visualization, overwrite: true, managed: false }],
warnings: [],
});
});
@ -163,6 +163,7 @@ export default function ({ getService }: FtrProviderContext) {
type: 'visualization',
id: '1',
meta: { title: 'My favorite vis', icon: 'visualizeApp' },
managed: false,
},
],
warnings: [],

View file

@ -0,0 +1,4 @@
{"id":"3fdaa535-5baf-46bc-8265-705eda43b181","type":"visualization","namespaces":["default"],"coreMigrationVersion":"8.8.0","typeMigrationVersion":"8.5.0","updated_at":"2023-04-24T19:57:13.859Z","created_at":"2023-04-24T19:57:13.859Z","version":"WzExNCwxXQ==","attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Managed Count of requests","uiStateJSON":"{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}","version":1,"visState":"{\"title\":\"Count of requests\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\",\"legendSize\":\"auto\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}"},"references":[{"id":"91200a00-9efd-11e7-acb3-3dab96693fab","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"managed":true}
{"id":"00ad6a46-6ac3-4f6c-892c-2f72c54a5e7d","type":"tag","namespaces":["default"],"coreMigrationVersion":"8.8.0","typeMigrationVersion":"8.0.0","updated_at":"2023-04-24T19:58:24.550Z","created_at":"2023-04-24T19:58:24.550Z","version":"WzEyMSwxXQ==","attributes":{"color":"#173a58","description":"Editable","name":"unmanaged"},"references":[],"managed":false}
{"id":"0ed60f29-2021-4fd2-ba4e-943c61e2738c","type":"tag","namespaces":["default"],"coreMigrationVersion":"8.8.0","typeMigrationVersion":"8.0.0","updated_at":"2023-04-24T19:58:24.550Z","created_at":"2023-04-24T19:58:24.550Z","version":"WzEyMiwxXQ==","attributes":{"color":"#E7664C","description":"read-only","name":"managed"},"references":[],"managed":true}
{"id":"11fb046d-0e50-48a0-a410-a744b82cbffd","type":"dashboard","namespaces":["default"],"coreMigrationVersion":"8.8.0","typeMigrationVersion":"8.7.0","updated_at":"2023-04-24T20:54:57.921Z","created_at":"2023-04-24T20:54:57.921Z","version":"WzMwOSwxXQ==","attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}"},"optionsJSON":"{\"darkTheme\":false}","panelsJSON":"[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":12,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"}]","refreshInterval":{"display":"Off","pause":false,"value":0},"timeFrom":"Wed Sep 16 2015 22:52:17 GMT-0700","timeRestore":true,"timeTo":"Fri Sep 18 2015 12:24:38 GMT-0700","title":"Managed Requests","version":1},"references":[{"id":"dd7caf20-9efd-11e7-acb3-3dab96693fab","name":"1:panel_1","type":"visualization"}],"managed":true}

View file

@ -0,0 +1,110 @@
{
"id": "3fdaa535-5baf-46bc-8265-705eda43b181",
"type": "visualization",
"namespaces": [
"default"
],
"coreMigrationVersion": "8.8.0",
"typeMigrationVersion": "8.5.0",
"updated_at": "2023-04-24T19:57:13.859Z",
"created_at": "2023-04-24T19:57:13.859Z",
"version": "WzExNCwxXQ==",
"attributes": {
"description": "",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
},
"title": "Managed Count of requests",
"uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
"version": 1,
"visState": "{\"title\":\"Count of requests\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\",\"legendSize\":\"auto\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}"
},
"references": [
{
"id": "91200a00-9efd-11e7-acb3-3dab96693fab",
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"type": "index-pattern"
}
],
"managed": true
}
{
"id": "00ad6a46-6ac3-4f6c-892c-2f72c54a5e7d",
"type": "tag",
"namespaces": [
"default"
],
"coreMigrationVersion": "8.8.0",
"typeMigrationVersion": "8.0.0",
"updated_at": "2023-04-24T19:58:24.550Z",
"created_at": "2023-04-24T19:58:24.550Z",
"version": "WzEyMSwxXQ==",
"attributes": {
"color": "#173a58",
"description": "Editable",
"name": "unmanaged"
},
"references": [],
"managed": false
}
{
"id": "0ed60f29-2021-4fd2-ba4e-943c61e2738c",
"type": "tag",
"namespaces": [
"default"
],
"coreMigrationVersion": "8.8.0",
"typeMigrationVersion": "8.0.0",
"updated_at": "2023-04-24T19:58:24.550Z",
"created_at": "2023-04-24T19:58:24.550Z",
"version": "WzEyMiwxXQ==",
"attributes": {
"color": "#E7664C",
"description": "read-only",
"name": "managed"
},
"references": [],
"managed": true
}
{
"id": "11fb046d-0e50-48a0-a410-a744b82cbffd",
"type": "dashboard",
"namespaces": [
"default"
],
"coreMigrationVersion": "8.8.0",
"typeMigrationVersion": "8.7.0",
"updated_at": "2023-04-24T20:54:57.921Z",
"created_at": "2023-04-24T20:54:57.921Z",
"version": "WzMwOSwxXQ==",
"attributes": {
"description": "",
"hits": 0,
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}"
},
"optionsJSON": "{\"darkTheme\":false}",
"panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":12,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"}]",
"refreshInterval": {
"display": "Off",
"pause": false,
"value": 0
},
"timeFrom": "Wed Sep 16 2015 22:52:17 GMT-0700",
"timeRestore": true,
"timeTo": "Fri Sep 18 2015 12:24:38 GMT-0700",
"title": "Managed Requests",
"version": 1
},
"references": [
{
"id": "dd7caf20-9efd-11e7-acb3-3dab96693fab",
"name": "1:panel_1",
"type": "visualization"
}
],
"managed": true
}

File diff suppressed because one or more lines are too long

View file

@ -47,6 +47,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
meta: {
title: 'my title',
},
managed: false,
},
],
});

View file

@ -59,6 +59,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
title: 'new title!',
},
overwrite: true,
managed: false,
},
],
});

View file

@ -188,6 +188,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
title: 'I am hidden from http apis but the client can still see me',
},
type: 'test-hidden-from-http-apis-importable-exportable',
managed: false,
},
{
id: 'not-hidden-from-http-apis-import1',
@ -195,6 +196,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
title: 'I am not hidden from http apis',
},
type: 'test-not-hidden-from-http-apis-importable-exportable',
managed: false,
},
],
warnings: [],

View file

@ -89,6 +89,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
title: 'Saved object type that is not visible in management',
},
type: 'test-not-visible-in-management',
managed: false,
},
],
warnings: [],

View file

@ -182,6 +182,7 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
icon: 'dashboardApp',
},
destinationId: dashboardDestinationId,
managed: false,
},
],
},
@ -229,24 +230,28 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
title: `Copy to Space index pattern 1 from ${spaceId} space`,
},
destinationId: indexPatternDestinationId,
managed: false,
},
{
id: `cts_vis_1_${spaceId}`,
type: 'visualization',
meta: { icon: 'visualizeApp', title: `CTS vis 1 from ${spaceId} space` },
destinationId: vis1DestinationId,
managed: false,
},
{
id: `cts_vis_2_${spaceId}`,
type: 'visualization',
meta: { icon: 'visualizeApp', title: `CTS vis 2 from ${spaceId} space` },
destinationId: vis2DestinationId,
managed: false,
},
{
id: `cts_vis_3_${spaceId}`,
type: 'visualization',
meta: { icon: 'visualizeApp', title: `CTS vis 3 from ${spaceId} space` },
destinationId: vis3DestinationId,
managed: false,
},
{
id: `cts_dashboard_${spaceId}`,
@ -256,6 +261,7 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
title: `This is the ${spaceId} test space CTS dashboard`,
},
destinationId: dashboardDestinationId,
managed: false,
},
],
},
@ -357,18 +363,21 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
},
overwrite: true,
destinationId: `cts_ip_1_${destination}`, // this conflicted with another index pattern in the destination space because of a shared originId
managed: false,
},
{
id: `cts_vis_1_${spaceId}`,
type: 'visualization',
meta: { icon: 'visualizeApp', title: `CTS vis 1 from ${spaceId} space` },
destinationId: vis1DestinationId,
managed: false,
},
{
id: `cts_vis_2_${spaceId}`,
type: 'visualization',
meta: { icon: 'visualizeApp', title: `CTS vis 2 from ${spaceId} space` },
destinationId: vis2DestinationId,
managed: false,
},
{
id: `cts_vis_3_${spaceId}`,
@ -376,6 +385,7 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
meta: { icon: 'visualizeApp', title: `CTS vis 3 from ${spaceId} space` },
overwrite: true,
destinationId: `cts_vis_3_${destination}`, // this conflicted with another visualization in the destination space because of a shared originId
managed: false,
},
{
id: `cts_dashboard_${spaceId}`,
@ -386,6 +396,7 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
},
overwrite: true,
destinationId: `cts_dashboard_${destination}`, // this conflicted with another dashboard in the destination space because of a shared originId
managed: false,
},
],
},
@ -419,12 +430,14 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
type: 'visualization',
meta: { icon: 'visualizeApp', title: `CTS vis 1 from ${spaceId} space` },
destinationId: vis1DestinationId,
managed: false,
},
{
id: `cts_vis_2_${spaceId}`,
type: 'visualization',
meta: { icon: 'visualizeApp', title: `CTS vis 2 from ${spaceId} space` },
destinationId: vis2DestinationId,
managed: false,
},
];
const expectedErrors = [
@ -522,7 +535,8 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
const destinationId = successResults![0].destinationId;
expect(destinationId).to.match(UUID_PATTERN);
const meta = { title, icon: 'beaker' };
expect(successResults).to.eql([{ type, id: sourceId, meta, destinationId }]);
const managed = false; // default added By `create`
expect(successResults).to.eql([{ type, id: sourceId, meta, destinationId, managed }]);
expect(errors).to.be(undefined);
};
@ -594,7 +608,14 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
expect(success).to.eql(true);
expect(successCount).to.eql(1);
expect(successResults).to.eql([
{ type, id: inexactMatchIdA, meta, overwrite: true, destinationId },
{
type,
id: inexactMatchIdA,
meta,
overwrite: true,
destinationId,
managed: false,
},
]);
expect(errors).to.be(undefined);
} else {
@ -635,7 +656,14 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
expect(success).to.eql(true);
expect(successCount).to.eql(1);
expect(successResults).to.eql([
{ type, id: inexactMatchIdB, meta, overwrite: true, destinationId },
{
type,
id: inexactMatchIdB,
meta,
overwrite: true,
destinationId,
managed: false,
},
]);
expect(errors).to.be(undefined);
} else {
@ -676,7 +704,14 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
expect(success).to.eql(true);
expect(successCount).to.eql(1);
expect(successResults).to.eql([
{ type, id: inexactMatchIdC, meta, overwrite: true, destinationId },
{
type,
id: inexactMatchIdC,
meta,
overwrite: true,
destinationId,
managed: false,
},
]);
expect(errors).to.be(undefined);
} else {

View file

@ -108,6 +108,7 @@ export function resolveCopyToSpaceConflictsSuite(context: FtrProviderContext) {
},
destinationId: `cts_ip_1_${destination}`, // this conflicted with another index pattern in the destination space because of a shared originId
overwrite: true,
managed: false,
},
{
id: `cts_vis_3_${sourceSpaceId}`,
@ -118,6 +119,7 @@ export function resolveCopyToSpaceConflictsSuite(context: FtrProviderContext) {
},
destinationId: `cts_vis_3_${destination}`, // this conflicted with another visualization in the destination space because of a shared originId
overwrite: true,
managed: false,
},
],
},
@ -147,6 +149,7 @@ export function resolveCopyToSpaceConflictsSuite(context: FtrProviderContext) {
},
destinationId: `cts_dashboard_${destinationSpaceId}`, // this conflicted with another dashboard in the destination space because of a shared originId
overwrite: true,
managed: false,
},
],
},
@ -380,7 +383,14 @@ export function resolveCopyToSpaceConflictsSuite(context: FtrProviderContext) {
})();
const meta = { title, icon: 'beaker' };
expect(successResults).to.eql([
{ type, id, meta, overwrite: true, ...(destinationId && { destinationId }) },
{
type,
id,
meta,
overwrite: true,
...(destinationId && { destinationId }),
managed: false,
},
]);
};