From e8776fe46b438bd5a504dc1358dce704dcd0971d Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Sat, 4 May 2024 17:48:58 -0700 Subject: [PATCH] [saved objects] enable deletion of saved objects by type if configured (#6443) * [saved objects] enable deletion of saved objects by type if configured Adds the following settings: ``` migrations.delete.enabled migrations.delete.types ``` `unknown` types already exist but the purpose of this type is for plugins that are disabled. OpenSearch Dashboards gets confused when a plugin is not defining a saved object type but the saved object exists. This can occur when migrating from a non-OSD version and there exists non-compatiable saved objects. If OSD is failing to migrate an index because of a document, I can now configure OSD to delete types of saved objects that I specified because I know that these types should not be carried over. resolves: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1040 Signed-off-by: Kawika Avilla * address comments Signed-off-by: Kawika Avilla * Changeset file for PR #6443 created/updated --------- Signed-off-by: Kawika Avilla Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: yujin-emma --- changelogs/fragments/6443.yml | 2 + config/opensearch_dashboards.yml | 8 +- .../migrations/core/index_migrator.test.ts | 222 + .../migrations/core/index_migrator.ts | 29 + .../migrations/core/migration_context.test.ts | 87 +- .../migrations/core/migration_context.ts | 57 +- .../core/migration_opensearch_client.test.ts | 4 + .../core/migration_opensearch_client.ts | 2 + .../opensearch_dashboards_migrator.test.ts | 42 +- .../opensearch_dashboards_migrator.ts | 3 + .../saved_objects/saved_objects_config.ts | 13 + .../bin/opensearch-dashboards-docker | 2 + .../dashboard_listing.test.tsx.snap | 8203 ++++++++++++++++- 13 files changed, 8660 insertions(+), 14 deletions(-) create mode 100644 changelogs/fragments/6443.yml diff --git a/changelogs/fragments/6443.yml b/changelogs/fragments/6443.yml new file mode 100644 index 000000000000..2de7191e0d09 --- /dev/null +++ b/changelogs/fragments/6443.yml @@ -0,0 +1,2 @@ +feat: +- Adds `migrations.delete` to delete saved objects by type during a migration ([#6443](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6443)) \ No newline at end of file diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index a8ef9638ce76..3066cff7ed0a 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -314,6 +314,12 @@ # Set the value to true to enable workspace feature # workspace.enabled: false +# Optional settings to specify saved object types to be deleted during migration. +# This feature can help address compatibility issues that may arise during the migration of saved objects, such as types defined by legacy applications. +# Please note, using this feature carries a risk. Deleting saved objects during migration could potentially lead to unintended data loss. Use with caution. +# migrations.delete.enabled: false +# migrations.delete.types: [] + # Set the value to true to enable Ui Metric Collectors in Usage Collector # This publishes the Application Usage and UI Metrics into the saved object, which can be accessed by /api/stats?extended=true&legacy=true&exclude_usage=false -# usageCollection.uiMetric.enabled: false \ No newline at end of file +# usageCollection.uiMetric.enabled: false diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 8b1f5df9640a..d55959ef769c 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -435,6 +435,228 @@ describe('IndexMigrator', () => { }); }); + test('deletes saved objects by type if configured', async () => { + const { client } = testOpts; + + const deleteType = 'delete_type'; + + const rawConfig = configMock.create(); + rawConfig.get.mockImplementation((path) => { + if (path === 'migrations.delete.enabled') { + return true; + } + if (path === 'migrations.delete.types') { + return [deleteType]; + } + }); + testOpts.opensearchDashboardsRawConfig = rawConfig; + + testOpts.mappingProperties = { foo: { type: 'text' } as any }; + + withIndex(client, { + index: { + '.kibana_1': { + aliases: {}, + mappings: { + properties: { + delete_type: { properties: { type: deleteType } }, + }, + }, + }, + }, + }); + + await new IndexMigrator(testOpts).migrate(); + + expect(client.indices.create).toHaveBeenCalledWith({ + body: { + mappings: { + dynamic: 'strict', + _meta: { + migrationMappingPropertyHashes: { + foo: '625b32086eb1d1203564cf85062dd22e', + migrationVersion: '4a1746014a75ade3a714e1db5763276f', + namespace: '2f4316de49999235636386fe51dc06c1', + namespaces: '2f4316de49999235636386fe51dc06c1', + originId: '2f4316de49999235636386fe51dc06c1', + references: '7997cf5a56cc02bdc9c93361bde732b0', + type: '2f4316de49999235636386fe51dc06c1', + updated_at: '00da57df13e94e9d98437d13ace4bfe0', + }, + }, + properties: { + foo: { type: 'text' }, + migrationVersion: { dynamic: 'true', type: 'object' }, + namespace: { type: 'keyword' }, + namespaces: { type: 'keyword' }, + originId: { type: 'keyword' }, + type: { type: 'keyword' }, + updated_at: { type: 'date' }, + references: { + type: 'nested', + properties: { + name: { type: 'keyword' }, + type: { type: 'keyword' }, + id: { type: 'keyword' }, + }, + }, + }, + }, + settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, + }, + index: '.kibana_2', + }); + }); + + test('retains saved objects by type if delete is not enabled', async () => { + const { client } = testOpts; + + const deleteType = 'delete_type'; + + const rawConfig = configMock.create(); + rawConfig.get.mockImplementation((path) => { + if (path === 'migrations.delete.enabled') { + return false; + } + if (path === 'migrations.delete.types') { + return [deleteType]; + } + }); + testOpts.opensearchDashboardsRawConfig = rawConfig; + + testOpts.mappingProperties = { foo: { type: 'text' } as any }; + + withIndex(client, { + index: { + '.kibana_1': { + aliases: {}, + mappings: { + properties: { + delete_type: { properties: { type: deleteType } }, + }, + }, + }, + }, + }); + + await new IndexMigrator(testOpts).migrate(); + + expect(client.indices.create).toHaveBeenCalledWith({ + body: { + mappings: { + dynamic: 'strict', + _meta: { + migrationMappingPropertyHashes: { + foo: '625b32086eb1d1203564cf85062dd22e', + migrationVersion: '4a1746014a75ade3a714e1db5763276f', + namespace: '2f4316de49999235636386fe51dc06c1', + namespaces: '2f4316de49999235636386fe51dc06c1', + originId: '2f4316de49999235636386fe51dc06c1', + references: '7997cf5a56cc02bdc9c93361bde732b0', + type: '2f4316de49999235636386fe51dc06c1', + updated_at: '00da57df13e94e9d98437d13ace4bfe0', + }, + }, + properties: { + delete_type: { dynamic: false, properties: {} }, + foo: { type: 'text' }, + migrationVersion: { dynamic: 'true', type: 'object' }, + namespace: { type: 'keyword' }, + namespaces: { type: 'keyword' }, + originId: { type: 'keyword' }, + type: { type: 'keyword' }, + updated_at: { type: 'date' }, + references: { + type: 'nested', + properties: { + name: { type: 'keyword' }, + type: { type: 'keyword' }, + id: { type: 'keyword' }, + }, + }, + }, + }, + settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, + }, + index: '.kibana_2', + }); + }); + + test('retains saved objects by type if delete types does not exist', async () => { + const { client } = testOpts; + + const deleteType = 'delete_type'; + const retainType = 'retain_type'; + + const rawConfig = configMock.create(); + rawConfig.get.mockImplementation((path) => { + if (path === 'migrations.delete.enabled') { + return true; + } + if (path === 'migrations.delete.types') { + return [deleteType]; + } + }); + testOpts.opensearchDashboardsRawConfig = rawConfig; + + testOpts.mappingProperties = { foo: { type: 'text' } as any }; + + withIndex(client, { + index: { + '.kibana_1': { + aliases: {}, + mappings: { + properties: { + retain_type: { properties: { type: retainType } }, + }, + }, + }, + }, + }); + + await new IndexMigrator(testOpts).migrate(); + + expect(client.indices.create).toHaveBeenCalledWith({ + body: { + mappings: { + dynamic: 'strict', + _meta: { + migrationMappingPropertyHashes: { + foo: '625b32086eb1d1203564cf85062dd22e', + migrationVersion: '4a1746014a75ade3a714e1db5763276f', + namespace: '2f4316de49999235636386fe51dc06c1', + namespaces: '2f4316de49999235636386fe51dc06c1', + originId: '2f4316de49999235636386fe51dc06c1', + references: '7997cf5a56cc02bdc9c93361bde732b0', + type: '2f4316de49999235636386fe51dc06c1', + updated_at: '00da57df13e94e9d98437d13ace4bfe0', + }, + }, + properties: { + retain_type: { dynamic: false, properties: {} }, + foo: { type: 'text' }, + migrationVersion: { dynamic: 'true', type: 'object' }, + namespace: { type: 'keyword' }, + namespaces: { type: 'keyword' }, + originId: { type: 'keyword' }, + type: { type: 'keyword' }, + updated_at: { type: 'date' }, + references: { + type: 'nested', + properties: { + name: { type: 'keyword' }, + type: { type: 'keyword' }, + id: { type: 'keyword' }, + }, + }, + }, + }, + settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, + }, + index: '.kibana_2', + }); + }); + test('points the alias at the dest index', async () => { const { client } = testOpts; diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index 1a616f8a2c7d..20784d6db8f6 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -28,6 +28,7 @@ * under the License. */ +import { DeleteByQueryRequest } from '@opensearch-project/opensearch/api/types'; import { diffMappings } from './build_active_mappings'; import * as Index from './opensearch_index'; import { migrateRawDocs } from './migrate_raw_docs'; @@ -123,6 +124,7 @@ async function migrateIndex(context: Context): Promise { const { client, alias, source, dest, log } = context; await deleteIndexTemplates(context); + await deleteSavedObjectsByType(context); log.info(`Creating index ${dest.indexName}.`); @@ -171,6 +173,33 @@ async function deleteIndexTemplates({ client, log, obsoleteIndexTemplatePattern return Promise.all(templateNames.map((name) => client.indices.deleteTemplate({ name: name! }))); } +/** + * Delete saved objects by type. If migrations.delete.types is specified, + * any saved objects that matches that type will be deleted. + */ +async function deleteSavedObjectsByType(context: Context) { + const { client, source, log, typesToDelete } = context; + if (!source.exists || !typesToDelete || typesToDelete.length === 0) { + return; + } + + log.info(`Removing saved objects of types: ${typesToDelete.join(', ')}`); + const params = { + index: source.indexName, + body: { + query: { + bool: { + should: [...typesToDelete.map((type) => ({ term: { type } }))], + }, + }, + }, + conflicts: 'proceed', + refresh: true, + } as DeleteByQueryRequest; + log.debug(`Delete by query params: ${JSON.stringify(params)}`); + return client.deleteByQuery(params); +} + /** * Moves all docs from sourceIndex to destIndex, migrating each as necessary. * This moves documents from the concrete index, rather than the alias, to prevent diff --git a/src/core/server/saved_objects/migrations/core/migration_context.test.ts b/src/core/server/saved_objects/migrations/core/migration_context.test.ts index 71db15842cd3..c30e6910cbf4 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.test.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.test.ts @@ -28,7 +28,8 @@ * under the License. */ -import { disableUnknownTypeMappingFields } from './migration_context'; +import { disableUnknownTypeMappingFields, deleteTypeMappingsFields } from './migration_context'; +import { configMock } from '../../../config/mocks'; describe('disableUnknownTypeMappingFields', () => { const sourceMappings = { @@ -97,3 +98,87 @@ describe('disableUnknownTypeMappingFields', () => { }); }); }); + +describe('deleteTypeMappingsFields', () => { + it('should delete specified type mappings fields', () => { + const targetMappings = { + properties: { + type1: { type: 'text' }, + type2: { type: 'keyword' }, + type3: { type: 'boolean' }, + }, + } as const; + + const rawConfig = configMock.create(); + rawConfig.get.mockImplementation((path) => { + if (path === 'migrations.delete.enabled') { + return true; + } + if (path === 'migrations.delete.types') { + return ['type1', 'type3']; + } + }); + + const updatedMappings = deleteTypeMappingsFields(targetMappings, rawConfig); + + expect(updatedMappings.properties).toEqual({ + type2: { type: 'keyword' }, + }); + }); + + it('should not delete any type mappings fields if delete is not enabled', () => { + const targetMappings = { + properties: { + type1: { type: 'text' }, + type2: { type: 'keyword' }, + type3: { type: 'boolean' }, + }, + } as const; + + const rawConfig = configMock.create(); + rawConfig.get.mockImplementation((path) => { + if (path === 'migrations.delete.enabled') { + return false; + } + if (path === 'migrations.delete.types') { + return ['type1', 'type3']; + } + }); + + const updatedMappings = deleteTypeMappingsFields(targetMappings, rawConfig); + + expect(updatedMappings.properties).toEqual({ + type1: { type: 'text' }, + type2: { type: 'keyword' }, + type3: { type: 'boolean' }, + }); + }); + + it('should not delete any type mappings fields if delete types are not specified', () => { + const targetMappings = { + properties: { + type1: { type: 'text' }, + type2: { type: 'keyword' }, + type3: { type: 'boolean' }, + }, + } as const; + + const rawConfig = configMock.create(); + rawConfig.get.mockImplementation((path) => { + if (path === 'migrations.delete.enabled') { + return true; + } + if (path === 'migrations.delete.types') { + return []; + } + }); + + const updatedMappings = deleteTypeMappingsFields(targetMappings, rawConfig); + + expect(updatedMappings.properties).toEqual({ + type1: { type: 'text' }, + type2: { type: 'keyword' }, + type3: { type: 'boolean' }, + }); + }); +}); diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts index 91114701d95f..987115c2ce08 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -66,6 +66,11 @@ export interface MigrationOpts { * prior to running migrations. For example: 'opensearch_dashboards_index_template*' */ obsoleteIndexTemplatePattern?: string; + /** + * If specified, types matching the specified list will be removed prior to + * running migrations. Useful for removing types that are not supported. + */ + typesToDelete?: string[]; opensearchDashboardsRawConfig?: Config; } @@ -84,6 +89,7 @@ export interface Context { scrollDuration: string; serializer: SavedObjectsSerializer; obsoleteIndexTemplatePattern?: string; + typesToDelete?: string[]; convertToAliasScript?: string; } @@ -114,6 +120,7 @@ export async function migrationContext(opts: MigrationOpts): Promise { scrollDuration: opts.scrollDuration, serializer: opts.serializer, obsoleteIndexTemplatePattern: opts.obsoleteIndexTemplatePattern, + typesToDelete: opts.typesToDelete, convertToAliasScript: opts.convertToAliasScript, }; } @@ -135,9 +142,12 @@ function createDestContext( typeMappingDefinitions: SavedObjectsTypeMappingDefinitions, opensearchDashboardsRawConfig?: Config ): Index.FullIndexInfo { - const targetMappings = disableUnknownTypeMappingFields( - buildActiveMappings(typeMappingDefinitions, opensearchDashboardsRawConfig), - source.mappings + const targetMappings = deleteTypeMappingsFields( + disableUnknownTypeMappingFields( + buildActiveMappings(typeMappingDefinitions, opensearchDashboardsRawConfig), + source.mappings + ), + opensearchDashboardsRawConfig ); return { @@ -162,7 +172,7 @@ function createDestContext( * type's mappings are set to `dynamic: false`. * * (Since we're using the source index mappings instead of looking at actual - * document types in the inedx, we potentially add more "unknown types" than + * document types in the index, we potentially add more "unknown types" than * what would be necessary to support migrating all the data over to the * target index.) * @@ -199,6 +209,43 @@ export function disableUnknownTypeMappingFields( }; } +/** + * This function is used to modify the target mappings object by deleting specified type mappings fields. + * + * The function operates under the following conditions: + * - It checks if the 'migrations.delete.enabled' configuration is set to true. + * - If true, it retrieves the 'migrations.delete.types' configuration + * - For each type, it deletes the corresponding property from the target mappings object. + * + * The purpose of this function is to allow for dynamic modification of the target mappings object + * based on the application's configuration. This can be useful in scenarios where certain type + * mappings are no longer needed and should be removed from the target mappings. + * + * @param {Object} targetMappings - The target mappings object to be modified. + * @param {Object} opensearchDashboardsRawConfig - The application's configuration object. + * @returns The mappings that should be applied to the target index. + */ +export function deleteTypeMappingsFields( + targetMappings: IndexMapping, + opensearchDashboardsRawConfig?: Config +) { + if (opensearchDashboardsRawConfig?.get('migrations.delete.enabled')) { + const deleteTypes = new Set(opensearchDashboardsRawConfig.get('migrations.delete.types')); + const newProperties = Object.keys(targetMappings.properties) + .filter((key) => !deleteTypes.has(key)) + .reduce((obj, key) => { + return { ...obj, [key]: targetMappings.properties[key] }; + }, {}); + + return { + ...targetMappings, + properties: newProperties, + }; + } + + return targetMappings; +} + /** * Gets the next index name in a sequence, based on specified current index's info. * We're using a numeric counter to create new indices. So, `.opensearch_dashboards_1`, `.opensearch_dashboards_2`, etc @@ -206,6 +253,6 @@ export function disableUnknownTypeMappingFields( */ function nextIndexName(indexName: string, alias: string) { const indexSuffix = (indexName.match(/[\d]+$/) || [])[0]; - const indexNum = parseInt(indexSuffix, 10) || 0; + const indexNum = parseInt(indexSuffix!, 10) || 0; return `${alias}_${indexNum + 1}`; } diff --git a/src/core/server/saved_objects/migrations/core/migration_opensearch_client.test.ts b/src/core/server/saved_objects/migrations/core/migration_opensearch_client.test.ts index 91f11cbd4878..8675f86c10ea 100644 --- a/src/core/server/saved_objects/migrations/core/migration_opensearch_client.test.ts +++ b/src/core/server/saved_objects/migrations/core/migration_opensearch_client.test.ts @@ -76,4 +76,8 @@ describe('MigrationOpenSearchClient', () => { expect(SavedObjectsErrorHelpers.isSavedObjectsClientError(e)).toBe(false); } }); + + it('should have the deleteByQuery method', () => { + expect(client.deleteByQuery).toBeDefined(); + }); }); diff --git a/src/core/server/saved_objects/migrations/core/migration_opensearch_client.ts b/src/core/server/saved_objects/migrations/core/migration_opensearch_client.ts index 7ab77d5a62dd..4cb4fef39de3 100644 --- a/src/core/server/saved_objects/migrations/core/migration_opensearch_client.ts +++ b/src/core/server/saved_objects/migrations/core/migration_opensearch_client.ts @@ -51,6 +51,7 @@ const methods = [ 'search', 'scroll', 'tasks.get', + 'deleteByQuery', ] as const; type MethodName = typeof methods[number]; @@ -77,6 +78,7 @@ export interface MigrationOpenSearchClient { tasks: { get: OpenSearchClient['tasks']['get']; }; + deleteByQuery: OpenSearchClient['deleteByQuery']; } export function createMigrationOpenSearchClient( diff --git a/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.test.ts b/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.test.ts index e65effdd8eaa..0a52b1947f2f 100644 --- a/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.test.ts @@ -89,6 +89,12 @@ describe('OpenSearchDashboardsMigrator', () => { const mappings = new OpenSearchDashboardsMigrator(options).getActiveMappings(); expect(mappings).toHaveProperty('properties.workspaces'); }); + + it('text field does not exist in the mappings when the feature is enabled', () => { + const options = mockOptions(false, false, { enabled: true, types: ['text'] }); + const mappings = new OpenSearchDashboardsMigrator(options).getActiveMappings(); + expect(mappings).not.toHaveProperty('properties.text'); + }); }); describe('runMigrations', () => { @@ -159,10 +165,14 @@ type MockedOptions = OpenSearchDashboardsMigratorOptions & { client: ReturnType; }; -const mockOptions = (isWorkspaceEnabled?: boolean, isPermissionControlEnabled?: boolean) => { +const mockOptions = ( + isWorkspaceEnabled?: boolean, + isPermissionControlEnabled?: boolean, + deleteConfig?: { enabled: boolean; types: string[] } +) => { const rawConfig = configMock.create(); rawConfig.get.mockReturnValue(false); - if (isWorkspaceEnabled || isPermissionControlEnabled) { + if (isWorkspaceEnabled || isPermissionControlEnabled || deleteConfig?.enabled) { rawConfig.get.mockReturnValue(true); } rawConfig.get.mockImplementation((path) => { @@ -178,6 +188,18 @@ const mockOptions = (isWorkspaceEnabled?: boolean, isPermissionControlEnabled?: } else { return false; } + } else if (path === 'migrations.delete.enabled') { + if (deleteConfig?.enabled) { + return true; + } else { + return false; + } + } else if (path === 'migrations.delete.types') { + if (deleteConfig?.enabled) { + return deleteConfig?.types; + } else { + return []; + } } else { return false; } @@ -209,6 +231,18 @@ const mockOptions = (isWorkspaceEnabled?: boolean, isPermissionControlEnabled?: }, migrations: {}, }, + { + name: 'testtype3', + hidden: false, + namespaceType: 'single', + indexPattern: 'other-index', + mappings: { + properties: { + name: { type: 'text' }, + }, + }, + migrations: {}, + }, ]), opensearchDashboardsConfig: { enabled: true, @@ -219,6 +253,10 @@ const mockOptions = (isWorkspaceEnabled?: boolean, isPermissionControlEnabled?: pollInterval: 20000, scrollDuration: '10m', skip: false, + delete: { + enabled: rawConfig.get('migrations.delete.enabled'), + types: rawConfig.get('migrations.delete.types'), + }, }, client: opensearchClientMock.createOpenSearchClient(), opensearchDashboardsRawConfig: rawConfig, diff --git a/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.ts b/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.ts index d6c119569a2e..e0e623f20f94 100644 --- a/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.ts +++ b/src/core/server/saved_objects/migrations/opensearch_dashboards/opensearch_dashboards_migrator.ts @@ -187,6 +187,9 @@ export class OpenSearchDashboardsMigrator { index === opensearchDashboardsIndexName ? 'opensearch_dashboards_index_template*' : undefined, + typesToDelete: this.savedObjectsConfig.delete.enabled + ? this.savedObjectsConfig.delete.types + : undefined, convertToAliasScript: indexMap[index].script, opensearchDashboardsRawConfig: this.opensearchDashboardsRawConfig, }); diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index e6ffaefb8a59..ccf95b21cd45 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -39,6 +39,19 @@ export const savedObjectsMigrationConfig = { scrollDuration: schema.string({ defaultValue: '15m' }), pollInterval: schema.number({ defaultValue: 1500 }), skip: schema.boolean({ defaultValue: false }), + delete: schema.object( + { + enabled: schema.boolean({ defaultValue: false }), + types: schema.arrayOf(schema.string(), { defaultValue: [] }), + }, + { + validate(value) { + if (value.enabled === true && value.types.length === 0) { + return 'delete types cannot be empty when delete is enabled'; + } + }, + } + ), }), }; diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker index 124a5e074842..c9cf5d1213c0 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker @@ -76,6 +76,8 @@ opensearch_dashboards_vars=( map.tilemap.options.minZoom map.tilemap.options.subdomains map.tilemap.url + migrations.delete.enabled + migrations.delete.types monitoring.cluster_alerts.email_notifications.email_address monitoring.enabled monitoring.opensearchDashboards.collection.enabled diff --git a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap index b0ee4d34705f..c853b9fc7941 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -1129,11 +1129,1481 @@ exports[`dashboard listing hideWriteControls 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -2335,11 +3805,2151 @@ exports[`dashboard listing render table listing with initial filters from URL 1` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "dashboard", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -3541,11 +7151,274 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = data-test-subj="dashboardLandingPage" >
+ > + + +
+ + + + } + body={ + +

+ +

+

+ + + , + } + } + /> +

+
+ } + iconType="dashboardApp" + title={ +

+ +

+ } + > +
+ + + + +
+ + +

+ + Create your first dashboard + +

+
+ + + +
+ + +
+

+ + You can combine data views from any OpenSearch Dashboards app into one dashboard and see everything in one place. + +

+

+ + + , + } + } + > + New to OpenSearch Dashboards? + + + + to take a test drive. + +

+
+
+ + + +
+ + + + + + +
+ +
+ + +
@@ -4747,11 +8620,2111 @@ exports[`dashboard listing renders table rows 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -5953,11 +11926,2231 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + > +
+
+ + + + Listing limit exceeded + + +
+ +
+ +
+

+ + + , + "entityNamePlural": "dashboards", + "listingLimitText": + listingLimit + , + "listingLimitValue": 1, + "totalItems": 2, + } + } + > + You have 2 dashboards, but your + + listingLimit + + setting prevents the table below from displaying more than 1. You can change this setting under + + + + Advanced Settings + + + + . + +

+
+
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +