diff --git a/x-pack/plugins/apm/common/apm_saved_object_constants.ts b/x-pack/plugins/apm/common/apm_saved_object_constants.ts index 7d9e571242afe..17c5a802a440a 100644 --- a/x-pack/plugins/apm/common/apm_saved_object_constants.ts +++ b/x-pack/plugins/apm/common/apm_saved_object_constants.ts @@ -8,9 +8,9 @@ // the types have to match the names of the saved object mappings // in /x-pack/plugins/apm/mappings.json -// APM indices -export const APM_INDICES_SAVED_OBJECT_TYPE = 'apm-indices'; -export const APM_INDICES_SAVED_OBJECT_ID = 'apm-indices'; +// APM index settings +export const APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE = 'apm-indices'; +export const APM_INDEX_SETTINGS_SAVED_OBJECT_ID = 'apm-indices'; // APM telemetry export const APM_TELEMETRY_SAVED_OBJECT_TYPE = 'apm-telemetry'; diff --git a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.test.tsx b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.test.tsx deleted file mode 100644 index 1b19bb5860b2c..0000000000000 --- a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React from 'react'; -import { ApmIndices } from '.'; -import * as hooks from '../../../../hooks/use_fetcher'; -import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; - -describe('ApmIndices', () => { - it('should not get stuck in infinite loop', () => { - const spy = jest.spyOn(hooks, 'useFetcher').mockReturnValue({ - data: undefined, - status: hooks.FETCH_STATUS.LOADING, - refetch: jest.fn(), - }); - const { getByText } = render( - - - - ); - - expect(getByText('Indices')).toMatchInlineSnapshot(` -

- Indices -

