diff --git a/src/core/server/saved_objects/deprecations/deprecation_factory.ts b/src/core/server/saved_objects/deprecations/deprecation_factory.ts new file mode 100644 index 00000000000000..670b43bfa7c771 --- /dev/null +++ b/src/core/server/saved_objects/deprecations/deprecation_factory.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { RegisterDeprecationsConfig } from '../../deprecations'; +import type { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import type { SavedObjectConfig } from '../saved_objects_config'; +import type { KibanaConfigType } from '../../kibana_config'; +import { getUnknownTypesDeprecations } from './unknown_object_types'; + +interface GetDeprecationProviderOptions { + typeRegistry: ISavedObjectTypeRegistry; + savedObjectsConfig: SavedObjectConfig; + kibanaConfig: KibanaConfigType; + kibanaVersion: string; +} + +export const getSavedObjectsDeprecationsProvider = ( + config: GetDeprecationProviderOptions +): RegisterDeprecationsConfig => { + return { + getDeprecations: async (context) => { + return [ + ...(await getUnknownTypesDeprecations({ + ...config, + esClient: context.esClient, + })), + ]; + }, + }; +}; diff --git a/src/core/server/saved_objects/deprecations/index.ts b/src/core/server/saved_objects/deprecations/index.ts new file mode 100644 index 00000000000000..5cf1590ad43d29 --- /dev/null +++ b/src/core/server/saved_objects/deprecations/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getSavedObjectsDeprecationsProvider } from './deprecation_factory'; +export { deleteUnknownTypeObjects } from './unknown_object_types'; diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.test.mocks.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.test.mocks.ts new file mode 100644 index 00000000000000..312204ad778468 --- /dev/null +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.test.mocks.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const getIndexForTypeMock = jest.fn(); + +jest.doMock('../service/lib/get_index_for_type', () => ({ + getIndexForType: getIndexForTypeMock, +})); diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts new file mode 100644 index 00000000000000..d7ea73456e236a --- /dev/null +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getIndexForTypeMock } from './unknown_object_types.test.mocks'; + +import { estypes } from '@elastic/elasticsearch'; +import { deleteUnknownTypeObjects, getUnknownTypesDeprecations } from './unknown_object_types'; +import { typeRegistryMock } from '../saved_objects_type_registry.mock'; +import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; +import type { KibanaConfigType } from '../../kibana_config'; +import type { SavedObjectConfig } from '../saved_objects_config'; +import { SavedObjectsType } from 'kibana/server'; + +const createSearchResponse = (count: number): estypes.SearchResponse => { + return { + hits: { + total: count, + max_score: 0, + hits: new Array(count).fill({}), + }, + } as estypes.SearchResponse; +}; + +describe('unknown saved object types deprecation', () => { + const kibanaVersion = '8.0.0'; + + let typeRegistry: ReturnType; + let esClient: ReturnType; + let kibanaConfig: KibanaConfigType; + let savedObjectsConfig: SavedObjectConfig; + + beforeEach(() => { + typeRegistry = typeRegistryMock.create(); + esClient = elasticsearchClientMock.createScopedClusterClient(); + + typeRegistry.getAllTypes.mockReturnValue([ + { name: 'foo' }, + { name: 'bar' }, + ] as SavedObjectsType[]); + getIndexForTypeMock.mockImplementation(({ type }: { type: string }) => `${type}-index`); + + kibanaConfig = { + index: '.kibana', + enabled: true, + }; + + savedObjectsConfig = { + migration: { + enableV2: true, + }, + } as SavedObjectConfig; + }); + + afterEach(() => { + getIndexForTypeMock.mockReset(); + }); + + describe('getUnknownTypesDeprecations', () => { + beforeEach(() => { + esClient.asInternalUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(createSearchResponse(0)) + ); + }); + + it('calls `esClient.asInternalUser.search` with the correct parameters', async () => { + await getUnknownTypesDeprecations({ + savedObjectsConfig, + esClient, + typeRegistry, + kibanaConfig, + kibanaVersion, + }); + + expect(esClient.asInternalUser.search).toHaveBeenCalledTimes(1); + expect(esClient.asInternalUser.search).toHaveBeenCalledWith({ + index: ['foo-index', 'bar-index'], + body: { + size: 10000, + query: { + bool: { + must_not: [{ term: { type: 'foo' } }, { term: { type: 'bar' } }], + }, + }, + }, + }); + }); + + it('returns no deprecation if no unknown type docs are found', async () => { + esClient.asInternalUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(createSearchResponse(0)) + ); + + const deprecations = await getUnknownTypesDeprecations({ + savedObjectsConfig, + esClient, + typeRegistry, + kibanaConfig, + kibanaVersion, + }); + + expect(deprecations.length).toEqual(0); + }); + + it('returns a deprecation if any unknown type docs are found', async () => { + esClient.asInternalUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(createSearchResponse(1)) + ); + + const deprecations = await getUnknownTypesDeprecations({ + savedObjectsConfig, + esClient, + typeRegistry, + kibanaConfig, + kibanaVersion, + }); + + expect(deprecations.length).toEqual(1); + expect(deprecations[0]).toEqual({ + title: expect.any(String), + message: expect.any(String), + level: 'critical', + requireRestart: false, + deprecationType: undefined, + correctiveActions: { + manualSteps: expect.any(Array), + api: { + path: '/internal/saved_objects/deprecations/_delete_unknown_types', + method: 'POST', + body: {}, + }, + }, + }); + }); + }); + + describe('deleteUnknownTypeObjects', () => { + it('calls `esClient.asInternalUser.search` with the correct parameters', async () => { + await deleteUnknownTypeObjects({ + savedObjectsConfig, + esClient, + typeRegistry, + kibanaConfig, + kibanaVersion, + }); + + expect(esClient.asInternalUser.deleteByQuery).toHaveBeenCalledTimes(1); + expect(esClient.asInternalUser.deleteByQuery).toHaveBeenCalledWith({ + index: ['foo-index', 'bar-index'], + wait_for_completion: false, + body: { + query: { + bool: { + must_not: [{ term: { type: 'foo' } }, { term: { type: 'bar' } }], + }, + }, + }, + }); + }); + }); +}); diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.ts new file mode 100644 index 00000000000000..c966e621ca6055 --- /dev/null +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { estypes } from '@elastic/elasticsearch'; +import { i18n } from '@kbn/i18n'; +import type { DeprecationsDetails } from '../../deprecations'; +import { IScopedClusterClient } from '../../elasticsearch'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { SavedObjectsRawDocSource } from '../serialization'; +import type { KibanaConfigType } from '../../kibana_config'; +import type { SavedObjectConfig } from '../saved_objects_config'; +import { getIndexForType } from '../service/lib'; + +interface UnknownTypesDeprecationOptions { + typeRegistry: ISavedObjectTypeRegistry; + esClient: IScopedClusterClient; + kibanaConfig: KibanaConfigType; + savedObjectsConfig: SavedObjectConfig; + kibanaVersion: string; +} + +const getKnownTypes = (typeRegistry: ISavedObjectTypeRegistry) => + typeRegistry.getAllTypes().map((type) => type.name); + +const getTargetIndices = ({ + types, + typeRegistry, + kibanaVersion, + kibanaConfig, + savedObjectsConfig, +}: { + types: string[]; + typeRegistry: ISavedObjectTypeRegistry; + savedObjectsConfig: SavedObjectConfig; + kibanaConfig: KibanaConfigType; + kibanaVersion: string; +}) => { + return [ + ...new Set( + types.map((type) => + getIndexForType({ + type, + typeRegistry, + migV2Enabled: savedObjectsConfig.migration.enableV2, + kibanaVersion, + defaultIndex: kibanaConfig.index, + }) + ) + ), + ]; +}; + +const getUnknownTypesQuery = (knownTypes: string[]): estypes.QueryDslQueryContainer => { + return { + bool: { + must_not: knownTypes.map((type) => ({ + term: { type }, + })), + }, + }; +}; + +const getUnknownSavedObjects = async ({ + typeRegistry, + esClient, + kibanaConfig, + savedObjectsConfig, + kibanaVersion, +}: UnknownTypesDeprecationOptions) => { + const knownTypes = getKnownTypes(typeRegistry); + const targetIndices = getTargetIndices({ + types: knownTypes, + typeRegistry, + kibanaConfig, + kibanaVersion, + savedObjectsConfig, + }); + const query = getUnknownTypesQuery(knownTypes); + + const { body } = await esClient.asInternalUser.search({ + index: targetIndices, + body: { + size: 10000, + query, + }, + }); + const { hits: unknownDocs } = body.hits; + + return unknownDocs.map((doc) => ({ id: doc._id, type: doc._source?.type ?? 'unknown' })); +}; + +export const getUnknownTypesDeprecations = async ( + options: UnknownTypesDeprecationOptions +): Promise => { + const deprecations: DeprecationsDetails[] = []; + const unknownDocs = await getUnknownSavedObjects(options); + if (unknownDocs.length) { + deprecations.push({ + title: i18n.translate('core.savedObjects.deprecations.unknownTypes.title', { + defaultMessage: 'Saved objects with unknown types are present in Kibana system indices', + }), + message: i18n.translate('core.savedObjects.deprecations.unknownTypes.message', { + defaultMessage: + '{objectCount, plural, one {# object} other {# objects}} with unknown types {objectCount, plural, one {was} other {were}} found in Kibana system indices. ' + + 'Upgrading with unknown savedObject types is no longer supported. ' + + `To ensure that upgrades will succeed in the future, either re-enable plugins or delete these documents from the Kibana indices`, + values: { + objectCount: unknownDocs.length, + }, + }), + level: 'critical', + requireRestart: false, + deprecationType: undefined, // not config nor feature... + correctiveActions: { + manualSteps: [ + i18n.translate('core.savedObjects.deprecations.unknownTypes.manualSteps.1', { + defaultMessage: 'Enable disabled plugins then restart Kibana.', + }), + i18n.translate('core.savedObjects.deprecations.unknownTypes.manualSteps.2', { + defaultMessage: + 'If no plugins are disabled, or if enabling them does not fix the issue, delete the documents.', + }), + ], + api: { + path: '/internal/saved_objects/deprecations/_delete_unknown_types', + method: 'POST', + body: {}, + }, + }, + }); + } + return deprecations; +}; + +interface DeleteUnknownTypesOptions { + typeRegistry: ISavedObjectTypeRegistry; + esClient: IScopedClusterClient; + kibanaConfig: KibanaConfigType; + savedObjectsConfig: SavedObjectConfig; + kibanaVersion: string; +} + +export const deleteUnknownTypeObjects = async ({ + esClient, + typeRegistry, + kibanaConfig, + savedObjectsConfig, + kibanaVersion, +}: DeleteUnknownTypesOptions) => { + const knownTypes = getKnownTypes(typeRegistry); + const targetIndices = getTargetIndices({ + types: knownTypes, + typeRegistry, + kibanaConfig, + kibanaVersion, + savedObjectsConfig, + }); + const query = getUnknownTypesQuery(knownTypes); + + await esClient.asInternalUser.deleteByQuery({ + index: targetIndices, + wait_for_completion: false, + body: { + query, + }, + }); +}; diff --git a/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts b/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts new file mode 100644 index 00000000000000..a9e1a41f01d916 --- /dev/null +++ b/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IRouter } from '../../../http'; +import { catchAndReturnBoomErrors } from '../utils'; +import { deleteUnknownTypeObjects } from '../../deprecations'; +import { SavedObjectConfig } from '../../saved_objects_config'; +import { KibanaConfigType } from '../../../kibana_config'; + +interface RouteDependencies { + config: SavedObjectConfig; + kibanaConfig: KibanaConfigType; + kibanaVersion: string; +} + +export const registerDeleteUnknownTypesRoute = ( + router: IRouter, + { config, kibanaConfig, kibanaVersion }: RouteDependencies +) => { + router.post( + { + path: '/deprecations/_delete_unknown_types', + validate: false, + }, + catchAndReturnBoomErrors(async (context, req, res) => { + await deleteUnknownTypeObjects({ + esClient: context.core.elasticsearch.client, + typeRegistry: context.core.savedObjects.typeRegistry, + savedObjectsConfig: config, + kibanaConfig, + kibanaVersion, + }); + return res.ok({ + body: { + success: true, + }, + }); + }) + ); +}; diff --git a/src/core/server/saved_objects/routes/deprecations/index.ts b/src/core/server/saved_objects/routes/deprecations/index.ts new file mode 100644 index 00000000000000..07e6b987d7c609 --- /dev/null +++ b/src/core/server/saved_objects/routes/deprecations/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { registerDeleteUnknownTypesRoute } from './delete_unknown_types'; diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts index 89240544f5e022..ef462d3726845e 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -27,6 +27,8 @@ import { registerResolveImportErrorsRoute } from './resolve_import_errors'; import { registerMigrateRoute } from './migrate'; import { registerLegacyImportRoute } from './legacy_import_export/import'; import { registerLegacyExportRoute } from './legacy_import_export/export'; +import { registerDeleteUnknownTypesRoute } from './deprecations'; +import { KibanaConfigType } from '../../kibana_config'; export function registerRoutes({ http, @@ -35,6 +37,7 @@ export function registerRoutes({ config, migratorPromise, kibanaVersion, + kibanaConfig, }: { http: InternalHttpServiceSetup; coreUsageData: CoreUsageDataSetup; @@ -42,6 +45,7 @@ export function registerRoutes({ config: SavedObjectConfig; migratorPromise: Promise; kibanaVersion: string; + kibanaConfig: KibanaConfigType; }) { const router = http.createRouter('/api/saved_objects/'); @@ -70,4 +74,5 @@ export function registerRoutes({ const internalRouter = http.createRouter('/internal/saved_objects/'); registerMigrateRoute(internalRouter, migratorPromise); + registerDeleteUnknownTypesRoute(internalRouter, { config, kibanaConfig, kibanaVersion }); } diff --git a/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts b/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts new file mode 100644 index 00000000000000..fef2b2d5870e0c --- /dev/null +++ b/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import supertest from 'supertest'; +import { UnwrapPromise } from '@kbn/utility-types'; +import { registerDeleteUnknownTypesRoute } from '../deprecations'; +import { elasticsearchServiceMock } from '../../../../../core/server/elasticsearch/elasticsearch_service.mock'; +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import { setupServer } from '../test_utils'; +import { KibanaConfigType } from '../../../kibana_config'; +import { SavedObjectConfig } from '../../saved_objects_config'; +import { SavedObjectsType } from 'kibana/server'; + +type SetupServerReturn = UnwrapPromise>; + +describe('POST /internal/saved_objects/deprecations/_delete_unknown_types', () => { + const kibanaVersion = '8.0.0'; + const kibanaConfig: KibanaConfigType = { + enabled: true, + index: '.kibana', + }; + const config: SavedObjectConfig = { + maxImportExportSize: 10000, + maxImportPayloadBytes: 24000000, + migration: { + enableV2: true, + } as SavedObjectConfig['migration'], + }; + + let server: SetupServerReturn['server']; + let httpSetup: SetupServerReturn['httpSetup']; + let handlerContext: SetupServerReturn['handlerContext']; + let typeRegistry: ReturnType; + let elasticsearchClient: ReturnType; + + beforeEach(async () => { + ({ server, httpSetup, handlerContext } = await setupServer()); + elasticsearchClient = elasticsearchServiceMock.createScopedClusterClient(); + typeRegistry = typeRegistryMock.create(); + + typeRegistry.getAllTypes.mockReturnValue([{ name: 'known-type' } as SavedObjectsType]); + typeRegistry.getIndex.mockImplementation((type) => `${type}-index`); + + handlerContext.savedObjects.typeRegistry = typeRegistry; + handlerContext.elasticsearch.client.asCurrentUser = elasticsearchClient.asCurrentUser; + handlerContext.elasticsearch.client.asInternalUser = elasticsearchClient.asInternalUser; + + const router = httpSetup.createRouter('/internal/saved_objects/'); + registerDeleteUnknownTypesRoute(router, { + kibanaVersion, + kibanaConfig, + config, + }); + + await server.start(); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('formats successful response', async () => { + const result = await supertest(httpSetup.server.listener) + .post('/internal/saved_objects/deprecations/_delete_unknown_types') + .expect(200); + + expect(result.body).toEqual({ success: true }); + }); + + it('calls upon esClient.deleteByQuery', async () => { + await supertest(httpSetup.server.listener) + .post('/internal/saved_objects/deprecations/_delete_unknown_types') + .expect(200); + + expect(elasticsearchClient.asInternalUser.deleteByQuery).toHaveBeenCalledTimes(1); + expect(elasticsearchClient.asInternalUser.deleteByQuery).toHaveBeenCalledWith({ + index: ['known-type-index_8.0.0'], + wait_for_completion: false, + body: { + query: { + bool: { + must_not: expect.any(Array), + }, + }, + }, + }); + }); +}); diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 135996f49cea42..6477d1a3dfbeb1 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -20,17 +20,26 @@ import { Env } from '../config'; import { configServiceMock } from '../mocks'; import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; import { coreUsageDataServiceMock } from '../core_usage_data/core_usage_data_service.mock'; +import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { httpServerMock } from '../http/http_server.mocks'; import { SavedObjectsClientFactoryProvider } from './service/lib'; import { NodesVersionCompatibility } from '../elasticsearch/version_check/ensure_es_version'; import { SavedObjectsRepository } from './service/lib/repository'; import { registerCoreObjectTypes } from './object_types'; +import { getSavedObjectsDeprecationsProvider } from './deprecations'; jest.mock('./service/lib/repository'); jest.mock('./object_types'); +jest.mock('./deprecations'); describe('SavedObjectsService', () => { + let deprecationsSetup: ReturnType; + + beforeEach(() => { + deprecationsSetup = deprecationsServiceMock.createInternalSetupContract(); + }); + const createCoreContext = ({ skipMigration = true, env, @@ -53,6 +62,7 @@ describe('SavedObjectsService', () => { return { http: httpServiceMock.createInternalSetupContract(), elasticsearch: elasticsearchMock, + deprecations: deprecationsSetup, coreUsageData: coreUsageDataServiceMock.createSetupContract(), }; }; @@ -79,6 +89,24 @@ describe('SavedObjectsService', () => { expect(mockedRegisterCoreObjectTypes).toHaveBeenCalledTimes(1); }); + it('register the deprecation provider', async () => { + const coreContext = createCoreContext(); + const soService = new SavedObjectsService(coreContext); + + const mockRegistry = deprecationsServiceMock.createSetupContract(); + deprecationsSetup.getRegistry.mockReturnValue(mockRegistry); + + const deprecations = Symbol('deprecations'); + const mockedGetSavedObjectsDeprecationsProvider = getSavedObjectsDeprecationsProvider as jest.Mock; + mockedGetSavedObjectsDeprecationsProvider.mockReturnValue(deprecations); + await soService.setup(createSetupDeps()); + + expect(deprecationsSetup.getRegistry).toHaveBeenCalledTimes(1); + expect(deprecationsSetup.getRegistry).toHaveBeenCalledWith('savedObjects'); + expect(mockRegistry.registerDeprecations).toHaveBeenCalledTimes(1); + expect(mockRegistry.registerDeprecations).toHaveBeenCalledWith(deprecations); + }); + describe('#setClientFactoryProvider', () => { it('registers the factory to the clientProvider', async () => { const coreContext = createCoreContext(); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 6933752cd210cf..fd0aa238db2a10 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -22,6 +22,7 @@ import { InternalElasticsearchServiceSetup, InternalElasticsearchServiceStart, } from '../elasticsearch'; +import { InternalDeprecationsServiceSetup } from '../deprecations'; import { KibanaConfigType } from '../kibana_config'; import { SavedObjectsConfigType, @@ -44,6 +45,7 @@ import { registerRoutes } from './routes'; import { ServiceStatus } from '../status'; import { calculateStatus$ } from './status'; import { registerCoreObjectTypes } from './object_types'; +import { getSavedObjectsDeprecationsProvider } from './deprecations'; /** * Saved Objects is Kibana's data persistence mechanism allowing plugins to @@ -251,6 +253,7 @@ export interface SavedObjectsSetupDeps { http: InternalHttpServiceSetup; elasticsearch: InternalElasticsearchServiceSetup; coreUsageData: CoreUsageDataSetup; + deprecations: InternalDeprecationsServiceSetup; } interface WrappedClientFactoryWrapper { @@ -286,7 +289,7 @@ export class SavedObjectsService this.logger.debug('Setting up SavedObjects service'); this.setupDeps = setupDeps; - const { http, elasticsearch, coreUsageData } = setupDeps; + const { http, elasticsearch, coreUsageData, deprecations } = setupDeps; const savedObjectsConfig = await this.coreContext.configService .atPath('savedObjects') @@ -298,6 +301,20 @@ export class SavedObjectsService .toPromise(); this.config = new SavedObjectConfig(savedObjectsConfig, savedObjectsMigrationConfig); + const kibanaConfig = await this.coreContext.configService + .atPath('kibana') + .pipe(first()) + .toPromise(); + + deprecations.getRegistry('savedObjects').registerDeprecations( + getSavedObjectsDeprecationsProvider({ + kibanaConfig, + savedObjectsConfig: this.config, + kibanaVersion: this.coreContext.env.packageInfo.version, + typeRegistry: this.typeRegistry, + }) + ); + coreUsageData.registerType(this.typeRegistry); registerRoutes({ @@ -306,6 +323,7 @@ export class SavedObjectsService logger: this.logger, config: this.config, migratorPromise: this.migrator$.pipe(first()).toPromise(), + kibanaConfig, kibanaVersion: this.coreContext.env.packageInfo.version, }); diff --git a/src/core/server/saved_objects/service/lib/get_index_for_type.test.ts b/src/core/server/saved_objects/service/lib/get_index_for_type.test.ts new file mode 100644 index 00000000000000..fa065b02b8050a --- /dev/null +++ b/src/core/server/saved_objects/service/lib/get_index_for_type.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getIndexForType } from './get_index_for_type'; +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; + +describe('getIndexForType', () => { + const kibanaVersion = '8.0.0'; + const defaultIndex = '.kibana'; + let typeRegistry: ReturnType; + + beforeEach(() => { + typeRegistry = typeRegistryMock.create(); + }); + + describe('when migV2 is enabled', () => { + const migV2Enabled = true; + + it('returns the correct index for a type specifying a custom index', () => { + typeRegistry.getIndex.mockImplementation((type) => `.${type}-index`); + expect( + getIndexForType({ + type: 'foo', + typeRegistry, + defaultIndex, + kibanaVersion, + migV2Enabled, + }) + ).toEqual('.foo-index_8.0.0'); + }); + + it('returns the correct index for a type not specifying a custom index', () => { + typeRegistry.getIndex.mockImplementation((type) => undefined); + expect( + getIndexForType({ + type: 'foo', + typeRegistry, + defaultIndex, + kibanaVersion, + migV2Enabled, + }) + ).toEqual('.kibana_8.0.0'); + }); + }); + + describe('when migV2 is disabled', () => { + const migV2Enabled = false; + + it('returns the correct index for a type specifying a custom index', () => { + typeRegistry.getIndex.mockImplementation((type) => `.${type}-index`); + expect( + getIndexForType({ + type: 'foo', + typeRegistry, + defaultIndex, + kibanaVersion, + migV2Enabled, + }) + ).toEqual('.foo-index'); + }); + + it('returns the correct index for a type not specifying a custom index', () => { + typeRegistry.getIndex.mockImplementation((type) => undefined); + expect( + getIndexForType({ + type: 'foo', + typeRegistry, + defaultIndex, + kibanaVersion, + migV2Enabled, + }) + ).toEqual('.kibana'); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/get_index_for_type.ts b/src/core/server/saved_objects/service/lib/get_index_for_type.ts new file mode 100644 index 00000000000000..cef477e6dd8402 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/get_index_for_type.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; + +interface GetIndexForTypeOptions { + type: string; + typeRegistry: ISavedObjectTypeRegistry; + migV2Enabled: boolean; + kibanaVersion: string; + defaultIndex: string; +} + +export const getIndexForType = ({ + type, + typeRegistry, + migV2Enabled, + defaultIndex, + kibanaVersion, +}: GetIndexForTypeOptions): string => { + // TODO migrationsV2: Remove once we remove migrations v1 + // This is a hacky, but it required the least amount of changes to + // existing code to support a migrations v2 index. Long term we would + // want to always use the type registry to resolve a type's index + // (including the default index). + if (migV2Enabled) { + return `${typeRegistry.getIndex(type) || defaultIndex}_${kibanaVersion}`; + } else { + return typeRegistry.getIndex(type) || defaultIndex; + } +}; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index 661d04b8a0b2a0..ec283f3d3741e9 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -41,3 +41,5 @@ export type { SavedObjectsUpdateObjectsSpacesResponse, SavedObjectsUpdateObjectsSpacesResponseObject, } from './update_objects_spaces'; + +export { getIndexForType } from './get_index_for_type'; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index e49b2e413981f3..c425f8c40fed11 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -92,6 +92,7 @@ import { SavedObjectsUpdateObjectsSpacesObject, SavedObjectsUpdateObjectsSpacesOptions, } from './update_objects_spaces'; +import { getIndexForType } from './get_index_for_type'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -2099,16 +2100,13 @@ export class SavedObjectsRepository { * @param type - the type */ private getIndexForType(type: string) { - // TODO migrationsV2: Remove once we remove migrations v1 - // This is a hacky, but it required the least amount of changes to - // existing code to support a migrations v2 index. Long term we would - // want to always use the type registry to resolve a type's index - // (including the default index). - if (this._migrator.soMigrationsConfig.enableV2) { - return `${this._registry.getIndex(type) || this._index}_${this._migrator.kibanaVersion}`; - } else { - return this._registry.getIndex(type) || this._index; - } + return getIndexForType({ + type, + defaultIndex: this._index, + typeRegistry: this._registry, + kibanaVersion: this._migrator.kibanaVersion, + migV2Enabled: this._migrator.soMigrationsConfig.enableV2, + }); } /** diff --git a/src/core/server/server.ts b/src/core/server/server.ts index f3bcc8d8c702bf..59cde6835c5198 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -233,6 +233,7 @@ export class Server { const savedObjectsSetup = await this.savedObjects.setup({ http: httpSetup, elasticsearch: elasticsearchServiceSetup, + deprecations: deprecationsSetup, coreUsageData: coreUsageDataSetup, }); @@ -301,6 +302,7 @@ export class Server { const executionContextStart = this.executionContext.start(); const elasticsearchStart = await this.elasticsearch.start(); + const deprecationsStart = this.deprecations.start(); const soStartSpan = startTransaction?.startSpan('saved_objects.migration', 'migration'); const savedObjectsStart = await this.savedObjects.start({ elasticsearch: elasticsearchStart, @@ -318,7 +320,7 @@ export class Server { savedObjects: savedObjectsStart, exposedConfigsToUsage: this.plugins.getExposedPluginConfigsToUsage(), }); - const deprecationsStart = this.deprecations.start(); + this.status.start(); this.coreStart = { diff --git a/test/api_integration/apis/saved_objects/delete_unknown_types.ts b/test/api_integration/apis/saved_objects/delete_unknown_types.ts new file mode 100644 index 00000000000000..42caa753683e19 --- /dev/null +++ b/test/api_integration/apis/saved_objects/delete_unknown_types.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('/deprecations/_delete_unknown_types', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + await esArchiver.load( + 'test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types' + ); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types' + ); + }); + + const fetchIndexContent = async () => { + const { body } = await es.search<{ type: string }>({ + index: '.kibana', + body: { + size: 100, + }, + }); + return body.hits.hits + .map((hit) => ({ + type: hit._source!.type, + id: hit._id, + })) + .sort((a, b) => { + return a.id > b.id ? 1 : -1; + }); + }; + + it('should return 200 with individual responses', async () => { + const beforeDelete = await fetchIndexContent(); + expect(beforeDelete).to.eql([ + { + id: 'dashboard:b70c7ae0-3224-11e8-a572-ffca06da1357', + type: 'dashboard', + }, + { + id: 'index-pattern:8963ca30-3224-11e8-a572-ffca06da1357', + type: 'index-pattern', + }, + { + id: 'search:960372e0-3224-11e8-a572-ffca06da1357', + type: 'search', + }, + { + id: 'space:default', + type: 'space', + }, + { + id: 'unknown-shareable-doc', + type: 'unknown-shareable-type', + }, + { + id: 'unknown-type:unknown-doc', + type: 'unknown-type', + }, + { + id: 'visualization:a42c0580-3224-11e8-a572-ffca06da1357', + type: 'visualization', + }, + ]); + + await supertest + .post(`/internal/saved_objects/deprecations/_delete_unknown_types`) + .send({}) + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ success: true }); + }); + + for (let i = 0; i < 10; i++) { + const afterDelete = await fetchIndexContent(); + // we're deleting with `wait_for_completion: false` and we don't surface + // the task ID in the API, so we're forced to use pooling for the FTR tests + if (afterDelete.map((obj) => obj.type).includes('unknown-type') && i < 10) { + await delay(1000); + continue; + } + expect(afterDelete).to.eql([ + { + id: 'dashboard:b70c7ae0-3224-11e8-a572-ffca06da1357', + type: 'dashboard', + }, + { + id: 'index-pattern:8963ca30-3224-11e8-a572-ffca06da1357', + type: 'index-pattern', + }, + { + id: 'search:960372e0-3224-11e8-a572-ffca06da1357', + type: 'search', + }, + { + id: 'space:default', + type: 'space', + }, + { + id: 'visualization:a42c0580-3224-11e8-a572-ffca06da1357', + type: 'visualization', + }, + ]); + break; + } + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/index.ts b/test/api_integration/apis/saved_objects/index.ts index 2af1df01c0f924..12189bce302b8c 100644 --- a/test/api_integration/apis/saved_objects/index.ts +++ b/test/api_integration/apis/saved_objects/index.ts @@ -23,5 +23,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { 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/es_archiver/saved_objects/delete_unknown_types/data.json b/test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/data.json new file mode 100644 index 00000000000000..3d6ecd160db004 --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/data.json @@ -0,0 +1,182 @@ +{ + "type": "doc", + "value": { + "id": "index-pattern:8963ca30-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "index-pattern": { + "fields": "[{\"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\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "title": "saved_objects*" + }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], + "type": "index-pattern", + "updated_at": "2018-03-28T01:08:34.290Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "search:960372e0-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "8963ca30-3224-11e8-a572-ffca06da1357", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "_source" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"id:3\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "_score", + "desc" + ] + ], + "title": "OneRecord", + "version": 1 + }, + "type": "search", + "updated_at": "2018-03-28T01:08:55.182Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:a42c0580-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "960372e0-3224-11e8-a572-ffca06da1357", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization", + "updated_at": "2018-03-28T01:09:18.936Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + }, + "savedSearchRefName": "search_0", + "title": "VisualizationFromSavedSearch", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "version": 1, + "visState": "{\"title\":\"VisualizationFromSavedSearch\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\",\"showToolbar\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:b70c7ae0-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.0.0-alpha1\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"7.0.0-alpha1\",\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"}]", + "timeRestore": false, + "title": "Dashboard", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "add810b0-3224-11e8-a572-ffca06da1357", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "a42c0580-3224-11e8-a572-ffca06da1357", + "name": "panel_1", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-03-28T01:09:50.606Z" + }, + "type": "_doc" + } +} + + +{ + "type": "doc", + "value": { + "id": "unknown-type:unknown-doc", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "unknown-type": { + "foo": "bar" + }, + "migrationVersion": {}, + "references": [ + ], + "type": "unknown-type", + "updated_at": "2018-03-28T01:08:34.290Z" + }, + "type": "_doc" + } +} + + +{ + "type": "doc", + "value": { + "id": "unknown-shareable-doc", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "unknown-shareable-type": { + "foo": "bar" + }, + "migrationVersion": {}, + "references": [ + ], + "type": "unknown-shareable-type", + "updated_at": "2018-03-28T01:08:34.290Z" + }, + "type": "_doc" + } +} diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/mappings.json b/test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/mappings.json new file mode 100644 index 00000000000000..f745e0f69c5d31 --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/mappings.json @@ -0,0 +1,530 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana_$KIBANA_PACKAGE_VERSION": {}, + ".kibana": {} + }, + "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "index-pattern": "45915a1ad866812242df474eb0479052", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355" + } + }, + "dynamic": "strict", + "properties": { + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "unknown-type": { + "dynamic": "false", + "properties": { + "foo": { + "type": "keyword" + } + } + }, + "unknown-shareable-type": { + "dynamic": "false", + "properties": { + "foo": { + "type": "keyword" + } + } + }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "optionsJSON": { + "index": false, + "type": "text" + }, + "panelsJSON": { + "index": false, + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "pause": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "section": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "value": { + "doc_values": false, + "index": false, + "type": "integer" + } + } + }, + "timeFrom": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "timeRestore": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "timeTo": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "dynamic": "false", + "properties": { + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "properties": { + "disabled": { + "type": "boolean" + }, + "sourceId": { + "type": "keyword" + }, + "targetType": { + "type": "keyword" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "server": { + "dynamic": "false", + "type": "object" + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "savedSearchRefName": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "index": false, + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "index": false, + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s", + "routing_partition_size": "1" + } + } + } +}