From 5e0be082baf2a6fc59d63840ba01d396191ccace Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 6 Sep 2021 12:07:17 +0200 Subject: [PATCH 01/13] Add deprecation warning when unknown types are present --- .../deprecations/deprecation_factory.ts | 35 +++++++ .../saved_objects/deprecations/index.ts | 10 ++ .../deprecations/unknown_object_types.ts | 95 +++++++++++++++++++ .../deprecations/delete_unknown_types.ts | 27 ++++++ .../routes/deprecations/index.ts | 9 ++ src/core/server/saved_objects/routes/index.ts | 2 + .../saved_objects/saved_objects_service.ts | 19 +++- .../service/lib/get_index_for_type.ts | 36 +++++++ .../server/saved_objects/service/lib/index.ts | 2 + .../saved_objects/service/lib/repository.ts | 18 ++-- src/core/server/server.ts | 12 ++- 11 files changed, 249 insertions(+), 16 deletions(-) create mode 100644 src/core/server/saved_objects/deprecations/deprecation_factory.ts create mode 100644 src/core/server/saved_objects/deprecations/index.ts create mode 100644 src/core/server/saved_objects/deprecations/unknown_object_types.ts create mode 100644 src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts create mode 100644 src/core/server/saved_objects/routes/deprecations/index.ts create mode 100644 src/core/server/saved_objects/service/lib/get_index_for_type.ts 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..1569cdbd171be7 --- /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 { hasUnknownObjectTypes } from './unknown_object_types'; +export { getSavedObjectsDeprecationsProvider } from './deprecation_factory'; 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..4f0a3267c209ed --- /dev/null +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.ts @@ -0,0 +1,95 @@ +/* + * 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; +} + +export const hasUnknownObjectTypes = async ({ + typeRegistry, + esClient, + kibanaConfig, + savedObjectsConfig, + kibanaVersion, +}: UnknownTypesDeprecationOptions) => { + const knownTypes = typeRegistry.getAllTypes().map((type) => type.name); + const targetIndices = [ + ...new Set( + knownTypes.map((type) => + getIndexForType({ + type, + typeRegistry, + migV2Enabled: savedObjectsConfig.migration.enableV2, + kibanaVersion, + defaultIndex: kibanaConfig.index, + }) + ) + ), + ]; + + const query: estypes.QueryDslQueryContainer = { + bool: { + must_not: knownTypes.map((type) => ({ + term: { type }, + })), + }, + }; + + const { body } = await esClient.asInternalUser.search({ + index: targetIndices, + body: { + size: 10000, + query, + }, + }); + const { hits: unknownDocs } = body.hits; + + return unknownDocs.length > 0; +}; + +export const getUnknownTypesDeprecations = async ( + options: UnknownTypesDeprecationOptions +): Promise => { + const deprecations: DeprecationsDetails[] = []; + if (await hasUnknownObjectTypes(options)) { + 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: '', + }), + level: 'critical', + requireRestart: false, + deprecationType: undefined, // not config nor feature... + correctiveActions: { + manualSteps: [], // TODO + api: { + path: '/internal/saved_objects/deprecations/_delete_unknown_types', + method: 'POST', + body: {}, + }, + }, + }); + } + return deprecations; +}; 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..9cad7f7dfcafd6 --- /dev/null +++ b/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts @@ -0,0 +1,27 @@ +/* + * 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'; + +export const registerDeleteUnknownTypesRoute = (router: IRouter) => { + router.post( + { + path: '/deprecations/_delete_unknown_types', + validate: false, + }, + catchAndReturnBoomErrors(async (context, req, res) => { + // TODO: actually peform the action. + 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 889edfb66a20f6..64832cdabe2766 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -25,6 +25,7 @@ import { registerExportRoute } from './export'; import { registerImportRoute } from './import'; import { registerResolveImportErrorsRoute } from './resolve_import_errors'; import { registerMigrateRoute } from './migrate'; +import { registerDeleteUnknownTypesRoute } from './deprecations'; export function registerRoutes({ http, @@ -58,4 +59,5 @@ export function registerRoutes({ const internalRouter = http.createRouter('/internal/saved_objects/'); registerMigrateRoute(internalRouter, migratorPromise); + registerDeleteUnknownTypesRoute(internalRouter); } diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 074eae55acaeab..69e5f708f20326 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: InternalCoreUsageDataSetup; + 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('core').registerDeprecations( + getSavedObjectsDeprecationsProvider({ + kibanaConfig, + savedObjectsConfig: this.config, + kibanaVersion: this.coreContext.env.packageInfo.version, + typeRegistry: this.typeRegistry, + }) + ); + coreUsageData.registerType(this.typeRegistry); registerRoutes({ 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 865cc71a7e26b3..f39cd7112ae162 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -211,6 +211,10 @@ export class Server { const capabilitiesSetup = this.capabilities.setup({ http: httpSetup }); + const deprecationsSetup = this.deprecations.setup({ + http: httpSetup, + }); + const elasticsearchServiceSetup = await this.elasticsearch.setup({ http: httpSetup, executionContext: executionContextSetup, @@ -228,6 +232,7 @@ export class Server { const savedObjectsSetup = await this.savedObjects.setup({ http: httpSetup, elasticsearch: elasticsearchServiceSetup, + deprecations: deprecationsSetup, coreUsageData: coreUsageDataSetup, }); @@ -259,10 +264,6 @@ export class Server { const loggingSetup = this.logging.setup(); - const deprecationsSetup = this.deprecations.setup({ - http: httpSetup, - }); - const coreSetup: InternalCoreSetup = { capabilities: capabilitiesSetup, context: contextServiceSetup, @@ -302,6 +303,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, @@ -319,7 +321,7 @@ export class Server { savedObjects: savedObjectsStart, exposedConfigsToUsage: this.plugins.getExposedPluginConfigsToUsage(), }); - const deprecationsStart = this.deprecations.start(); + this.status.start(); this.coreStart = { From 9e8552dc207e0830930798f706221fd885f95574 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 6 Sep 2021 13:39:09 +0200 Subject: [PATCH 02/13] fix and add service tests --- .../saved_objects_service.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) 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..284ffec7640ff3 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('core'); + expect(mockRegistry.registerDeprecations).toHaveBeenCalledTimes(1); + expect(mockRegistry.registerDeprecations).toHaveBeenCalledWith(deprecations); + }); + describe('#setClientFactoryProvider', () => { it('registers the factory to the clientProvider', async () => { const coreContext = createCoreContext(); From fb9616855b97b62b2034930495099cd9cbbfab6e Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 6 Sep 2021 13:41:40 +0200 Subject: [PATCH 03/13] remove export --- src/core/server/saved_objects/deprecations/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/server/saved_objects/deprecations/index.ts b/src/core/server/saved_objects/deprecations/index.ts index 1569cdbd171be7..438f96dc9c40b8 100644 --- a/src/core/server/saved_objects/deprecations/index.ts +++ b/src/core/server/saved_objects/deprecations/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export { hasUnknownObjectTypes } from './unknown_object_types'; export { getSavedObjectsDeprecationsProvider } from './deprecation_factory'; From 4c33e2e6b0cdf421bab3a2d35600afcb7f5baad9 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 6 Sep 2021 15:58:55 +0200 Subject: [PATCH 04/13] plug deprecation route --- .../saved_objects/deprecations/index.ts | 1 + .../deprecations/unknown_object_types.ts | 78 +++++++++++++++++-- .../deprecations/delete_unknown_types.ts | 22 +++++- src/core/server/saved_objects/routes/index.ts | 7 +- .../saved_objects/saved_objects_service.ts | 2 + 5 files changed, 99 insertions(+), 11 deletions(-) diff --git a/src/core/server/saved_objects/deprecations/index.ts b/src/core/server/saved_objects/deprecations/index.ts index 438f96dc9c40b8..5cf1590ad43d29 100644 --- a/src/core/server/saved_objects/deprecations/index.ts +++ b/src/core/server/saved_objects/deprecations/index.ts @@ -7,3 +7,4 @@ */ export { getSavedObjectsDeprecationsProvider } from './deprecation_factory'; +export { deleteUnknownTypeObjects } from './unknown_object_types'; diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.ts index 4f0a3267c209ed..56a52547e7d82c 100644 --- a/src/core/server/saved_objects/deprecations/unknown_object_types.ts +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.ts @@ -24,17 +24,25 @@ interface UnknownTypesDeprecationOptions { kibanaVersion: string; } -export const hasUnknownObjectTypes = async ({ +const getKnownTypes = (typeRegistry: ISavedObjectTypeRegistry) => + typeRegistry.getAllTypes().map((type) => type.name); + +const getTargetIndices = ({ + types, typeRegistry, - esClient, + kibanaVersion, kibanaConfig, savedObjectsConfig, - kibanaVersion, -}: UnknownTypesDeprecationOptions) => { - const knownTypes = typeRegistry.getAllTypes().map((type) => type.name); - const targetIndices = [ +}: { + types: string[]; + typeRegistry: ISavedObjectTypeRegistry; + savedObjectsConfig: SavedObjectConfig; + kibanaConfig: KibanaConfigType; + kibanaVersion: string; +}) => { + return [ ...new Set( - knownTypes.map((type) => + types.map((type) => getIndexForType({ type, typeRegistry, @@ -45,14 +53,34 @@ export const hasUnknownObjectTypes = async ({ ) ), ]; +}; - const query: estypes.QueryDslQueryContainer = { +const getUnknownTypesQuery = (knownTypes: string[]): estypes.QueryDslQueryContainer => { + return { bool: { must_not: knownTypes.map((type) => ({ term: { type }, })), }, }; +}; + +export const hasUnknownObjectTypes = 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, @@ -93,3 +121,37 @@ export const getUnknownTypesDeprecations = async ( } 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 index 9cad7f7dfcafd6..a9e1a41f01d916 100644 --- a/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts +++ b/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts @@ -8,15 +8,33 @@ import { IRouter } from '../../../http'; import { catchAndReturnBoomErrors } from '../utils'; +import { deleteUnknownTypeObjects } from '../../deprecations'; +import { SavedObjectConfig } from '../../saved_objects_config'; +import { KibanaConfigType } from '../../../kibana_config'; -export const registerDeleteUnknownTypesRoute = (router: IRouter) => { +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) => { - // TODO: actually peform the action. + 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/index.ts b/src/core/server/saved_objects/routes/index.ts index 64832cdabe2766..daf0ad70bd5d22 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -26,6 +26,7 @@ import { registerImportRoute } from './import'; import { registerResolveImportErrorsRoute } from './resolve_import_errors'; import { registerMigrateRoute } from './migrate'; import { registerDeleteUnknownTypesRoute } from './deprecations'; +import { KibanaConfigType } from '../../kibana_config'; export function registerRoutes({ http, @@ -33,12 +34,16 @@ export function registerRoutes({ logger, config, migratorPromise, + kibanaVersion, + kibanaConfig, }: { http: InternalHttpServiceSetup; coreUsageData: InternalCoreUsageDataSetup; logger: Logger; config: SavedObjectConfig; migratorPromise: Promise; + kibanaVersion: string; + kibanaConfig: KibanaConfigType; }) { const router = http.createRouter('/api/saved_objects/'); @@ -59,5 +64,5 @@ export function registerRoutes({ const internalRouter = http.createRouter('/internal/saved_objects/'); registerMigrateRoute(internalRouter, migratorPromise); - registerDeleteUnknownTypesRoute(internalRouter); + registerDeleteUnknownTypesRoute(internalRouter, { config, kibanaConfig, kibanaVersion }); } diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 69e5f708f20326..74dccfca1f486d 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -323,6 +323,8 @@ export class SavedObjectsService logger: this.logger, config: this.config, migratorPromise: this.migrator$.pipe(first()).toPromise(), + kibanaConfig, + kibanaVersion: this.coreContext.env.packageInfo.version, }); registerCoreObjectTypes(this.typeRegistry); From a912050bc873cc6e350d5f496509b9b09aca8607 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 6 Sep 2021 16:42:15 +0200 Subject: [PATCH 05/13] add integration test for new route --- .../delete_unknown_types.test.ts | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts 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), + }, + }, + }, + }); + }); +}); From fc11738f6039504cc22cfcce37bbb9a2dd17ebcb Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 6 Sep 2021 16:54:11 +0200 Subject: [PATCH 06/13] add unit test for getIndexForType --- .../service/lib/get_index_for_type.test.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/core/server/saved_objects/service/lib/get_index_for_type.test.ts 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'); + }); + }); +}); From 18678d4e42a2e89ea6c984688c8a05425fa93911 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 7 Sep 2021 08:03:46 +0200 Subject: [PATCH 07/13] add unit tests --- .../unknown_object_types.test.mocks.ts | 13 ++ .../deprecations/unknown_object_types.test.ts | 165 ++++++++++++++++++ .../deprecations/unknown_object_types.ts | 4 +- 3 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 src/core/server/saved_objects/deprecations/unknown_object_types.test.mocks.ts create mode 100644 src/core/server/saved_objects/deprecations/unknown_object_types.test.ts 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 index 56a52547e7d82c..d60a8f6e521d16 100644 --- a/src/core/server/saved_objects/deprecations/unknown_object_types.ts +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.ts @@ -65,7 +65,7 @@ const getUnknownTypesQuery = (knownTypes: string[]): estypes.QueryDslQueryContai }; }; -export const hasUnknownObjectTypes = async ({ +const hasUnknownObjectTypes = async ({ typeRegistry, esClient, kibanaConfig, @@ -104,7 +104,7 @@ export const getUnknownTypesDeprecations = async ( defaultMessage: 'Saved objects with unknown types are present in Kibana system indices', }), message: i18n.translate('core.savedObjects.deprecations.unknownTypes.message', { - defaultMessage: '', + defaultMessage: 'Unknown saved object types can be caused by disabled plugins, or ', }), level: 'critical', requireRestart: false, From 3f49db452ab0cdc40c0d01bb24da5f1eea29ea88 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 7 Sep 2021 08:24:45 +0200 Subject: [PATCH 08/13] improve deprecation messages --- .../deprecations/unknown_object_types.ts | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.ts index d60a8f6e521d16..5bccbace729959 100644 --- a/src/core/server/saved_objects/deprecations/unknown_object_types.ts +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.ts @@ -65,7 +65,7 @@ const getUnknownTypesQuery = (knownTypes: string[]): estypes.QueryDslQueryContai }; }; -const hasUnknownObjectTypes = async ({ +const getUnknownSavedObjects = async ({ typeRegistry, esClient, kibanaConfig, @@ -91,26 +91,37 @@ const hasUnknownObjectTypes = async ({ }); const { hits: unknownDocs } = body.hits; - return unknownDocs.length > 0; + return unknownDocs.map((doc) => ({ id: doc._id, type: doc._source?.type ?? 'unknown' })); }; export const getUnknownTypesDeprecations = async ( options: UnknownTypesDeprecationOptions ): Promise => { const deprecations: DeprecationsDetails[] = []; - if (await hasUnknownObjectTypes(options)) { + 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: 'Unknown saved object types can be caused by disabled plugins, or ', + defaultMessage: + 'Upgrades will fail for 8.0+ because documents were found for unknown saved object types.' + + `To ensure that upgrades will succeed in the future, either re-enable plugins or delete these documents from the Kibana indices`, }), level: 'critical', requireRestart: false, deprecationType: undefined, // not config nor feature... correctiveActions: { - manualSteps: [], // TODO + manualSteps: [ + i18n.translate('core.savedObjects.deprecations.unknownTypes.manualSteps.1', { + defaultMessage: 'If plugins are disabled, re-enable the, 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', From 694ab5704feb6fb5c06f6dec2e8453683545a0b7 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 7 Sep 2021 10:12:14 +0200 Subject: [PATCH 09/13] add FTR test --- .../saved_objects/delete_unknown_types.ts | 125 +++++ .../apis/saved_objects/index.ts | 1 + .../delete_unknown_types/data.json | 182 ++++++ .../delete_unknown_types/mappings.json | 530 ++++++++++++++++++ 4 files changed, 838 insertions(+) create mode 100644 test/api_integration/apis/saved_objects/delete_unknown_types.ts create mode 100644 test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/data.json create mode 100644 test/api_integration/fixtures/es_archiver/saved_objects/delete_unknown_types/mappings.json 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" + } + } + } +} From 6352eeb9ba6033657849e55b129bf1314ece89cb Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 13 Sep 2021 14:52:59 +0200 Subject: [PATCH 10/13] fix things due to merge --- src/core/server/server.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 8afabe839f52f0..cd133def69a67c 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -215,10 +215,6 @@ export class Server { const capabilitiesSetup = this.capabilities.setup({ http: httpSetup }); - const deprecationsSetup = this.deprecations.setup({ - http: httpSetup, - }); - const elasticsearchServiceSetup = await this.elasticsearch.setup({ http: httpSetup, executionContext: executionContextSetup, From 0a745d2c8749bccc8eb83b3a8dc2e0e51098502d Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 13 Sep 2021 14:55:55 +0200 Subject: [PATCH 11/13] change the name of the deprecation provider --- src/core/server/saved_objects/saved_objects_service.test.ts | 2 +- src/core/server/saved_objects/saved_objects_service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 284ffec7640ff3..6477d1a3dfbeb1 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -102,7 +102,7 @@ describe('SavedObjectsService', () => { await soService.setup(createSetupDeps()); expect(deprecationsSetup.getRegistry).toHaveBeenCalledTimes(1); - expect(deprecationsSetup.getRegistry).toHaveBeenCalledWith('core'); + expect(deprecationsSetup.getRegistry).toHaveBeenCalledWith('savedObjects'); expect(mockRegistry.registerDeprecations).toHaveBeenCalledTimes(1); expect(mockRegistry.registerDeprecations).toHaveBeenCalledWith(deprecations); }); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 74dccfca1f486d..ee56744249c5b1 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -306,7 +306,7 @@ export class SavedObjectsService .pipe(first()) .toPromise(); - deprecations.getRegistry('core').registerDeprecations( + deprecations.getRegistry('savedObjects').registerDeprecations( getSavedObjectsDeprecationsProvider({ kibanaConfig, savedObjectsConfig: this.config, From 026a80d183c7ed904e9ad36a398e519fbcbbefde Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 13 Sep 2021 17:08:23 +0200 Subject: [PATCH 12/13] improve message --- .../server/saved_objects/deprecations/unknown_object_types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.ts index 5bccbace729959..dba6ad16d92a3c 100644 --- a/src/core/server/saved_objects/deprecations/unknown_object_types.ts +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.ts @@ -115,7 +115,7 @@ export const getUnknownTypesDeprecations = async ( correctiveActions: { manualSteps: [ i18n.translate('core.savedObjects.deprecations.unknownTypes.manualSteps.1', { - defaultMessage: 'If plugins are disabled, re-enable the, then restart Kibana.', + defaultMessage: 'Enable disabled plugins then restart Kibana.', }), i18n.translate('core.savedObjects.deprecations.unknownTypes.manualSteps.2', { defaultMessage: From d3e47f6cfa5ee997e1dfc95c3ac9d16b8bc7ae9e Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 14 Sep 2021 08:06:00 +0200 Subject: [PATCH 13/13] improve message again --- .../saved_objects/deprecations/unknown_object_types.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.ts index dba6ad16d92a3c..c966e621ca6055 100644 --- a/src/core/server/saved_objects/deprecations/unknown_object_types.ts +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.ts @@ -106,8 +106,12 @@ export const getUnknownTypesDeprecations = async ( }), message: i18n.translate('core.savedObjects.deprecations.unknownTypes.message', { defaultMessage: - 'Upgrades will fail for 8.0+ because documents were found for unknown saved object types.' + + '{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,