diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb066f49315..5f7d089ba687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### 📈 Features/Enhancements - [Multiple Datasource] Add TLS configuration for multiple data sources ([#6171](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6171)) +- [Multiple DataSource] Do not support import data source object to Local cluster when not enable data source ([#6395](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6395)) ### 🐛 Bug Fixes diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts index fff5b60c89cc..ea4ac66b1a0b 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts @@ -96,11 +96,12 @@ describe('#importSavedObjectsFromStream', () => { let savedObjectsClient: jest.Mocked; let typeRegistry: jest.Mocked; const namespace = 'some-namespace'; - const testDataSourceId = 'some-datasource'; + const testDataSourceId = uuidv4(); const setupOptions = ( createNewCopies: boolean = false, - dataSourceId: string | undefined = undefined + dataSourceId: string | undefined = undefined, + dataSourceEnabled: boolean | undefined = false ): SavedObjectsImportOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); @@ -135,6 +136,17 @@ describe('#importSavedObjectsFromStream', () => { attributes: { title: 'some-title' }, }; }; + + const createDataSourceObject = (): SavedObject<{ + title: string; + }> => { + return { + type: 'data-source', + id: uuidv4(), + references: [], + attributes: { title: 'some-title' }, + }; + }; const createError = (): SavedObjectsImportError => { const title = 'some-title'; return { @@ -589,5 +601,94 @@ describe('#importSavedObjectsFromStream', () => { const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id })); expect(result).toEqual({ success: false, successCount: 0, errors: expectedErrors }); }); + + test('early return if import data source objects to non-MDS cluster', async () => { + const options = setupOptions(false, testDataSourceId, false); + const dsObj = createDataSourceObject(); + const dsExportedObj = createObject(testDataSourceId); + const collectedObjects = [dsObj, dsExportedObj]; + + const errors = [ + { + type: dsObj.type, + id: dsObj.id, + title: dsObj.attributes.title, + meta: { title: dsObj.attributes.title }, + error: { type: 'unsupported_type' }, + }, + { + type: dsExportedObj.type, + id: dsExportedObj.id, + title: dsExportedObj.attributes.title, + meta: { title: dsExportedObj.attributes.title }, + error: { type: 'unsupported_type' }, + }, + ]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), + }); + const result = await importSavedObjectsFromStream(options); + const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id })); + expect(result).toEqual({ success: false, successCount: 0, errors: expectedErrors }); + }); + + test('early return if import mixed non/data source objects to non-MDS cluster', async () => { + const options = setupOptions(false, testDataSourceId, false); + const dsObj = createDataSourceObject(); + const dsExportedObj = createObject(testDataSourceId); + const nonDsExportedObj = createObject(); + const collectedObjects = [dsObj, dsExportedObj, nonDsExportedObj]; + + const errors = [ + { + type: dsObj.type, + id: dsObj.id, + title: dsObj.attributes.title, + meta: { title: dsObj.attributes.title }, + error: { type: 'unsupported_type' }, + }, + { + type: dsExportedObj.type, + id: dsExportedObj.id, + title: dsExportedObj.attributes.title, + meta: { title: dsExportedObj.attributes.title }, + error: { type: 'unsupported_type' }, + }, + ]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), + }); + const result = await importSavedObjectsFromStream(options); + const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id })); + expect(result).toEqual({ success: false, successCount: 0, errors: expectedErrors }); + }); + + test('early return if import single data source objects to non-MDS cluster', async () => { + const options = setupOptions(false, testDataSourceId, false); + const dsObj = createDataSourceObject(); + const collectedObjects = [dsObj]; + + const errors = [ + { + type: dsObj.type, + id: dsObj.id, + title: dsObj.attributes.title, + meta: { title: dsObj.attributes.title }, + error: { type: 'unsupported_type' }, + }, + ]; + getMockFn(collectSavedObjects).mockResolvedValue({ + errors: [], + collectedObjects, + importIdMap: new Map(), + }); + const result = await importSavedObjectsFromStream(options); + const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id })); + expect(result).toEqual({ success: false, successCount: 0, errors: expectedErrors }); + }); }); }); diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts index e82b4e634e0f..cfd091149004 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -33,6 +33,7 @@ import { SavedObjectsImportError, SavedObjectsImportResponse, SavedObjectsImportOptions, + SavedObjectsImportUnsupportedTypeError, } from './types'; import { validateReferences } from './validate_references'; import { checkOriginConflicts } from './check_origin_conflicts'; @@ -40,6 +41,7 @@ import { createSavedObjects } from './create_saved_objects'; import { checkConflicts } from './check_conflicts'; import { regenerateIds } from './regenerate_ids'; import { checkConflictsForDataSource } from './check_conflict_for_data_source'; +import { isSavedObjectWithDataSource } from './validate_object_id'; /** * Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more @@ -58,6 +60,7 @@ export async function importSavedObjectsFromStream({ dataSourceId, dataSourceTitle, workspaces, + dataSourceEnabled, }: SavedObjectsImportOptions): Promise { let errorAccumulator: SavedObjectsImportError[] = []; const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); @@ -69,6 +72,28 @@ export async function importSavedObjectsFromStream({ supportedTypes, dataSourceId, }); + // if not enable data_source, throw error early + if (!dataSourceEnabled) { + const notSupportedErrors: SavedObjectsImportError[] = collectSavedObjectsResult.collectedObjects.reduce( + (errors: SavedObjectsImportError[], obj) => { + if (obj.type === 'data-source' || isSavedObjectWithDataSource(obj.id)) { + const error: SavedObjectsImportUnsupportedTypeError = { type: 'unsupported_type' }; + const { title } = obj.attributes; + errors.push({ error, type: obj.type, id: obj.id, title, meta: { title } }); + } + return errors; // Return the accumulator in each iteration + }, + [] + ); + if (notSupportedErrors?.length > 0) { + return { + successCount: 0, + success: false, + errors: notSupportedErrors, + }; + } + } + errorAccumulator = [...errorAccumulator, ...collectSavedObjectsResult.errors]; /** Map of all IDs for objects that we are attempting to import; each value is empty by default */ let importIdMap = collectSavedObjectsResult.importIdMap; diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 426d4cfde86c..8612870fcf56 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -189,7 +189,7 @@ export interface SavedObjectsImportOptions { createNewCopies: boolean; dataSourceId?: string; dataSourceTitle?: string; - /** if specified, will import in given workspaces */ + dataSourceEnabled?: boolean; workspaces?: SavedObjectsBaseOptions['workspaces']; } diff --git a/src/core/server/saved_objects/import/validate_object_id.test.ts b/src/core/server/saved_objects/import/validate_object_id.test.ts new file mode 100644 index 000000000000..2f0cb3c6487a --- /dev/null +++ b/src/core/server/saved_objects/import/validate_object_id.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { isSavedObjectWithDataSource } from './validate_object_id'; + +describe('isObjectWithDataSource', () => { + test('should return false for valid object with data source ID but in wrong format', () => { + // Valid ID with two parts separated by underscore, and both parts being UUIDs + const inValidId = 'invalid_uuid_1234-invalid_uuid_5678'; + expect(isSavedObjectWithDataSource(inValidId)).toBe(false); + }); + + test('should return false for invalid IDs', () => { + // Missing underscore + const invalidId1 = 'missingunderscore'; + expect(isSavedObjectWithDataSource(invalidId1)).toBe(false); + + // Invalid UUID in the second part + const invalidId2 = 'valid_uuid_1234-invalid_uuid'; + expect(isSavedObjectWithDataSource(invalidId2)).toBe(false); + + // Missing second part + const invalidId3 = 'valid_uuid_1234'; + expect(isSavedObjectWithDataSource(invalidId3)).toBe(false); + + // More than two parts + const invalidId4 = 'valid_uuid_1234-valid_uuid_5678-extra_part'; + expect(isSavedObjectWithDataSource(invalidId4)).toBe(false); + }); + + test('should return false for non-UUID parts', () => { + // First part is not a UUID + const invalidId1 = 'not_a_uuid_valid_uuid_1234'; + expect(isSavedObjectWithDataSource(invalidId1)).toBe(false); + + // Second part is not a UUID + const invalidId2 = 'valid_uuid_1234_not_a_uuid'; + expect(isSavedObjectWithDataSource(invalidId2)).toBe(false); + + // Both parts are not UUIDs + const invalidId3 = 'not_a_uuid_not_a_uuid'; + expect(isSavedObjectWithDataSource(invalidId3)).toBe(false); + }); + + test('should return false for string with underscore but not with UUID', () => { + // First part is not a UUID + const invalidId = 'saved_object_with_index_pattern_conflict'; + expect(isSavedObjectWithDataSource(invalidId)).toBe(false); + }); + + test('should return false for string with underscore but with three UUIDs', () => { + // First part is not a UUID + const invalidId = + '7cbd2350-2223-11e8-b802-5bcf64c2cfb4_7cbd2350-2223-11e8-b802-5bcf64c2cfb4_7cbd2350-2223-11e8-b802-5bcf64c2cfb4'; + expect(isSavedObjectWithDataSource(invalidId)).toBe(false); + }); +}); diff --git a/src/core/server/saved_objects/import/validate_object_id.ts b/src/core/server/saved_objects/import/validate_object_id.ts new file mode 100644 index 000000000000..9a496c4d572a --- /dev/null +++ b/src/core/server/saved_objects/import/validate_object_id.ts @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * When enable multiple data source, exported objects from a data source will maintain object id like + * "69a34b00-9ee8-11e7-8711-e7a007dcef99_7cbd2350-2223-11e8-b802-5bcf64c2cfb4" + * two UUIDs are connected with a underscore, + * before the underscore, the UUID represents the data source + * after the underscore, the UUID is the original object id + * when disable multiple data source, the exported object from local cluster will look like 7cbd2350-2223-11e8-b802-5bcf64c2cfb4 + * we can use this format to tell out whether a single object is exported from MDS enabled/disabled cluster + * + * This file to going to group some validate function to tell source of object based on the object id + */ + +/** + * + * @param candidate: string without underscore + * @returns + */ +const isUUID = (candidate: string): boolean => { + // Regular expression pattern for UUID + const uuidPattern: RegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + return uuidPattern.test(candidate); +}; + +/** + * + * @param id single object id + * @returns + */ +export const isSavedObjectWithDataSource = (id: string): boolean => { + const idParts = id.split('_'); + /** + * check with the + */ + return idParts && idParts.length === 2 && idParts.every(isUUID); +}; diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 1fc739ea168c..a2a5bdcacd7a 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -64,6 +64,7 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) workspaces: schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) ), + dataSourceEnabled: schema.maybe(schema.boolean({ defaultValue: false })), }, { validate: (object) => { @@ -116,6 +117,8 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) workspaces = [workspaces]; } + const dataSourceEnabled = req.query.dataSourceEnabled; + const result = await importSavedObjectsFromStream({ savedObjectsClient: context.core.savedObjects.client, typeRegistry: context.core.savedObjects.typeRegistry, @@ -126,6 +129,7 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) dataSourceId, dataSourceTitle, workspaces, + dataSourceEnabled, }); return res.ok({ body: result }); diff --git a/src/plugins/saved_objects_management/public/lib/import_file.ts b/src/plugins/saved_objects_management/public/lib/import_file.ts index 3753a8251e10..f5156cd94f1d 100644 --- a/src/plugins/saved_objects_management/public/lib/import_file.ts +++ b/src/plugins/saved_objects_management/public/lib/import_file.ts @@ -41,7 +41,8 @@ export async function importFile( http: HttpStart, file: File, { createNewCopies, overwrite }: ImportMode, - selectedDataSourceId?: string + selectedDataSourceId?: string, + dataSourceEnabled?: boolean ) { const formData = new FormData(); formData.append('file', file); @@ -49,6 +50,9 @@ export async function importFile( if (selectedDataSourceId) { query.dataSourceId = selectedDataSourceId; } + if (dataSourceEnabled) { + query.dataSourceEnabled = dataSourceEnabled; + } return await http.post('/api/saved_objects/_import', { body: formData, headers: { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index 4237582ea53a..d68f17a64f6c 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -221,6 +221,7 @@ describe('Flyout', () => { createNewCopies: true, overwrite: true, }, + undefined, undefined ); expect(component.state()).toMatchObject({ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index b2542c4a56c0..fb2f98855364 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -195,13 +195,19 @@ export class Flyout extends Component { * Does the initial import of a file, resolveImportErrors then handles errors and retries */ import = async () => { - const { http } = this.props; + const { http, dataSourceEnabled } = this.props; const { file, importMode, selectedDataSourceId } = this.state; this.setState({ status: 'loading', error: undefined }); // Import the file try { - const response = await importFile(http, file!, importMode, selectedDataSourceId); + const response = await importFile( + http, + file!, + importMode, + selectedDataSourceId, + dataSourceEnabled + ); this.setState(processImportResponse(response), () => { // Resolve import errors right away if there's no index patterns to match // This will ask about overwriting each object, etc diff --git a/src/plugins/workspace/server/saved_objects/integration_tests/saved_objects_wrapper_for_check_workspace_conflict.test.ts b/src/plugins/workspace/server/saved_objects/integration_tests/saved_objects_wrapper_for_check_workspace_conflict.test.ts index 1700c51ee11e..7967992690d0 100644 --- a/src/plugins/workspace/server/saved_objects/integration_tests/saved_objects_wrapper_for_check_workspace_conflict.test.ts +++ b/src/plugins/workspace/server/saved_objects/integration_tests/saved_objects_wrapper_for_check_workspace_conflict.test.ts @@ -493,7 +493,7 @@ describe('saved_objects_wrapper_for_check_workspace_conflict integration test', const importWithWorkspacesResult = await osdTestServer.request .post( root, - `/api/saved_objects/_import?workspaces=${createdFooWorkspace.id}&overwrite=true` + `/api/saved_objects/_import?workspaces=${createdFooWorkspace.id}&overwrite=true&dataSourceEnabled=true` ) .attach( 'file',