- `); - - expect(spy).toHaveBeenCalledTimes(2); - }); -}); diff --git a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx index 1a1654e22eb19..383be90168a10 100644 --- a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx @@ -8,6 +8,7 @@ import { EuiButton, EuiButtonEmpty, + EuiCallOut, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -19,9 +20,12 @@ import { EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; import React, { useEffect, useState } from 'react'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useFetcher } from '../../../../hooks/use_fetcher'; +import { ApmPluginStartDeps } from '../../../../plugin'; import { clearCache } from '../../../../services/rest/call_api'; import { APIReturnType, @@ -93,6 +97,8 @@ const INITIAL_STATE: ApiResponse = { apmIndexSettings: [] }; export function ApmIndices() { const { core } = useApmPluginContext(); + const { services } = useKibana(); + const { notifications, application } = core; const canSave = application.capabilities.apm.save; @@ -108,6 +114,10 @@ export function ApmIndices() { [canSave] ); + const { data: space } = useFetcher(() => { + return services.spaces?.getActiveSpace(); + }, [services.spaces]); + useEffect(() => { setApmIndices( data.apmIndexSettings.reduce( @@ -191,6 +201,31 @@ export function ApmIndices() { + {space?.name && ( + <> + + + + {space?.name}, + }} + /> + + } + /> + + + + + )} + diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 952df64da840a..75c3c290512d8 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -53,6 +53,7 @@ import { getLazyAPMPolicyCreateExtension } from './components/fleet_integration/ import { getLazyAPMPolicyEditExtension } from './components/fleet_integration/lazy_apm_policy_edit_extension'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import type { SecurityPluginStart } from '../../security/public'; +import { SpacesPluginStart } from '../../spaces/public'; export type ApmPluginSetup = ReturnType; @@ -82,6 +83,7 @@ export interface ApmPluginStartDeps { observability: ObservabilityPublicStart; fleet?: FleetStart; security?: SecurityPluginStart; + spaces?: SpacesPluginStart; } const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', { diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index e5ad2cb6c6c1f..34a09fe57a05b 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -48,6 +48,7 @@ import { TRANSACTION_TYPE, } from '../common/elasticsearch_fieldnames'; import { tutorialProvider } from './tutorial'; +import { migrateLegacyAPMIndicesToSpaceAware } from './saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware'; export class APMPlugin implements @@ -247,6 +248,11 @@ export class APMPlugin config: this.currentConfig, logger: this.logger, }); + + migrateLegacyAPMIndicesToSpaceAware({ + coreStart: core, + logger: this.logger, + }); } public stop() {} diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices/get_apm_indices.ts index 450ce3fa18dad..7d98502ee5d93 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices/get_apm_indices.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices/get_apm_indices.ts @@ -7,13 +7,14 @@ import { SavedObjectsClient } from 'src/core/server'; import { - APM_INDICES_SAVED_OBJECT_TYPE, - APM_INDICES_SAVED_OBJECT_ID, + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + APM_INDEX_SETTINGS_SAVED_OBJECT_ID, } from '../../../../common/apm_saved_object_constants'; import { APMConfig } from '../../..'; import { APMRouteHandlerResources } from '../../typings'; import { withApmSpan } from '../../../utils/with_apm_span'; import { ApmIndicesConfig } from '../../../../../observability/common/typings'; +import { APMIndices } from '../../../saved_objects/apm_indices'; export type { ApmIndicesConfig }; @@ -22,13 +23,15 @@ type ISavedObjectsClient = Pick; async function getApmIndicesSavedObject( savedObjectsClient: ISavedObjectsClient ) { - const apmIndices = await withApmSpan('get_apm_indices_saved_object', () => - savedObjectsClient.get>( - APM_INDICES_SAVED_OBJECT_TYPE, - APM_INDICES_SAVED_OBJECT_ID - ) + const apmIndicesSavedObject = await withApmSpan( + 'get_apm_indices_saved_object', + () => + savedObjectsClient.get>( + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + APM_INDEX_SETTINGS_SAVED_OBJECT_ID + ) ); - return apmIndices.attributes; + return apmIndicesSavedObject.attributes.apmIndices; } export function getApmIndicesConfig(config: APMConfig): ApmIndicesConfig { @@ -90,6 +93,6 @@ export async function getApmIndexSettings({ return apmIndices.map((configurationName) => ({ configurationName, defaultValue: apmIndicesConfig[configurationName], // value defined in kibana[.dev].yml - savedValue: apmIndicesSavedObject[configurationName], // value saved via Saved Objects service + savedValue: apmIndicesSavedObject?.[configurationName], // value saved via Saved Objects service })); } diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts index bce6857aa4ff2..0c91bccb42999 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.test.ts @@ -26,7 +26,10 @@ describe('saveApmIndices', () => { await saveApmIndices(savedObjectsClient, apmIndices); expect(savedObjectsClient.create).toHaveBeenCalledWith( expect.any(String), - { settingA: 'aa', settingF: 'ff', settingG: 'gg' }, + { + apmIndices: { settingA: 'aa', settingF: 'ff', settingG: 'gg' }, + isSpaceAware: true, + }, expect.any(Object) ); }); diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts index 14a5830d8246c..2bd4273910bbf 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices/save_apm_indices.ts @@ -7,9 +7,10 @@ import { SavedObjectsClientContract } from '../../../../../../../src/core/server'; import { - APM_INDICES_SAVED_OBJECT_TYPE, - APM_INDICES_SAVED_OBJECT_ID, + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + APM_INDEX_SETTINGS_SAVED_OBJECT_ID, } from '../../../../common/apm_saved_object_constants'; +import { APMIndices } from '../../../saved_objects/apm_indices'; import { withApmSpan } from '../../../utils/with_apm_span'; import { ApmIndicesConfig } from './get_apm_indices'; @@ -18,13 +19,10 @@ export function saveApmIndices( apmIndices: Partial ) { return withApmSpan('save_apm_indices', () => - savedObjectsClient.create( - APM_INDICES_SAVED_OBJECT_TYPE, - removeEmpty(apmIndices), - { - id: APM_INDICES_SAVED_OBJECT_ID, - overwrite: true, - } + savedObjectsClient.create( + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + { apmIndices: removeEmpty(apmIndices), isSpaceAware: true }, + { id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, overwrite: true } ) ); } diff --git a/x-pack/plugins/apm/server/saved_objects/apm_indices.ts b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts index 4aa6c4953056a..4a3b0d32e9667 100644 --- a/x-pack/plugins/apm/server/saved_objects/apm_indices.ts +++ b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts @@ -10,20 +10,43 @@ import { i18n } from '@kbn/i18n'; import { updateApmOssIndexPaths } from './migrations/update_apm_oss_index_paths'; import { ApmIndicesConfigName } from '..'; -const properties: { [Property in ApmIndicesConfigName]: { type: 'keyword' } } = - { - sourcemap: { type: 'keyword' }, - error: { type: 'keyword' }, - onboarding: { type: 'keyword' }, - span: { type: 'keyword' }, - transaction: { type: 'keyword' }, - metric: { type: 'keyword' }, +export interface APMIndices { + apmIndices?: { + sourcemap?: string; + error?: string; + onboarding?: string; + span?: string; + transaction?: string; + metric?: string; }; + isSpaceAware?: boolean; +} + +const properties: { + apmIndices: { + properties: { + [Property in ApmIndicesConfigName]: { type: 'keyword' }; + }; + }; + isSpaceAware: { type: 'boolean' }; +} = { + apmIndices: { + properties: { + sourcemap: { type: 'keyword' }, + error: { type: 'keyword' }, + onboarding: { type: 'keyword' }, + span: { type: 'keyword' }, + transaction: { type: 'keyword' }, + metric: { type: 'keyword' }, + }, + }, + isSpaceAware: { type: 'boolean' }, +}; export const apmIndices: SavedObjectsType = { name: 'apm-indices', hidden: false, - namespaceType: 'agnostic', + namespaceType: 'single', mappings: { properties }, management: { importableAndExportable: true, diff --git a/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.test.ts b/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.test.ts new file mode 100644 index 0000000000000..404e08a6b112b --- /dev/null +++ b/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.test.ts @@ -0,0 +1,206 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { CoreStart, Logger } from 'src/core/server'; +import { + APM_INDEX_SETTINGS_SAVED_OBJECT_ID, + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, +} from '../../../common/apm_saved_object_constants'; +import { migrateLegacyAPMIndicesToSpaceAware } from './migrate_legacy_apm_indices_to_space_aware'; + +const loggerMock = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +} as unknown as Logger; + +describe('migrateLegacyAPMIndicesToSpaceAware', () => { + describe('when legacy APM indices is not found', () => { + const mockBulkCreate = jest.fn(); + const mockCreate = jest.fn(); + const mockFind = jest.fn(); + const core = { + savedObjects: { + createInternalRepository: jest.fn().mockReturnValue({ + get: () => { + throw new Error('BOOM'); + }, + find: mockFind, + bulkCreate: mockBulkCreate, + create: mockCreate, + }), + }, + } as unknown as CoreStart; + + it('does not save any new saved object', () => { + migrateLegacyAPMIndicesToSpaceAware({ + coreStart: core, + logger: loggerMock, + }); + expect(mockFind).not.toHaveBeenCalled(); + expect(mockBulkCreate).not.toHaveBeenCalled(); + expect(mockCreate).not.toHaveBeenCalled(); + }); + }); + + describe('when only default space is available', () => { + const mockBulkCreate = jest.fn(); + const mockCreate = jest.fn(); + const mockSpaceFind = jest.fn().mockReturnValue({ + page: 1, + per_page: 10000, + total: 3, + saved_objects: [ + { + type: 'space', + id: 'default', + attributes: { + name: 'Default', + }, + references: [], + migrationVersion: { + space: '6.6.0', + }, + coreMigrationVersion: '8.2.0', + updated_at: '2022-02-22T14:13:28.839Z', + version: 'WzI4OSwxXQ==', + score: 0, + }, + ], + }); + const core = { + savedObjects: { + createInternalRepository: jest.fn().mockReturnValue({ + get: jest.fn().mockReturnValue({ + id: 'apm-indices', + type: 'apm-indices', + namespaces: [], + updated_at: '2022-02-22T14:17:10.584Z', + version: 'WzE1OSwxXQ==', + attributes: { + transaction: 'default-apm-*', + span: 'default-apm-*', + error: 'default-apm-*', + metric: 'default-apm-*', + sourcemap: 'default-apm-*', + onboarding: 'default-apm-*', + }, + references: [], + migrationVersion: { + 'apm-indices': '7.16.0', + }, + coreMigrationVersion: '8.2.0', + }), + find: mockSpaceFind, + bulkCreate: mockBulkCreate, + create: mockCreate, + }), + }, + } as unknown as CoreStart; + it('creates new default saved object with space awareness and delete legacy', async () => { + await migrateLegacyAPMIndicesToSpaceAware({ + coreStart: core, + logger: loggerMock, + }); + expect(mockCreate).toBeCalledWith( + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + { + apmIndices: { + transaction: 'default-apm-*', + span: 'default-apm-*', + error: 'default-apm-*', + metric: 'default-apm-*', + sourcemap: 'default-apm-*', + onboarding: 'default-apm-*', + }, + isSpaceAware: true, + }, + { + id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, + overwrite: true, + } + ); + }); + }); + + describe('when multiple spaces are found', () => { + const mockBulkCreate = jest.fn(); + const mockCreate = jest.fn(); + + const savedObjects = [ + { id: 'default', name: 'Default' }, + { id: 'space-a', name: 'Space A' }, + { id: 'space-b', name: 'Space B' }, + ]; + const mockSpaceFind = jest.fn().mockReturnValue({ + page: 1, + per_page: 10000, + total: 3, + saved_objects: savedObjects.map(({ id, name }) => { + return { + type: 'space', + id, + attributes: { name }, + references: [], + migrationVersion: { space: '6.6.0' }, + coreMigrationVersion: '8.2.0', + updated_at: '2022-02-22T14:13:28.839Z', + version: 'WzI4OSwxXQ==', + score: 0, + }; + }), + }); + const attributes = { + transaction: 'space-apm-*', + span: 'space-apm-*', + error: 'space-apm-*', + metric: 'space-apm-*', + sourcemap: 'space-apm-*', + onboarding: 'space-apm-*', + }; + const core = { + savedObjects: { + createInternalRepository: jest.fn().mockReturnValue({ + get: jest.fn().mockReturnValue({ + id: 'apm-indices', + type: 'apm-indices', + namespaces: [], + updated_at: '2022-02-22T14:17:10.584Z', + version: 'WzE1OSwxXQ==', + attributes, + references: [], + migrationVersion: { + 'apm-indices': '7.16.0', + }, + coreMigrationVersion: '8.2.0', + }), + find: mockSpaceFind, + bulkCreate: mockBulkCreate, + create: mockCreate, + }), + }, + } as unknown as CoreStart; + it('creates multiple saved objects with space awareness and delete legacies', async () => { + await migrateLegacyAPMIndicesToSpaceAware({ + coreStart: core, + logger: loggerMock, + }); + expect(mockCreate).toBeCalled(); + expect(mockBulkCreate).toBeCalledWith( + savedObjects + .filter(({ id }) => id !== 'default') + .map(({ id }) => { + return { + type: APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, + initialNamespaces: [id], + attributes: { apmIndices: attributes, isSpaceAware: true }, + }; + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.ts b/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.ts new file mode 100644 index 0000000000000..130070b80ff14 --- /dev/null +++ b/x-pack/plugins/apm/server/saved_objects/migrations/migrate_legacy_apm_indices_to_space_aware.ts @@ -0,0 +1,90 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + CoreStart, + Logger, + ISavedObjectsRepository, +} from 'src/core/server'; +import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; +import { + APM_INDEX_SETTINGS_SAVED_OBJECT_ID, + APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, +} from '../../../common/apm_saved_object_constants'; +import { ApmIndicesConfig } from '../../routes/settings/apm_indices/get_apm_indices'; +import { APMIndices } from '../apm_indices'; + +async function fetchLegacyAPMIndices(repository: ISavedObjectsRepository) { + try { + const apmIndices = await repository.get< + Partial + >(APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, APM_INDEX_SETTINGS_SAVED_OBJECT_ID); + if (apmIndices.attributes.isSpaceAware) { + // This has already been migrated to become space-aware + return null; + } + return apmIndices; + } catch (err) { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + // This can happen if APM is not being used + return null; + } + throw err; + } +} + +export async function migrateLegacyAPMIndicesToSpaceAware({ + coreStart, + logger, +}: { + coreStart: CoreStart; + logger: Logger; +}) { + const repository = coreStart.savedObjects.createInternalRepository(['space']); + try { + // Fetch legacy APM indices + const legacyAPMIndices = await fetchLegacyAPMIndices(repository); + + if (legacyAPMIndices === null) { + return; + } + // Fetch spaces available + const spaces = await repository.find({ + type: 'space', + page: 1, + perPage: 10_000, // max number of spaces as of 8.2 + fields: ['name'], // to avoid fetching *all* fields + }); + + const savedObjectAttributes: APMIndices = { + apmIndices: legacyAPMIndices.attributes, + isSpaceAware: true, + }; + + // Calls create first to update the default space setting isSpaceAware to true + await repository.create< + Partial + >(APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, savedObjectAttributes, { + id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, + overwrite: true, + }); + + // Create new APM indices space aware for all spaces available + await repository.bulkCreate>( + spaces.saved_objects + // Skip default space since it was already updated + .filter(({ id: spaceId }) => spaceId !== 'default') + .map(({ id: spaceId }) => ({ + id: APM_INDEX_SETTINGS_SAVED_OBJECT_ID, + type: APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE, + initialNamespaces: [spaceId], + attributes: savedObjectAttributes, + })) + ); + } catch (e) { + logger.error('Failed to migrate legacy APM indices object: ' + e.message); + } +}