From 2359b68e5abdf3b3875aadacb5d171797671d3b1 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Thu, 20 Apr 2023 15:22:57 -0700 Subject: [PATCH 01/12] Adds managed to import options --- .../src/saved_objects_imports.ts | 5 + .../src/import/import_saved_objects.ts | 10 +- .../import/lib/collect_saved_objects.test.ts | 30 +++-- .../src/import/lib/collect_saved_objects.ts | 5 + .../import/lib/create_saved_objects.test.ts | 71 +++++++---- .../src/import/lib/create_saved_objects.ts | 17 ++- .../src/import/lib/extract_errors.test.ts | 14 +++ .../src/import/lib/extract_errors.ts | 3 + .../src/import/resolve_import_errors.test.ts | 5 + .../src/import/resolve_import_errors.ts | 8 ++ .../src/import/saved_objects_importer.ts | 4 + .../lib/import_dashboards.test.ts | 3 +- .../core-saved-objects-server/src/import.ts | 16 +++ .../saved_objects/routes/import.test.ts | 67 ++++++++++- .../routes/resolve_import_errors.test.ts | 113 ++++++++++-------- 15 files changed, 283 insertions(+), 88 deletions(-) diff --git a/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts b/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts index 7d6700ed3b47c..ddfeaf803c41d 100644 --- a/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts +++ b/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts @@ -97,6 +97,7 @@ export interface SavedObjectsImportFailure { | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; + managed?: boolean; } /** @@ -125,6 +126,10 @@ 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; + /** + * @TINA add consistent, relevant description + */ + managed?: boolean; } /** diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.ts index 8650e082c4163..d16a23c9db065 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.ts @@ -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 { 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; @@ -159,7 +165,7 @@ export async function importSavedObjectsFromStream({ 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 +176,7 @@ export async function importSavedObjectsFromStream({ type, id, meta, + managed: createdObjectManaged ?? managed, ...(attemptedOverwrite && { overwrite: true }), ...(destinationId && { destinationId }), ...(destinationId && !originId && !createNewCopies && { createNewCopy: true }), @@ -182,6 +189,7 @@ export async function importSavedObjectsFromStream({ ...error, meta: { ...error.meta, icon }, ...(attemptedOverwrite && { overwrite: true }), + ...{ managed: error.managed }, }; }); const warnings = await executeImportHooks({ diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.test.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.test.ts index 73a1f9913a4de..b44ae1c48aa10 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.test.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.test.ts @@ -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 diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts index 92d8476759548..e46ff8ea1ba5f 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts @@ -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 opperation, applied to all objects being imported + ...(managed ? { managed } : { managed: obj.managed ?? false }), }; }), createConcatStream([]), diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.test.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.test.ts index 6938c251992f5..f65333ed3228d 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.test.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.test.ts @@ -19,19 +19,36 @@ import { type CreateSavedObjectsParams = Parameters[0]; +interface CreateOptions { + type: string; + id: string; + originId?: string; + managed?: boolean; +} /** * 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 = ( @@ -51,19 +68,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 +88,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 }], ]); @@ -99,7 +116,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,6 +127,7 @@ 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 = { @@ -131,7 +149,7 @@ describe('#createSavedObjects', () => { const getResultMock = { success: ( - { type, id, attributes, references, originId }: SavedObject, + { type, id, attributes, references, originId, managed }: SavedObject, { namespace }: CreateSavedObjectsParams ): SavedObject => ({ type, @@ -139,6 +157,7 @@ describe('#createSavedObjects', () => { attributes, references, ...(originId && { originId }), + ...(managed && { managed }), version: 'some-version', updated_at: 'some-date', namespaces: [namespace ?? 'default'], @@ -253,7 +272,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); @@ -288,7 +307,7 @@ describe('#createSavedObjects', () => { }); }); - it('filters out version from objects before create', async () => { + it('filters out version from objects before create and accepts managed', async () => { const options = setupParams({ objects: [{ ...obj1, version: 'foo' }] }); bulkCreate.mockResolvedValue({ saved_objects: [getResultMock.success(obj1, options)] }); diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts index a8de0bfc92750..083c8bd6003fa 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts @@ -30,6 +30,14 @@ export interface CreateSavedObjectsParams { * 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 { @@ -50,6 +58,7 @@ export const createSavedObjects = async ({ overwrite, refresh, compatibilityMode, + managed, }: CreateSavedObjectsParams): Promise> => { // filter out any objects that resulted in errors const errorSet = accumulatedErrors.reduce( @@ -96,7 +105,12 @@ export const createSavedObjects = async ({ ...(!importStateValue.omitOriginId && { originId: originId ?? object.id }), }; } - return { ...object, ...(references && { references }), ...(originId && { originId }) }; + return { + ...object, + ...(references && { references }), + ...(originId && { originId }), + ...(managed && { managed }), + }; }); const resolvableErrors = ['conflict', 'ambiguous_conflict', 'missing_references']; @@ -150,6 +164,7 @@ export const createSavedObjects = async ({ targetId: result.id, purpose: 'savedObjectImport', }, + managed, // we can safey create each doc with the given managed flag, even if it's set as the default, bulkCreate would "override" this otherwise. }); } } diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/extract_errors.test.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/extract_errors.test.ts index 26499dcefaf88..5340139fe1ca0 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/extract_errors.test.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/extract_errors.test.ts @@ -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)", }, diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/extract_errors.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/extract_errors.ts index 1f9ee579f39d5..0dad94a37df93 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/extract_errors.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/extract_errors.ts @@ -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, }); } } diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.test.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.test.ts index ded3cb788eb9a..b1f2ec9a34897 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.test.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.test.ts @@ -664,12 +664,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 +679,7 @@ describe('#importSavedObjectsFromStream', () => { meta: { title: obj3.attributes.title, icon: `${obj3.type}-icon` }, destinationId: obj3.destinationId, createNewCopy: true, + managed: false, }, ]; const errors = [ @@ -727,12 +730,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, }, ]; diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.ts index 8abacbeb310a4..299e135f98440 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.ts @@ -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 { // 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 @@ -212,6 +218,7 @@ export async function resolveSavedObjectsImportErrors({ namespace, overwrite, compatibilityMode, + managed, }; const { createdObjects, errors: bulkCreateErrors } = await createSavedObjects( createSavedObjectsParams @@ -235,6 +242,7 @@ export async function resolveSavedObjectsImportErrors({ ...(overwrite && { overwrite }), ...(destinationId && { destinationId }), ...(destinationId && !originId && !createNewCopies && { createNewCopy: true }), + ...{ managed: createdObject.managed ?? managed ?? false }, }; }), ]; diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/saved_objects_importer.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/saved_objects_importer.ts index 0a2b39993cd59..9098e2c6f1c19 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/saved_objects_importer.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/saved_objects_importer.ts @@ -57,6 +57,7 @@ export class SavedObjectsImporter implements ISavedObjectsImporter { overwrite, refresh, compatibilityMode, + managed = false, // @TINA set the default here and pass that all the way through. }: SavedObjectsImportOptions): Promise { 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 = false, // @TINA set the default here and pass that all the way through. }: SavedObjectsResolveImportErrorsOptions): Promise { return resolveSavedObjectsImportErrors({ readStream, @@ -89,6 +92,7 @@ export class SavedObjectsImporter implements ISavedObjectsImporter { savedObjectsClient: this.#savedObjectsClient, typeRegistry: this.#typeRegistry, importHooks: this.#importHooks, + managed, }); } } diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.test.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.test.ts index d6382efd673fa..156f9f42f792b 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.test.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.test.ts @@ -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); diff --git a/packages/core/saved-objects/core-saved-objects-server/src/import.ts b/packages/core/saved-objects/core-saved-objects-server/src/import.ts index a10fcd4a237a9..90d33e9339c43 100644 --- a/packages/core/saved-objects/core-saved-objects-server/src/import.ts +++ b/packages/core/saved-objects/core-saved-objects-server/src/import.ts @@ -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 = SavedObject & { destinationId?: string }; diff --git a/src/core/server/integration_tests/saved_objects/routes/import.test.ts b/src/core/server/integration_tests/saved_objects/routes/import.test.ts index 24116ff52267c..6ea25327b87be 100644 --- a/src/core/server/integration_tests/saved_objects/routes/import.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/import.test.ts @@ -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 @@ -981,6 +1043,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 diff --git a/src/core/server/integration_tests/saved_objects/routes/resolve_import_errors.test.ts b/src/core/server/integration_tests/saved_objects/routes/resolve_import_errors.test.ts index 2202735df957f..85eae7ef53937 100644 --- a/src/core/server/integration_tests/saved_objects/routes/resolve_import_errors.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/resolve_import_errors.test.ts @@ -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], @@ -476,42 +491,46 @@ describe(`POST ${URL}`, () => { ) .expect(200); - expect(result.body).toEqual({ - success: true, - successCount: 2, - successResults: [ - { - type: obj1.type, - id: 'my-vis', - meta: { title: obj1.attributes.title, icon: 'visualization-icon' }, - }, - { - type: obj2.type, - id: 'my-dashboard', - meta: { title: obj2.attributes.title, icon: 'dashboard-icon' }, - destinationId: obj2.id, - }, - ], - warnings: [], - }); - expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(2); // successResults objects were created because no resolvable errors are present - expect(savedObjectsClient.bulkCreate).toHaveBeenNthCalledWith( - 1, - [ - expect.objectContaining({ - type: 'visualization', - id: 'my-vis', - references: [{ name: 'ref_0', type: 'index-pattern', id: 'existing' }], - }), - expect.objectContaining({ - type: 'dashboard', - id: 'new-id-2', - originId: 'my-dashboard', - references: [{ name: 'ref_0', type: 'visualization', id: 'my-vis' }], - }), - ], - expect.any(Object) // options - ); + // expect(result.body).toEqual({ + // success: true, + // successCount: 2, + // successResults: [ + // { + // 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: [], + // }); + // expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(2); // successResults objects were created because no resolvable errors are present + // expect(savedObjectsClient.bulkCreate).toHaveBeenNthCalledWith( + // 1, + // [ + // expect.objectContaining({ + // 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 + // ); expect(savedObjectsClient.bulkCreate).toHaveBeenNthCalledWith( 2, [expect.objectContaining(legacyUrlAliasObj2)], From b81f8134174c50dbe52625d71aabf6a321dba7b2 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Fri, 21 Apr 2023 15:10:09 -0700 Subject: [PATCH 02/12] Updates import and resolve_import_errors ftr integration test --- .../src/lib/internal_utils.ts | 8 +- .../src/lib/repository.ts | 2 +- .../src/saved_objects_imports.ts | 1 - .../src/import/import_saved_objects.test.ts | 122 +++++++++++- .../src/import/import_saved_objects.ts | 2 +- .../src/import/lib/collect_saved_objects.ts | 4 +- .../import/lib/create_saved_objects.test.ts | 28 ++- .../src/import/lib/create_saved_objects.ts | 14 +- .../src/import/resolve_import_errors.test.ts | 178 +++++++++++++++++- .../src/import/resolve_import_errors.ts | 5 +- .../src/import/saved_objects_importer.ts | 4 +- .../routes/resolve_import_errors.test.ts | 80 ++++---- .../apis/saved_objects/export.ts | 45 ++++- .../apis/saved_objects/import.ts | 41 +++- .../saved_objects/resolve_import_errors.ts | 9 +- .../saved_objects/managedObjects.json | 98 ++++++++++ 16 files changed, 562 insertions(+), 79 deletions(-) create mode 100644 test/api_integration/fixtures/kbn_archiver/saved_objects/managedObjects.json diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts index afafd67fcab3a..1ffc5fcde62d8 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts @@ -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; } diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 0bde80ea0f32f..46459fef28fab 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -615,7 +615,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { typeMigrationVersion: object.typeMigrationVersion, ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - managed: setManaged({ optionsManaged, objectManaged: object.managed }), + managed: setManaged({ optionsManaged, objectManaged: object.managed }), // managed: optionsManaged ?? object.managed ?? false, updated_at: time, created_at: time, references: object.references || [], diff --git a/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts b/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts index ddfeaf803c41d..768d55134d1b7 100644 --- a/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts +++ b/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts @@ -97,7 +97,6 @@ export interface SavedObjectsImportFailure { | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; - managed?: boolean; } /** diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts index 66a010b548a4a..1a0a46672e104 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts @@ -80,10 +80,12 @@ describe('#importSavedObjectsFromStream', () => { management: { icon: `${type}-icon` }, } as any), importHooks = {}, + managed, }: { createNewCopies?: boolean; getTypeImpl?: (name: string) => any; importHooks?: Record; + 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, // yea, explicitly declare this 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,7 @@ describe('#importSavedObjectsFromStream', () => { importStateMap, overwrite, namespace, + managed: options.managed, }; expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams); }); @@ -383,6 +390,119 @@ describe('#importSavedObjectsFromStream', () => { expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams); }); }); + + describe('managed option', () => { + test('if not specified, applied default to objects if missing and does not override existing flag if already present', async () => { + const obj1 = createObject({ type: 'foo', managed: true }); + const obj2 = createObject({ type: 'bar', title: 'bar-title' }); + + const options = setupOptions({ + createNewCopies: false, + 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.mockResolvedValue({ + errors: [], + createdObjects: [obj1, { ...obj2, managed: false }], // default applied in createSavedObjects + }); + + const result = await importSavedObjectsFromStream(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, + meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` }, + managed: true, + }, + { + type: obj2.type, + id: obj2.id, + 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('if specified, overrides existing flag if already present', async () => { + const obj1 = createObject({ type: 'foo', managed: false }); + const obj2 = createObject({ type: 'bar', title: 'bar-title' }); + + const options = setupOptions({ + createNewCopies: false, + managed: true, + 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.mockResolvedValue({ + errors: [], + createdObjects: [ + { ...obj1, managed: true }, + { ...obj2, managed: true }, + ], // default applied in createSavedObjects + }); + + const result = await importSavedObjectsFromStream(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, + meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` }, + managed: true, + }, + { + type: obj2.type, + id: obj2.id, + 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 + }); }); describe('results', () => { diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.ts index d16a23c9db065..9d150386cee4d 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.ts @@ -160,6 +160,7 @@ export async function importSavedObjectsFromStream({ namespace, refresh, compatibilityMode, + managed, }; const createSavedObjectsResult = await createSavedObjects(createSavedObjectsParams); errorAccumulator = [...errorAccumulator, ...createSavedObjectsResult.errors]; @@ -189,7 +190,6 @@ export async function importSavedObjectsFromStream({ ...error, meta: { ...error.meta, icon }, ...(attemptedOverwrite && { overwrite: true }), - ...{ managed: error.managed }, }; }); const warnings = await executeImportHooks({ diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts index e46ff8ea1ba5f..60b13dfa0ec5e 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts @@ -67,12 +67,14 @@ export async function collectSavedObjects({ } } // Ensure migrations execute on every saved object + // Managed prop handling: // we don't make any assumptions about how to handle defaults for overwriting all abjects' managed flag if there isn't a `managed` option passed to the call. This allows us to retain the flag on objects being imported, if it's already present on the object. + // We fall back to false if managed isn't declared as an option and the object being imported doesn't already have the prop set. We do this to avoid having to run the core transform for backfilling. 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 opperation, applied to all objects being imported - ...(managed ? { managed } : { managed: obj.managed ?? false }), + ...{ managed: managed ?? obj.managed ?? false }, }; }), createConcatStream([]), diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.test.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.test.ts index f65333ed3228d..86d1a0635083d 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.test.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.test.ts @@ -55,12 +55,14 @@ const createLegacyUrlAliasObject = ( sourceId: string, targetId: string, targetType: string, - targetNamespace: string = 'default' + targetNamespace: string = 'default', + managed?: boolean ): SavedObject => ({ 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'; @@ -109,6 +111,7 @@ describe('#createSavedObjects', () => { namespace?: string; overwrite?: boolean; compatibilityMode?: boolean; + managed?: boolean; }): CreateSavedObjectsParams => { savedObjectsClient = savedObjectsClientMock.create(); bulkCreate = savedObjectsClient.bulkCreate; @@ -149,15 +152,16 @@ describe('#createSavedObjects', () => { const getResultMock = { success: ( - { type, id, attributes, references, originId, managed }: SavedObject, - { namespace }: CreateSavedObjectsParams + { type, id, attributes, references, originId, managed: objectManaged }: SavedObject, + { namespace, managed }: CreateSavedObjectsParams ): SavedObject => ({ type, id, attributes, references, ...(originId && { originId }), - ...(managed && { managed }), + ...((managed && { managed }) ?? + (objectManaged && { managed: objectManaged }) ?? { managed: false }), version: 'some-version', updated_at: 'some-date', namespaces: [namespace ?? 'default'], @@ -288,6 +292,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], @@ -308,7 +313,9 @@ describe('#createSavedObjects', () => { }); it('filters out version from objects before create and accepts managed', async () => { - const options = setupParams({ objects: [{ ...obj1, version: 'foo' }] }); + const options = setupParams({ objects: [{ ...obj1, version: 'foo' }] }); // here optionsManaged is undefined + // const saved_objects = [getResultMock.success(obj1, options)]; + // console.log('saved_objects[0]', JSON.stringify(saved_objects[0])); bulkCreate.mockResolvedValue({ saved_objects: [getResultMock.success(obj1, options)] }); await createSavedObjects(options); @@ -318,8 +325,9 @@ 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 options = setupParams({ objects: objs, namespace, compatibilityMode, managed }); setupMockResults(options); await createSavedObjects(options); @@ -380,8 +388,10 @@ describe('#createSavedObjects', () => { describe('with an undefined namespace', () => { test('calls bulkCreate according to input objects and compatibilityMode option', async () => { - await testBulkCreateObjects(); - await testBulkCreateObjects({ compatibilityMode: true }); + const expectedObjects = await testBulkCreateObjects(); + console.log('expectedObjects:', expectedObjects); + const realObjects = await testBulkCreateObjects({ compatibilityMode: true }); + console.log('realObjects:', realObjects); }); test('calls bulkCreate once with input options', async () => { await testBulkCreateOptions(); diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts index 083c8bd6003fa..f1bcfe9c24682 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts @@ -50,7 +50,7 @@ export interface CreateSavedObjectsResult { * the objects we create, and the create results should be mapped to the original IDs that consumers will be able to understand. */ export const createSavedObjects = async ({ - objects, + objects, // these already have the managed flag set accumulatedErrors, savedObjectsClient, importStateMap, @@ -109,9 +109,10 @@ export const createSavedObjects = async ({ ...object, ...(references && { references }), ...(originId && { originId }), - ...(managed && { managed }), + ...{ managed: managed ?? object.managed ?? false }, // trictly speaking this shouldn't be needed since the objects passed in should already have the flag set }; }); + console.log('objectsToCreate:', JSON.stringify(objectsToCreate)); const resolvableErrors = ['conflict', 'ambiguous_conflict', 'missing_references']; const hasResolvableErrors = accumulatedErrors.some(({ error: { type } }) => @@ -164,7 +165,7 @@ export const createSavedObjects = async ({ targetId: result.id, purpose: 'savedObjectImport', }, - managed, // we can safey create each doc with the given managed flag, even if it's set as the default, bulkCreate would "override" this otherwise. + ...{ 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. }); } } @@ -178,9 +179,12 @@ export const createSavedObjects = async ({ overwrite, refresh, }) - ).saved_objects + ).saved_objects // already have managed specified : []; - + console.log( + 'remappedResults.filter((obj) => !obj.error)', + JSON.stringify(remappedResults.filter((obj) => !obj.error)) + ); return { createdObjects: remappedResults.filter((obj) => !obj.error), errors: extractErrors(remappedResults, objects, legacyUrlAliasResults, legacyUrlAliases), diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.test.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.test.ts index b1f2ec9a34897..5b2a145b54dd6 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.test.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.test.ts @@ -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; + 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', () => { @@ -776,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 }); }); diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.ts index 299e135f98440..ade85f02988c5 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/resolve_import_errors.ts @@ -118,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; @@ -211,7 +212,7 @@ export async function resolveSavedObjectsImportErrors({ overwrite?: boolean ) => { const createSavedObjectsParams = { - objects, + objects, // these objects only have a title, no other properties accumulatedErrors, savedObjectsClient, importStateMap, @@ -242,7 +243,7 @@ export async function resolveSavedObjectsImportErrors({ ...(overwrite && { overwrite }), ...(destinationId && { destinationId }), ...(destinationId && !originId && !createNewCopies && { createNewCopy: true }), - ...{ managed: createdObject.managed ?? managed ?? false }, + ...{ managed: createdObject.managed ?? managed ?? false }, // double sure that this already exists but doing a check just in case }; }), ]; diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/saved_objects_importer.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/saved_objects_importer.ts index 9098e2c6f1c19..2c87395c255cd 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/saved_objects_importer.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/saved_objects_importer.ts @@ -57,7 +57,7 @@ export class SavedObjectsImporter implements ISavedObjectsImporter { overwrite, refresh, compatibilityMode, - managed = false, // @TINA set the default here and pass that all the way through. + managed, }: SavedObjectsImportOptions): Promise { return importSavedObjectsFromStream({ readStream, @@ -80,7 +80,7 @@ export class SavedObjectsImporter implements ISavedObjectsImporter { compatibilityMode, namespace, retries, - managed = false, // @TINA set the default here and pass that all the way through. + managed, }: SavedObjectsResolveImportErrorsOptions): Promise { return resolveSavedObjectsImportErrors({ readStream, diff --git a/src/core/server/integration_tests/saved_objects/routes/resolve_import_errors.test.ts b/src/core/server/integration_tests/saved_objects/routes/resolve_import_errors.test.ts index 85eae7ef53937..0e5af8e86be08 100644 --- a/src/core/server/integration_tests/saved_objects/routes/resolve_import_errors.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/resolve_import_errors.test.ts @@ -491,46 +491,46 @@ describe(`POST ${URL}`, () => { ) .expect(200); - // expect(result.body).toEqual({ - // success: true, - // successCount: 2, - // successResults: [ - // { - // 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: [], - // }); - // expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(2); // successResults objects were created because no resolvable errors are present - // expect(savedObjectsClient.bulkCreate).toHaveBeenNthCalledWith( - // 1, - // [ - // expect.objectContaining({ - // 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 - // ); + expect(result.body).toEqual({ + success: true, + successCount: 2, + successResults: [ + { + 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: [], + }); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(2); // successResults objects were created because no resolvable errors are present + expect(savedObjectsClient.bulkCreate).toHaveBeenNthCalledWith( + 1, + [ + expect.objectContaining({ + 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 + ); expect(savedObjectsClient.bulkCreate).toHaveBeenNthCalledWith( 2, [expect.objectContaining(legacyUrlAliasObj2)], diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index ec7b82453ffce..b3ac1a9899582 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -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 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/managedObjects.json', + { space: SPACE_ID } + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/managedObjects.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); + }); + }); + }); }); } diff --git a/test/api_integration/apis/saved_objects/import.ts b/test/api_integration/apis/saved_objects/import.ts index 0d9cfed365a00..e7e84bd077517 100644 --- a/test/api_integration/apis/saved_objects/import.ts +++ b/test/api_integration/apis/saved_objects/import.ts @@ -96,9 +96,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 +155,7 @@ export default function ({ getService }: FtrProviderContext) { title: 'dashboard-b', }, type: 'dashboard', + managed: false, }, { id: 'dashboard-a', @@ -163,6 +164,7 @@ export default function ({ getService }: FtrProviderContext) { title: 'dashboard-a', }, type: 'dashboard', + managed: false, }, ], warnings: [], @@ -239,6 +241,39 @@ 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: [], + }); + }); + }); }); }); } diff --git a/test/api_integration/apis/saved_objects/resolve_import_errors.ts b/test/api_integration/apis/saved_objects/resolve_import_errors.ts index 7ca61a26a11c1..ce9ca42a1dedc 100644 --- a/test/api_integration/apis/saved_objects/resolve_import_errors.ts +++ b/test/api_integration/apis/saved_objects/resolve_import_errors.ts @@ -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: [], diff --git a/test/api_integration/fixtures/kbn_archiver/saved_objects/managedObjects.json b/test/api_integration/fixtures/kbn_archiver/saved_objects/managedObjects.json new file mode 100644 index 0000000000000..7aa1e872adf41 --- /dev/null +++ b/test/api_integration/fixtures/kbn_archiver/saved_objects/managedObjects.json @@ -0,0 +1,98 @@ +{ + "attributes": { + "buildNum": 8467, + "defaultIndex": "91200a00-9efd-11e7-acb3-3dab96693fab" + }, + "coreMigrationVersion": "8.8.0", + "created_at": "2023-04-21T23:19:26.101Z", + "id": "c1818992-bb2c-4a9a-b276-83ada7cce03e", + "managed": false, + "references": [], + "type": "config", + "typeMigrationVersion": "8.7.0", + "updated_at": "2023-04-21T23:19:26.101Z", + "version": "WzkzLDFd" +} + +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "8.8.0", + "created_at": "2023-04-21T23:19:26.101Z", + "id": "6cda943f-a70e-43d4-b0cb-feb1b624cb62", + "managed": true, + "references": [], + "type": "index-pattern", + "typeMigrationVersion": "7.11.0", + "updated_at": "2023-04-21T23:19:26.101Z", + "version": "Wzk0LDFd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "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\":{}}}]}" + }, + "coreMigrationVersion": "8.8.0", + "created_at": "2023-04-21T23:19:26.101Z", + "id": "37fb7899-4b68-4a5b-9b4c-f9587ab7f0bc", + "managed": true, + "references": [ + { + "id": "6cda943f-a70e-43d4-b0cb-feb1b624cb62", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "typeMigrationVersion": "8.5.0", + "updated_at": "2023-04-21T23:19:26.101Z", + "version": "Wzk1LDFd" +} + +{ + "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": "Requests", + "version": 1 + }, + "coreMigrationVersion": "8.8.0", + "created_at": "2023-04-21T23:19:26.101Z", + "id": "def09d77-089a-4e4f-b30b-eb9848d59648", + "managed": true, + "references": [ + { + "id": "37fb7899-4b68-4a5b-9b4c-f9587ab7f0bc", + "name": "1:panel_1", + "type": "visualization" + } + ], + "type": "dashboard", + "typeMigrationVersion": "8.7.0", + "updated_at": "2023-04-21T23:19:26.101Z", + "version": "Wzk2LDFd" +} + From 03fdfe85f7e10856b9648892be1ed03924ba6f16 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Sun, 23 Apr 2023 11:33:36 -0700 Subject: [PATCH 03/12] Updates existing import jest integration tests to include the managed flag that is set by default to false during import and legacy alias creation - this is fine, a legacyUrlAlias is a SO and they all need managed set anyway --- .../src/legacy_alias/types.ts | 6 ++++ .../import/lib/create_saved_objects.test.ts | 29 +++++++++++++------ .../src/import/lib/create_saved_objects.ts | 5 ---- .../saved_objects/routes/import.test.ts | 6 +++- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/legacy_alias/types.ts b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/legacy_alias/types.ts index 9148b4e903c82..b0fc589e30c3d 100644 --- a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/legacy_alias/types.ts +++ b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/legacy_alias/types.ts @@ -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; } diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.test.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.test.ts index 86d1a0635083d..ac6831626f2f2 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.test.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.test.ts @@ -25,6 +25,9 @@ interface CreateOptions { 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 */ @@ -137,7 +140,12 @@ describe('#createSavedObjects', () => { 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); @@ -314,8 +322,6 @@ describe('#createSavedObjects', () => { it('filters out version from objects before create and accepts managed', async () => { const options = setupParams({ objects: [{ ...obj1, version: 'foo' }] }); // here optionsManaged is undefined - // const saved_objects = [getResultMock.success(obj1, options)]; - // console.log('saved_objects[0]', JSON.stringify(saved_objects[0])); bulkCreate.mockResolvedValue({ saved_objects: [getResultMock.success(obj1, options)] }); await createSavedObjects(options); @@ -327,7 +333,13 @@ describe('#createSavedObjects', () => { compatibilityMode, managed, }: { namespace?: string; compatibilityMode?: boolean; managed?: boolean } = {}) => { - const options = setupParams({ objects: objs, namespace, compatibilityMode, managed }); + const objsWithMissingManaged = addManagedDefault(objs); + const options = setupParams({ + objects: objsWithMissingManaged, + namespace, + compatibilityMode, + managed, + }); setupMockResults(options); await createSavedObjects(options); @@ -337,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. @@ -388,10 +401,8 @@ describe('#createSavedObjects', () => { describe('with an undefined namespace', () => { test('calls bulkCreate according to input objects and compatibilityMode option', async () => { - const expectedObjects = await testBulkCreateObjects(); - console.log('expectedObjects:', expectedObjects); - const realObjects = await testBulkCreateObjects({ compatibilityMode: true }); - console.log('realObjects:', realObjects); + await testBulkCreateObjects(); + await testBulkCreateObjects({ compatibilityMode: true }); }); test('calls bulkCreate once with input options', async () => { await testBulkCreateOptions(); diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts index f1bcfe9c24682..15aba3ab96153 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts @@ -112,7 +112,6 @@ export const createSavedObjects = async ({ ...{ managed: managed ?? object.managed ?? false }, // trictly speaking this shouldn't be needed since the objects passed in should already have the flag set }; }); - console.log('objectsToCreate:', JSON.stringify(objectsToCreate)); const resolvableErrors = ['conflict', 'ambiguous_conflict', 'missing_references']; const hasResolvableErrors = accumulatedErrors.some(({ error: { type } }) => @@ -181,10 +180,6 @@ export const createSavedObjects = async ({ }) ).saved_objects // already have managed specified : []; - console.log( - 'remappedResults.filter((obj) => !obj.error)', - JSON.stringify(remappedResults.filter((obj) => !obj.error)) - ); return { createdObjects: remappedResults.filter((obj) => !obj.error), errors: extractErrors(remappedResults, objects, legacyUrlAliasResults, legacyUrlAliases), diff --git a/src/core/server/integration_tests/saved_objects/routes/import.test.ts b/src/core/server/integration_tests/saved_objects/routes/import.test.ts index 6ea25327b87be..0a9ae5f164635 100644 --- a/src/core/server/integration_tests/saved_objects/routes/import.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/import.test.ts @@ -967,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 = { @@ -1018,6 +1020,7 @@ describe(`POST ${URL}`, () => { id: obj1.originId, meta: { title: obj1.attributes.title, icon: 'visualization-icon' }, destinationId: obj1.id, + managed: false, }, ], errors: [ @@ -1031,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: [], From 126727050bc9b20e2bf927090682474b5810f4f6 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Sun, 23 Apr 2023 15:33:05 -0700 Subject: [PATCH 04/12] adds unit test for creating managed objects --- .../src/import/import_saved_objects.test.ts | 149 ++++++++++++++++- .../apis/saved_objects/bulk_create.ts | 2 +- .../apis/saved_objects/bulk_get.ts | 150 ++++++++++++++++++ .../api_integration/apis/saved_objects/get.ts | 52 +++++- .../apis/saved_objects/import.ts | 77 +++++++++ .../apis/saved_objects/index.ts | 26 +-- .../fixtures/import_managed.ndjson | 4 + .../saved_objects/managed_basic.json | 110 +++++++++++++ 8 files changed, 552 insertions(+), 18 deletions(-) create mode 100644 test/api_integration/fixtures/import_managed.ndjson create mode 100644 test/api_integration/fixtures/kbn_archiver/saved_objects/managed_basic.json diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts index 1a0a46672e104..102da41eef902 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts @@ -330,6 +330,54 @@ describe('#importSavedObjectsFromStream', () => { }; 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); + }); }); describe('with createNewCopies enabled', () => { @@ -389,12 +437,48 @@ describe('#importSavedObjectsFromStream', () => { }; expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams); }); + + test('creates managed saved objects', async () => { + const options = setupOptions({ createNewCopies: true, managed: true }); + 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: options.managed, + }; + expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams); + }); }); describe('managed option', () => { - test('if not specified, applied default to objects if missing and does not override existing flag if already present', async () => { + test('if not provided, does not override existing property when already present, defaults to false for others', async () => { const obj1 = createObject({ type: 'foo', managed: true }); - const obj2 = createObject({ type: 'bar', title: 'bar-title' }); + const obj2 = createObject({ type: 'bar', title: 'bar-title', managed: false }); const options = setupOptions({ createNewCopies: false, @@ -445,7 +529,8 @@ describe('#importSavedObjectsFromStream', () => { warnings: [], }); }); // assert that the documents being imported retain their prop or have the default applied - test('if specified, overrides existing flag if already present', async () => { + + test('creates and converts objects from unmanaged to managed', async () => { const obj1 = createObject({ type: 'foo', managed: false }); const obj2 = createObject({ type: 'bar', title: 'bar-title' }); @@ -502,6 +587,64 @@ describe('#importSavedObjectsFromStream', () => { warnings: [], }); }); // assert that the documents being imported retain their prop or have the default applied + + test('creates and converts objects from managed to unmanaged', async () => { + const obj1 = createObject({ type: 'foo', managed: true }); + const obj2 = createObject({ type: 'bar', title: 'bar-title' }); + + const options = setupOptions({ + createNewCopies: false, + managed: false, + 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.mockResolvedValue({ + errors: [], + createdObjects: [ + { ...obj1, managed: false }, + { ...obj2, managed: false }, + ], + }); + + const result = await importSavedObjectsFromStream(options); + // successResults only includes the imported object's type, id, destinationId (if a new one was generated) and managed + const successResults = [ + { + type: obj1.type, + id: obj1.id, + meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` }, + managed: false, + }, + { + type: obj2.type, + id: obj2.id, + 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 }); }); diff --git a/test/api_integration/apis/saved_objects/bulk_create.ts b/test/api_integration/apis/saved_objects/bulk_create.ts index b365df10f7d74..5119e43368cc5 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.ts +++ b/test/api_integration/apis/saved_objects/bulk_create.ts @@ -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], }, diff --git a/test/api_integration/apis/saved_objects/bulk_get.ts b/test/api_integration/apis/saved_objects/bulk_get.ts index 6c97efd94d70a..c3adf21b6c55e 100644 --- a/test/api_integration/apis/saved_objects/bulk_get.ts +++ b/test/api_integration/apis/saved_objects/bulk_get.ts @@ -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(); + })); }); } diff --git a/test/api_integration/apis/saved_objects/get.ts b/test/api_integration/apis/saved_objects/get.ts index a7b1f58c531f4..bf4ca7d240a89 100644 --- a/test/api_integration/apis/saved_objects/get.ts +++ b/test/api_integration/apis/saved_objects/get.ts @@ -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 () => diff --git a/test/api_integration/apis/saved_objects/import.ts b/test/api_integration/apis/saved_objects/import.ts index e7e84bd077517..6c1f579abc436 100644 --- a/test/api_integration/apis/saved_objects/import.ts +++ b/test/api_integration/apis/saved_objects/import.ts @@ -40,17 +40,59 @@ 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 () => { 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 415 when no file passed in', async () => { @@ -274,6 +316,41 @@ export default function ({ getService }: FtrProviderContext) { }); }); }); + + 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: [], + }); + }); + }); }); }); } diff --git a/test/api_integration/apis/saved_objects/index.ts b/test/api_integration/apis/saved_objects/index.ts index c981add4540b3..f3a57e7d57aa6 100644 --- a/test/api_integration/apis/saved_objects/index.ts +++ b/test/api_integration/apis/saved_objects/index.ts @@ -10,19 +10,19 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('saved_objects', () => { - loadTestFile(require.resolve('./bulk_create')); - loadTestFile(require.resolve('./bulk_delete')); - loadTestFile(require.resolve('./bulk_get')); - loadTestFile(require.resolve('./bulk_update')); - loadTestFile(require.resolve('./create')); - loadTestFile(require.resolve('./delete')); + // loadTestFile(require.resolve('./bulk_create')); + // loadTestFile(require.resolve('./bulk_delete')); + // loadTestFile(require.resolve('./bulk_get')); + // loadTestFile(require.resolve('./bulk_update')); + // loadTestFile(require.resolve('./create')); + // loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./export')); - loadTestFile(require.resolve('./find')); - loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./import')); - loadTestFile(require.resolve('./resolve')); - loadTestFile(require.resolve('./resolve_import_errors')); - loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./delete_unknown_types')); + // loadTestFile(require.resolve('./find')); + // loadTestFile(require.resolve('./get')); + // loadTestFile(require.resolve('./import')); + // loadTestFile(require.resolve('./resolve')); + // loadTestFile(require.resolve('./resolve_import_errors')); + // loadTestFile(require.resolve('./update')); + // loadTestFile(require.resolve('./delete_unknown_types')); }); } diff --git a/test/api_integration/fixtures/import_managed.ndjson b/test/api_integration/fixtures/import_managed.ndjson new file mode 100644 index 0000000000000..82ebc57a8b688 --- /dev/null +++ b/test/api_integration/fixtures/import_managed.ndjson @@ -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} diff --git a/test/api_integration/fixtures/kbn_archiver/saved_objects/managed_basic.json b/test/api_integration/fixtures/kbn_archiver/saved_objects/managed_basic.json new file mode 100644 index 0000000000000..4ca74083f824f --- /dev/null +++ b/test/api_integration/fixtures/kbn_archiver/saved_objects/managed_basic.json @@ -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 +} From e19f896d13e80353e23efe76e7bf0609d5bd33ad Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Mon, 24 Apr 2023 18:09:18 -0700 Subject: [PATCH 05/12] updates ftr api_integration tests --- .../apis/saved_objects/export.ts | 2 +- .../apis/saved_objects/index.ts | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index b3ac1a9899582..927deb7dd6192 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -581,7 +581,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - describe('should retain the managed property of exported saved objects', () => { + 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', diff --git a/test/api_integration/apis/saved_objects/index.ts b/test/api_integration/apis/saved_objects/index.ts index f3a57e7d57aa6..c981add4540b3 100644 --- a/test/api_integration/apis/saved_objects/index.ts +++ b/test/api_integration/apis/saved_objects/index.ts @@ -10,19 +10,19 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('saved_objects', () => { - // loadTestFile(require.resolve('./bulk_create')); - // loadTestFile(require.resolve('./bulk_delete')); - // loadTestFile(require.resolve('./bulk_get')); - // loadTestFile(require.resolve('./bulk_update')); - // loadTestFile(require.resolve('./create')); - // loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./bulk_create')); + loadTestFile(require.resolve('./bulk_delete')); + loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./bulk_update')); + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./export')); - // loadTestFile(require.resolve('./find')); - // loadTestFile(require.resolve('./get')); - // loadTestFile(require.resolve('./import')); - // loadTestFile(require.resolve('./resolve')); - // loadTestFile(require.resolve('./resolve_import_errors')); - // loadTestFile(require.resolve('./update')); - // loadTestFile(require.resolve('./delete_unknown_types')); + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./import')); + loadTestFile(require.resolve('./resolve')); + loadTestFile(require.resolve('./resolve_import_errors')); + loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./delete_unknown_types')); }); } From c52bb41a8642e7dee885a8e5191f59d42f8ca2f7 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Mon, 24 Apr 2023 18:37:45 -0700 Subject: [PATCH 06/12] Updates type def --- .../core-saved-objects-common/src/saved_objects_imports.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts b/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts index 768d55134d1b7..f1c2d66717f15 100644 --- a/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts +++ b/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts @@ -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 From e7c031946879259eff41a08f131945c01794ffea Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 25 Apr 2023 07:26:21 -0700 Subject: [PATCH 07/12] Updates tests --- .../test_suites/saved_objects_hidden_type/import.ts | 1 + .../saved_objects_hidden_type/resolve_import_errors.ts | 1 + .../saved_objects_management/hidden_from_http_apis.ts | 2 ++ .../saved_objects_management/visible_in_management.ts | 1 + 4 files changed, 5 insertions(+) diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/import.ts b/test/plugin_functional/test_suites/saved_objects_hidden_type/import.ts index f8fa3c11a1b8c..897170a5f6a30 100644 --- a/test/plugin_functional/test_suites/saved_objects_hidden_type/import.ts +++ b/test/plugin_functional/test_suites/saved_objects_hidden_type/import.ts @@ -47,6 +47,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { meta: { title: 'my title', }, + managed: false, }, ], }); diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/resolve_import_errors.ts b/test/plugin_functional/test_suites/saved_objects_hidden_type/resolve_import_errors.ts index be2af8cd5d115..26688ddd20046 100644 --- a/test/plugin_functional/test_suites/saved_objects_hidden_type/resolve_import_errors.ts +++ b/test/plugin_functional/test_suites/saved_objects_hidden_type/resolve_import_errors.ts @@ -59,6 +59,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { title: 'new title!', }, overwrite: true, + managed: false, }, ], }); diff --git a/test/plugin_functional/test_suites/saved_objects_management/hidden_from_http_apis.ts b/test/plugin_functional/test_suites/saved_objects_management/hidden_from_http_apis.ts index d9516ee0331c4..c37bd671a764f 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/hidden_from_http_apis.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/hidden_from_http_apis.ts @@ -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: [], diff --git a/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts b/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts index c4cbd575dc064..3567db0172ac8 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts @@ -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: [], From cbd45618530730811b711ce86dd1d88b82cf9c94 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 25 Apr 2023 14:56:57 -0600 Subject: [PATCH 08/12] spaces_only ftr test updates --- .../src/saved_objects_imports.ts | 6 ++- .../common/suites/copy_to_space.ts | 43 +++++++++++++++++-- .../common/suites/get.ts | 3 ++ .../suites/resolve_copy_to_space_conflicts.ts | 12 +++++- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts b/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts index f1c2d66717f15..85d221c0c58af 100644 --- a/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts +++ b/packages/core/saved-objects/core-saved-objects-common/src/saved_objects_imports.ts @@ -127,7 +127,11 @@ export interface SavedObjectsImportSuccess { */ overwrite?: boolean; /** - * @TINA add consistent, relevant description + * 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; } diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index 4c5ae878bbf6e..2325e0259c8dd 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -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 { diff --git a/x-pack/test/spaces_api_integration/common/suites/get.ts b/x-pack/test/spaces_api_integration/common/suites/get.ts index 22862d7d67898..eee54af57dcd3 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get.ts @@ -57,18 +57,21 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperAgent) description: 'This is the default space', _reserved: true, disabledFeatures: [], + managed: false, }, { id: 'space_1', name: 'Space 1', description: 'This is the first test space', disabledFeatures: [], + managed: false, }, { id: 'space_2', name: 'Space 2', description: 'This is the second test space', disabledFeatures: [], + managed: false, }, ]; expect(resp.body).to.eql(allSpaces.find((space) => space.id === spaceId)); diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index 5f2c361714c49..0a71beb9bffa0 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -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, + }, ]); }; From 0669c428bd9f40641183f57b390fa4150fa72f0d Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 25 Apr 2023 14:21:14 -0700 Subject: [PATCH 09/12] fix one more failure in get --- x-pack/test/spaces_api_integration/common/suites/get.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/test/spaces_api_integration/common/suites/get.ts b/x-pack/test/spaces_api_integration/common/suites/get.ts index eee54af57dcd3..22862d7d67898 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get.ts @@ -57,21 +57,18 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperAgent) description: 'This is the default space', _reserved: true, disabledFeatures: [], - managed: false, }, { id: 'space_1', name: 'Space 1', description: 'This is the first test space', disabledFeatures: [], - managed: false, }, { id: 'space_2', name: 'Space 2', description: 'This is the second test space', disabledFeatures: [], - managed: false, }, ]; expect(resp.body).to.eql(allSpaces.find((space) => space.id === spaceId)); From 4eeb530cbdaebb95ef2e86f585d5160942368ed0 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 25 Apr 2023 16:50:52 -0700 Subject: [PATCH 10/12] cleanup --- .../src/lib/repository.ts | 2 +- .../src/import/import_saved_objects.test.ts | 10 +++++----- .../src/import/lib/collect_saved_objects.ts | 2 -- .../src/import/lib/create_saved_objects.ts | 6 +++--- test/api_integration/apis/saved_objects/export.ts | 4 ++-- test/api_integration/apis/saved_objects/import.ts | 6 ------ .../{managedObjects.json => managed_objects.json} | 0 7 files changed, 11 insertions(+), 19 deletions(-) rename test/api_integration/fixtures/kbn_archiver/saved_objects/{managedObjects.json => managed_objects.json} (100%) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 46459fef28fab..0bde80ea0f32f 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -615,7 +615,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { typeMigrationVersion: object.typeMigrationVersion, ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - managed: setManaged({ optionsManaged, objectManaged: object.managed }), // managed: optionsManaged ?? object.managed ?? false, + managed: setManaged({ optionsManaged, objectManaged: object.managed }), updated_at: time, created_at: time, references: object.references || [], diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts index 102da41eef902..4312e4fab4533 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts @@ -106,7 +106,7 @@ describe('#importSavedObjectsFromStream', () => { const createObject = ({ type = 'foo-type', title = 'some-title', - managed = undefined, // yea, explicitly declare this as not set to test against existing objects + 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; @@ -506,7 +506,7 @@ describe('#importSavedObjectsFromStream', () => { }); const result = await importSavedObjectsFromStream(options); - // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) + // successResults only includes the imported object's type, id, managed, and destinationId (if a new one was generated) const successResults = [ { type: obj1.type, @@ -560,11 +560,11 @@ describe('#importSavedObjectsFromStream', () => { createdObjects: [ { ...obj1, managed: true }, { ...obj2, managed: true }, - ], // default applied in createSavedObjects + ], // make sure the default isn't applied in createSavedObjects }); const result = await importSavedObjectsFromStream(options); - // successResults only includes the imported object's type, id, and destinationId (if a new one was generated) + // successResults only includes the imported object's type, id, managed and destinationId (if a new one was generated) const successResults = [ { type: obj1.type, @@ -622,7 +622,7 @@ describe('#importSavedObjectsFromStream', () => { }); const result = await importSavedObjectsFromStream(options); - // successResults only includes the imported object's type, id, destinationId (if a new one was generated) and managed + // successResults only includes the imported object's type, id, managed and destinationId (if a new one was generated) const successResults = [ { type: obj1.type, diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts index 60b13dfa0ec5e..c3801060f736c 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts @@ -67,8 +67,6 @@ export async function collectSavedObjects({ } } // Ensure migrations execute on every saved object - // Managed prop handling: // we don't make any assumptions about how to handle defaults for overwriting all abjects' managed flag if there isn't a `managed` option passed to the call. This allows us to retain the flag on objects being imported, if it's already present on the object. - // We fall back to false if managed isn't declared as an option and the object being imported doesn't already have the prop set. We do this to avoid having to run the core transform for backfilling. return { ...obj, ...(!obj.migrationVersion && !obj.typeMigrationVersion ? { typeMigrationVersion: '' } : {}), diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts index 15aba3ab96153..9f0f98484d594 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/create_saved_objects.ts @@ -50,7 +50,7 @@ export interface CreateSavedObjectsResult { * the objects we create, and the create results should be mapped to the original IDs that consumers will be able to understand. */ export const createSavedObjects = async ({ - objects, // these already have the managed flag set + objects, accumulatedErrors, savedObjectsClient, importStateMap, @@ -109,7 +109,7 @@ export const createSavedObjects = async ({ ...object, ...(references && { references }), ...(originId && { originId }), - ...{ managed: managed ?? object.managed ?? false }, // trictly speaking this shouldn't be needed since the objects passed in should already have the flag set + ...{ managed: managed ?? object.managed ?? false }, }; }); @@ -178,7 +178,7 @@ export const createSavedObjects = async ({ overwrite, refresh, }) - ).saved_objects // already have managed specified + ).saved_objects : []; return { createdObjects: remappedResults.filter((obj) => !obj.error), diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index 927deb7dd6192..0b22ec93c2b2c 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -588,13 +588,13 @@ export default function ({ getService }: FtrProviderContext) { { space: SPACE_ID } ); await kibanaServer.importExport.load( - 'test/api_integration/fixtures/kbn_archiver/saved_objects/managedObjects.json', + '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/managedObjects.json', + 'test/api_integration/fixtures/kbn_archiver/saved_objects/managed_objects.json', { space: SPACE_ID } ); }); diff --git a/test/api_integration/apis/saved_objects/import.ts b/test/api_integration/apis/saved_objects/import.ts index 6c1f579abc436..507c692194f92 100644 --- a/test/api_integration/apis/saved_objects/import.ts +++ b/test/api_integration/apis/saved_objects/import.ts @@ -82,17 +82,11 @@ 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 415 when no file passed in', async () => { diff --git a/test/api_integration/fixtures/kbn_archiver/saved_objects/managedObjects.json b/test/api_integration/fixtures/kbn_archiver/saved_objects/managed_objects.json similarity index 100% rename from test/api_integration/fixtures/kbn_archiver/saved_objects/managedObjects.json rename to test/api_integration/fixtures/kbn_archiver/saved_objects/managed_objects.json From f753e3201e1f5fb47939fb19d39a63c8138956e2 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Thu, 27 Apr 2023 10:00:13 -0700 Subject: [PATCH 11/12] asserts create params have the correct properties --- .../src/import/import_saved_objects.test.ts | 240 ++++++------------ 1 file changed, 72 insertions(+), 168 deletions(-) diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts index 4312e4fab4533..8e6576de34e01 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/import_saved_objects.test.ts @@ -437,10 +437,15 @@ describe('#importSavedObjectsFromStream', () => { }; expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams); }); + }); - test('creates managed saved objects', async () => { - const options = setupOptions({ createNewCopies: true, managed: true }); - const collectedObjects = [createObject({ managed: true })]; + 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]], @@ -457,7 +462,6 @@ describe('#importSavedObjectsFromStream', () => { 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' }], @@ -469,182 +473,82 @@ describe('#importSavedObjectsFromStream', () => { importStateMap, overwrite, namespace, - managed: options.managed, + managed: undefined, }; expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams); - }); - }); - - describe('managed option', () => { - test('if not provided, does not override existing property when already present, defaults to false for others', async () => { - const obj1 = createObject({ type: 'foo', managed: true }); - const obj2 = createObject({ type: 'bar', title: 'bar-title', managed: false }); - - const options = setupOptions({ - createNewCopies: false, - getTypeImpl: (type) => { - if (type === 'foo') { - return { - management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` }, - }; - } - return { - management: { icon: `${type}-icon` }, - }; - }, - }); + }); // assert that the call to create will not override the object props. - mockCheckConflicts.mockResolvedValue({ - errors: [], - filteredObjects: [], - importStateMap: new Map(), - pendingOverwrites: new Set(), - }); - mockCreateSavedObjects.mockResolvedValue({ - errors: [], - createdObjects: [obj1, { ...obj2, managed: false }], // default applied in createSavedObjects + 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 }], + ]), }); - - const result = await importSavedObjectsFromStream(options); - // successResults only includes the imported object's type, id, managed, and destinationId (if a new one was generated) - const successResults = [ - { - type: obj1.type, - id: obj1.id, - meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` }, - managed: true, - }, - { - type: obj2.type, - id: obj2.id, - meta: { title: 'bar-title', icon: `${obj2.type}-icon` }, - managed: false, - }, - ]; - - expect(result).toEqual({ - success: true, - successCount: 2, - successResults, - warnings: [], + mockCheckReferenceOrigins.mockResolvedValue({ + importStateMap: new Map([['bar', { isOnlyReference: true, destinationId: 'newId' }]]), }); - }); // assert that the documents being imported retain their prop or have the default applied - - test('creates and converts objects from unmanaged to managed', async () => { - const obj1 = createObject({ type: 'foo', managed: false }); - const obj2 = createObject({ type: 'bar', title: 'bar-title' }); + mockValidateReferences.mockResolvedValue([errors[1]]); + mockRegenerateIds.mockReturnValue(new Map([['foo', { destinationId: `randomId1` }]])); - const options = setupOptions({ - createNewCopies: false, + 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, - 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.mockResolvedValue({ - errors: [], - createdObjects: [ - { ...obj1, managed: true }, - { ...obj2, managed: true }, - ], // make sure the default isn't applied in createSavedObjects - }); - - const result = await importSavedObjectsFromStream(options); - // successResults only includes the imported object's type, id, managed and destinationId (if a new one was generated) - const successResults = [ - { - type: obj1.type, - id: obj1.id, - meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` }, - managed: true, - }, - { - type: obj2.type, - id: obj2.id, - 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 + }; + expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams); + }); test('creates and converts objects from managed to unmanaged', async () => { - const obj1 = createObject({ type: 'foo', managed: true }); - const obj2 = createObject({ type: 'bar', title: 'bar-title' }); - - const options = setupOptions({ - createNewCopies: false, - managed: false, - 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(), + 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 }], + ]), }); - mockCreateSavedObjects.mockResolvedValue({ - errors: [], - createdObjects: [ - { ...obj1, managed: false }, - { ...obj2, managed: false }, - ], + mockCheckReferenceOrigins.mockResolvedValue({ + importStateMap: new Map([['bar', { isOnlyReference: true, destinationId: 'newId' }]]), }); + mockValidateReferences.mockResolvedValue([errors[1]]); + mockRegenerateIds.mockReturnValue(new Map([['foo', { destinationId: `randomId1` }]])); - const result = await importSavedObjectsFromStream(options); - // successResults only includes the imported object's type, id, managed and destinationId (if a new one was generated) - const successResults = [ - { - type: obj1.type, - id: obj1.id, - meta: { title: 'getTitle-foo', icon: `${obj1.type}-icon` }, - managed: false, - }, - { - type: obj2.type, - id: obj2.id, - 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 + 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); + }); }); }); From a396a9fbf37b9342f9e1540ea1dc3caf9b320321 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Thu, 27 Apr 2023 10:04:01 -0700 Subject: [PATCH 12/12] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandro Fernández Haro --- .../src/import/lib/collect_saved_objects.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts index c3801060f736c..d5362e8cdc581 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/collect_saved_objects.ts @@ -71,7 +71,7 @@ export async function collectSavedObjects({ ...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 opperation, applied to all objects being imported + // this is a bulk operation, applied to all objects being imported ...{ managed: managed ?? obj.managed ?? false }, }; }),