Add deprecation warning when unknown SO types are present (#111268)

* Add deprecation warning when unknown types are present

* fix and add service tests

* remove export

* plug deprecation route

* add integration test for new route

* add unit test for getIndexForType

* add unit tests

* improve deprecation messages

* add FTR test

* fix things due to merge

* change the name of the deprecation provider

* improve message

* improve message again
This commit is contained in:
Pierre Gayvallet 2021-09-14 15:55:30 +02:00 committed by GitHub
parent b74f154903
commit 138371e50c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1561 additions and 12 deletions

View file

@ -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,
})),
];
},
};
};

View file

@ -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';

View file

@ -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,
}));

View file

@ -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<typeof typeRegistryMock.create>;
let esClient: ReturnType<typeof elasticsearchClientMock.createScopedClusterClient>;
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' } }],
},
},
},
});
});
});
});

View file

@ -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<SavedObjectsRawDocSource>({
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<DeprecationsDetails[]> => {
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,
},
});
};

View file

@ -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,
},
});
})
);
};

View file

@ -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';

View file

@ -26,6 +26,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,
@ -34,6 +36,7 @@ export function registerRoutes({
config,
migratorPromise,
kibanaVersion,
kibanaConfig,
}: {
http: InternalHttpServiceSetup;
coreUsageData: InternalCoreUsageDataSetup;
@ -41,6 +44,7 @@ export function registerRoutes({
config: SavedObjectConfig;
migratorPromise: Promise<IKibanaMigrator>;
kibanaVersion: string;
kibanaConfig: KibanaConfigType;
}) {
const router = http.createRouter('/api/saved_objects/');
@ -68,4 +72,5 @@ export function registerRoutes({
const internalRouter = http.createRouter('/internal/saved_objects/');
registerMigrateRoute(internalRouter, migratorPromise);
registerDeleteUnknownTypesRoute(internalRouter, { config, kibanaConfig, kibanaVersion });
}

View file

@ -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<ReturnType<typeof setupServer>>;
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<typeof typeRegistryMock.create>;
let elasticsearchClient: ReturnType<typeof elasticsearchServiceMock.createScopedClusterClient>;
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),
},
},
},
});
});
});

View file

@ -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<typeof deprecationsServiceMock.createInternalSetupContract>;
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();

View file

@ -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<SavedObjectsConfigType>('savedObjects')
@ -298,6 +301,20 @@ export class SavedObjectsService
.toPromise();
this.config = new SavedObjectConfig(savedObjectsConfig, savedObjectsMigrationConfig);
const kibanaConfig = await this.coreContext.configService
.atPath<KibanaConfigType>('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,
});

View file

@ -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<typeof typeRegistryMock.create>;
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');
});
});
});

View file

@ -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;
}
};

View file

@ -41,3 +41,5 @@ export type {
SavedObjectsUpdateObjectsSpacesResponse,
SavedObjectsUpdateObjectsSpacesResponseObject,
} from './update_objects_spaces';
export { getIndexForType } from './get_index_for_type';

View file

@ -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,
});
}
/**

View file

@ -233,6 +233,7 @@ export class Server {
const savedObjectsSetup = await this.savedObjects.setup({
http: httpSetup,
elasticsearch: elasticsearchServiceSetup,
deprecations: deprecationsSetup,
coreUsageData: coreUsageDataSetup,
});
@ -303,6 +304,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,
@ -320,7 +322,7 @@ export class Server {
savedObjects: savedObjectsStart,
exposedConfigsToUsage: this.plugins.getExposedPluginConfigsToUsage(),
});
const deprecationsStart = this.deprecations.start();
this.status.start();
this.coreStart = {

View file

@ -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;
}
});
});
}

View file

@ -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'));
});
}

View file

@ -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"
}
}

View file

@ -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"
}
}
}
}