From e5da1b3394834fd787c6346758fb3f8249a0ee30 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 7 Jun 2022 11:19:53 -0400 Subject: [PATCH] Migrate defaultIndex attribute for config saved object (#133339) (cherry picked from commit d732ebec919403f9069a33eee48b9eb1b875c244) # Conflicts: # src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts --- dev_docs/tutorials/advanced_settings.mdx | 10 ++ ...reate_or_upgrade_saved_config.test.mock.ts | 16 ++- .../create_or_upgrade_saved_config.test.ts | 72 +++++++---- .../create_or_upgrade_saved_config.ts | 18 ++- .../get_upgradeable_config.test.ts | 33 ++--- .../get_upgradeable_config.ts | 33 +++-- .../create_or_upgrade_saved_config/index.ts | 1 + .../create_or_upgrade.test.ts | 15 +++ .../server/ui_settings/saved_objects/index.ts | 3 + .../saved_objects/transforms.test.ts | 115 ++++++++++++++++++ .../ui_settings/saved_objects/transforms.ts | 96 +++++++++++++++ .../ui_settings/saved_objects/ui_settings.ts | 8 ++ ...ock.ts => ui_settings_client.test.mock.ts} | 10 +- .../ui_settings/ui_settings_client.test.ts | 35 +++--- .../telemetry_management_collector.ts | 7 +- 15 files changed, 394 insertions(+), 78 deletions(-) create mode 100644 src/core/server/ui_settings/saved_objects/transforms.test.ts create mode 100644 src/core/server/ui_settings/saved_objects/transforms.ts rename src/core/server/ui_settings/{create_or_upgrade_saved_config/get_upgradeable_config.test.mock.ts => ui_settings_client.test.mock.ts} (52%) diff --git a/dev_docs/tutorials/advanced_settings.mdx b/dev_docs/tutorials/advanced_settings.mdx index 4e5e208fc68f0c..1ca925e24f54a0 100644 --- a/dev_docs/tutorials/advanced_settings.mdx +++ b/dev_docs/tutorials/advanced_settings.mdx @@ -286,3 +286,13 @@ export const migrations = { }; ``` [1] Since all `uiSettings` migrations are added to the same migration function, while not required, grouping settings by team is good practice. + +### Creating Transforms + +If you have need to make a change that isn't possible in a saved object migration function (for example, you need to find other saved +objects), you can create a transform function instead. This will be applied when a `config` saved object is first created, and/or when it is +first upgraded. Note that you might need to add an extra attribute to verify that this transform has already been applied so it doesn't get +applied again in the future. + +For example, we needed to transform the `defaultIndex` attribute, and we added an extra `isDefaultIndexMigrated` attribute for this purpose. +See `src/core/server/ui_settings/saved_objects/transforms.ts` and [#13339](https://github.com/elastic/kibana/pull/133339) for an example. diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock.ts index 11dabaca71e13b..11ff892b89bf12 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock.ts @@ -6,7 +6,17 @@ * Side Public License, v 1. */ -export const createOrUpgradeSavedConfigMock = jest.fn(); -jest.doMock('./create_or_upgrade_saved_config', () => ({ - createOrUpgradeSavedConfig: createOrUpgradeSavedConfigMock, +import type { TransformConfigFn } from '../saved_objects'; +import type { getUpgradeableConfig } from './get_upgradeable_config'; + +export const mockTransform = jest.fn() as jest.MockedFunction; +jest.mock('../saved_objects', () => ({ + transforms: [mockTransform], +})); + +export const mockGetUpgradeableConfig = jest.fn() as jest.MockedFunction< + typeof getUpgradeableConfig +>; +jest.mock('./get_upgradeable_config', () => ({ + getUpgradeableConfig: mockGetUpgradeableConfig, })); diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts index 669849dcd8d9be..fe6aa83bb02961 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts @@ -6,29 +6,28 @@ * Side Public License, v 1. */ -import Chance from 'chance'; - -import { getUpgradeableConfigMock } from './get_upgradeable_config.test.mock'; +import { + mockTransform, + mockGetUpgradeableConfig, +} from './create_or_upgrade_saved_config.test.mock'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; import { savedObjectsClientMock } from '../../saved_objects/service/saved_objects_client.mock'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; -const chance = new Chance(); describe('uiSettings/createOrUpgradeSavedConfig', function () { afterEach(() => jest.resetAllMocks()); const version = '4.0.1'; const prevVersion = '4.0.0'; - const buildNum = chance.integer({ min: 1000, max: 5000 }); + const buildNum = 1337; function setup() { const logger = loggingSystemMock.create(); - const getUpgradeableConfig = getUpgradeableConfigMock; const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.create.mockImplementation( - async (type, attributes, options = {}) => + async (type, _, options = {}) => ({ type, id: options.id, @@ -46,8 +45,8 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { ...options, }); - expect(getUpgradeableConfigMock).toHaveBeenCalledTimes(1); - expect(getUpgradeableConfig).toHaveBeenCalledWith({ savedObjectsClient, version }); + expect(mockGetUpgradeableConfig).toHaveBeenCalledTimes(1); + expect(mockGetUpgradeableConfig).toHaveBeenCalledWith({ savedObjectsClient, version }); return resp; } @@ -58,7 +57,6 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { run, version, savedObjectsClient, - getUpgradeableConfig, }; } @@ -83,25 +81,21 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { describe('something is upgradeable', () => { it('should merge upgraded attributes with current build number in new config', async () => { - const { run, getUpgradeableConfig, savedObjectsClient } = setup(); + const { run, savedObjectsClient } = setup(); const savedAttributes = { buildNum: buildNum - 100, - [chance.word()]: chance.sentence(), - [chance.word()]: chance.sentence(), - [chance.word()]: chance.sentence(), + defaultIndex: 'some-index', }; - getUpgradeableConfig.mockResolvedValue({ + mockGetUpgradeableConfig.mockResolvedValue({ id: prevVersion, attributes: savedAttributes, - type: '', - references: [], }); await run(); - expect(getUpgradeableConfig).toHaveBeenCalledTimes(1); + expect(mockGetUpgradeableConfig).toHaveBeenCalledTimes(1); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(savedObjectsClient.create).toHaveBeenCalledWith( 'config', @@ -115,14 +109,42 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { ); }); + it('should prefer transformed attributes when merging', async () => { + const { run, savedObjectsClient } = setup(); + mockGetUpgradeableConfig.mockResolvedValue({ + id: prevVersion, + attributes: { + buildNum: buildNum - 100, + defaultIndex: 'some-index', + }, + }); + mockTransform.mockResolvedValue({ + defaultIndex: 'another-index', + isDefaultIndexMigrated: true, + }); + + await run(); + + expect(mockGetUpgradeableConfig).toHaveBeenCalledTimes(1); + expect(mockTransform).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.create).toHaveBeenCalledWith( + 'config', + { + buildNum, + defaultIndex: 'another-index', + isDefaultIndexMigrated: true, + }, + { id: version } + ); + }); + it('should log a message for upgrades', async () => { - const { getUpgradeableConfig, logger, run } = setup(); + const { logger, run } = setup(); - getUpgradeableConfig.mockResolvedValue({ + mockGetUpgradeableConfig.mockResolvedValue({ id: prevVersion, attributes: { buildNum: buildNum - 100 }, - type: '', - references: [], }); await run(); @@ -144,13 +166,11 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { }); it('does not log when upgrade fails', async () => { - const { getUpgradeableConfig, logger, run, savedObjectsClient } = setup(); + const { logger, run, savedObjectsClient } = setup(); - getUpgradeableConfig.mockResolvedValue({ + mockGetUpgradeableConfig.mockResolvedValue({ id: prevVersion, attributes: { buildNum: buildNum - 100 }, - type: '', - references: [], }); savedObjectsClient.create.mockRejectedValue(new Error('foo')); diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts index d015f506df6e3e..71293c139f177c 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts @@ -8,11 +8,13 @@ import { defaults } from 'lodash'; +import { asyncForEach } from '@kbn/std'; import { SavedObjectsClientContract } from '../../saved_objects/types'; import { SavedObjectsErrorHelpers } from '../../saved_objects/'; import { Logger, LogMeta } from '../../logging'; import { getUpgradeableConfig } from './get_upgradeable_config'; +import { transforms } from '../saved_objects'; interface ConfigLogMeta extends LogMeta { kibana: { @@ -39,10 +41,22 @@ export async function createOrUpgradeSavedConfig( version, }); + let transformDefaults = {}; + await asyncForEach(transforms, async (transformFn) => { + const result = await transformFn({ + savedObjectsClient, + configAttributes: upgradeableConfig?.attributes, + }); + transformDefaults = { ...transformDefaults, ...result }; + }); + // default to the attributes of the upgradeableConfig if available const attributes = defaults( - { buildNum }, - upgradeableConfig ? (upgradeableConfig.attributes as any) : {} + { + buildNum, + ...transformDefaults, // Any defaults that should be applied from transforms + }, + upgradeableConfig?.attributes ); try { diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.ts index 320fa9056fcaf9..34ef0c5b4f2e14 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.ts @@ -8,69 +8,70 @@ import { getUpgradeableConfig } from './get_upgradeable_config'; import { savedObjectsClientMock } from '../../saved_objects/service/saved_objects_client.mock'; +import { SavedObjectsFindResponse } from '../../saved_objects'; describe('getUpgradeableConfig', () => { it('finds saved objects with type "config"', async () => { const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.find.mockResolvedValue({ - saved_objects: [{ id: '7.5.0' }], - } as any); + saved_objects: [{ id: '7.5.0', attributes: 'foo' }], + } as SavedObjectsFindResponse); await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); expect(savedObjectsClient.find.mock.calls[0][0].type).toBe('config'); }); it('finds saved config with version < than Kibana version', async () => { - const savedConfig = { id: '7.4.0' }; + const savedConfig = { id: '7.4.0', attributes: 'foo' }; const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.find.mockResolvedValue({ saved_objects: [savedConfig], - } as any); + } as SavedObjectsFindResponse); const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); - expect(result).toBe(savedConfig); + expect(result).toEqual(savedConfig); }); it('finds saved config with RC version === Kibana version', async () => { - const savedConfig = { id: '7.5.0-rc1' }; + const savedConfig = { id: '7.5.0-rc1', attributes: 'foo' }; const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.find.mockResolvedValue({ saved_objects: [savedConfig], - } as any); + } as SavedObjectsFindResponse); const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); - expect(result).toBe(savedConfig); + expect(result).toEqual(savedConfig); }); it('does not find saved config with version === Kibana version', async () => { - const savedConfig = { id: '7.5.0' }; + const savedConfig = { id: '7.5.0', attributes: 'foo' }; const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.find.mockResolvedValue({ saved_objects: [savedConfig], - } as any); + } as SavedObjectsFindResponse); const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); - expect(result).toBe(undefined); + expect(result).toBe(null); }); it('does not find saved config with version > Kibana version', async () => { - const savedConfig = { id: '7.6.0' }; + const savedConfig = { id: '7.6.0', attributes: 'foo' }; const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.find.mockResolvedValue({ saved_objects: [savedConfig], - } as any); + } as SavedObjectsFindResponse); const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); - expect(result).toBe(undefined); + expect(result).toBe(null); }); it('handles empty config', async () => { const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.find.mockResolvedValue({ saved_objects: [], - } as any); + } as unknown as SavedObjectsFindResponse); const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); - expect(result).toBe(undefined); + expect(result).toBe(null); }); }); diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts index 493efa518f22ba..6c8e8cbe9ac3af 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts @@ -7,8 +7,18 @@ */ import { SavedObjectsClientContract } from '../../saved_objects/types'; +import type { ConfigAttributes } from '../saved_objects'; import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; +/** + * This contains a subset of `config` object attributes that are relevant for upgrading it using transform functions. + * It is a superset of all the attributes needed for all of the transform functions defined in `transforms.ts`. + */ +export interface UpgradeableConfigAttributes extends ConfigAttributes { + defaultIndex?: string; + isDefaultIndexMigrated?: boolean; +} + /** * Find the most recent SavedConfig that is upgradeable to the specified version * @param {Object} options @@ -24,14 +34,21 @@ export async function getUpgradeableConfig({ version: string; }) { // attempt to find a config we can upgrade - const { saved_objects: savedConfigs } = await savedObjectsClient.find({ - type: 'config', - page: 1, - perPage: 1000, - sortField: 'buildNum', - sortOrder: 'desc', - }); + const { saved_objects: savedConfigs } = + await savedObjectsClient.find({ + type: 'config', + page: 1, + perPage: 1000, + sortField: 'buildNum', + sortOrder: 'desc', + }); // try to find a config that we can upgrade - return savedConfigs.find((savedConfig) => isConfigVersionUpgradeable(savedConfig.id, version)); + const findResult = savedConfigs.find((savedConfig) => + isConfigVersionUpgradeable(savedConfig.id, version) + ); + if (findResult) { + return { id: findResult.id, attributes: findResult.attributes }; + } + return null; } diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/index.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/index.ts index 3559c8555803ae..e5f8f895e86317 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/index.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/index.ts @@ -7,3 +7,4 @@ */ export { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; +export type { UpgradeableConfigAttributes } from './get_upgradeable_config'; diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts index dc76f434f02de9..6c26ca0e38ce04 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts @@ -90,6 +90,9 @@ describe('createOrUpgradeSavedConfig()', () => { // 5.4.0-SNAPSHOT and @@version were ignored so we only have the // attributes from 5.4.0-rc1, even though the other build nums are greater '5.4.0-rc1': true, + + // Should have the transform(s) applied + isDefaultIndexMigrated: true, }); // add the 5.4.0 flag to the 5.4.0 savedConfig @@ -115,6 +118,9 @@ describe('createOrUpgradeSavedConfig()', () => { // should also include properties from 5.4.0 and 5.4.0-rc1 '5.4.0': true, '5.4.0-rc1': true, + + // Should have the transform(s) applied + isDefaultIndexMigrated: true, }); // add the 5.4.1 flag to the 5.4.1 savedConfig @@ -141,6 +147,9 @@ describe('createOrUpgradeSavedConfig()', () => { '5.4.1': true, '5.4.0': true, '5.4.0-rc1': true, + + // Should have the transform(s) applied + isDefaultIndexMigrated: true, }); // tag the 7.0.0-rc1 doc @@ -168,6 +177,9 @@ describe('createOrUpgradeSavedConfig()', () => { '5.4.1': true, '5.4.0': true, '5.4.0-rc1': true, + + // Should have the transform(s) applied + isDefaultIndexMigrated: true, }); // tag the 7.0.0 doc @@ -194,6 +206,9 @@ describe('createOrUpgradeSavedConfig()', () => { '5.4.1': true, '5.4.0': true, '5.4.0-rc1': true, + + // Should have the transform(s) applied + isDefaultIndexMigrated: true, }); }, 30000); }); diff --git a/src/core/server/ui_settings/saved_objects/index.ts b/src/core/server/ui_settings/saved_objects/index.ts index eb8e7cfcc2a1b2..92ef9961bb9084 100644 --- a/src/core/server/ui_settings/saved_objects/index.ts +++ b/src/core/server/ui_settings/saved_objects/index.ts @@ -6,4 +6,7 @@ * Side Public License, v 1. */ +export type { ConfigAttributes } from './ui_settings'; export { uiSettingsType } from './ui_settings'; +export type { TransformConfigFn } from './transforms'; +export { transforms } from './transforms'; diff --git a/src/core/server/ui_settings/saved_objects/transforms.test.ts b/src/core/server/ui_settings/saved_objects/transforms.test.ts new file mode 100644 index 00000000000000..afcd525673aa09 --- /dev/null +++ b/src/core/server/ui_settings/saved_objects/transforms.test.ts @@ -0,0 +1,115 @@ +/* + * 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 { savedObjectsClientMock } from '../../mocks'; +import { SavedObjectsErrorHelpers } from '../../saved_objects'; +import { SavedObject } from '../../types'; +import type { UpgradeableConfigAttributes } from '../create_or_upgrade_saved_config'; +import { transformDefaultIndex } from './transforms'; + +/** + * Test each transform function individually, not the entire exported `transforms` array. + */ +describe('#transformDefaultIndex', () => { + const savedObjectsClient = savedObjectsClientMock.create(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should return early if the config object has already been transformed', async () => { + const result = await transformDefaultIndex({ + savedObjectsClient, + configAttributes: { isDefaultIndexMigrated: true } as UpgradeableConfigAttributes, // We don't care about the other attributes + }); + + expect(savedObjectsClient.resolve).not.toHaveBeenCalled(); + expect(result).toEqual(null); // This is the only time we expect a null result + }); + + it('should return early if configAttributes is undefined', async () => { + const result = await transformDefaultIndex({ savedObjectsClient, configAttributes: undefined }); + + expect(savedObjectsClient.resolve).not.toHaveBeenCalled(); + expect(result).toEqual({ isDefaultIndexMigrated: true }); + }); + + it('should return early if the defaultIndex attribute is undefined', async () => { + const result = await transformDefaultIndex({ + savedObjectsClient, + configAttributes: { defaultIndex: undefined } as UpgradeableConfigAttributes, // We don't care about the other attributes + }); + + expect(savedObjectsClient.resolve).not.toHaveBeenCalled(); + expect(result).toEqual({ isDefaultIndexMigrated: true }); + }); + + describe('should resolve the data view for the defaultIndex and return the result according to the outcome', () => { + it('outcome: exactMatch', async () => { + savedObjectsClient.resolve.mockResolvedValue({ + outcome: 'exactMatch', + alias_target_id: 'another-index', // This wouldn't realistically be set if the outcome is exactMatch, but we're including it in this test to assert that the returned defaultIndex will be 'some-index' + saved_object: {} as SavedObject, // Doesn't matter + }); + const result = await transformDefaultIndex({ + savedObjectsClient, + configAttributes: { defaultIndex: 'some-index' } as UpgradeableConfigAttributes, // We don't care about the other attributes + }); + + expect(savedObjectsClient.resolve).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.resolve).toHaveBeenCalledWith('index-pattern', 'some-index'); + expect(result).toEqual({ isDefaultIndexMigrated: true, defaultIndex: 'some-index' }); + }); + + for (const outcome of ['aliasMatch' as const, 'conflict' as const]) { + it(`outcome: ${outcome}`, async () => { + savedObjectsClient.resolve.mockResolvedValue({ + outcome, + alias_target_id: 'another-index', + saved_object: {} as SavedObject, // Doesn't matter + }); + const result = await transformDefaultIndex({ + savedObjectsClient, + configAttributes: { defaultIndex: 'some-index' } as UpgradeableConfigAttributes, // We don't care about the other attributes + }); + + expect(savedObjectsClient.resolve).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.resolve).toHaveBeenCalledWith('index-pattern', 'some-index'); + expect(result).toEqual({ isDefaultIndexMigrated: true, defaultIndex: 'another-index' }); + }); + } + + it('returns the expected result if resolve fails with a Not Found error', async () => { + savedObjectsClient.resolve.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError('Oh no!') + ); + const result = await transformDefaultIndex({ + savedObjectsClient, + configAttributes: { defaultIndex: 'some-index' } as UpgradeableConfigAttributes, // We don't care about the other attributes + }); + + expect(savedObjectsClient.resolve).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.resolve).toHaveBeenCalledWith('index-pattern', 'some-index'); + expect(result).toEqual({ isDefaultIndexMigrated: true, defaultIndex: 'some-index' }); + }); + + it('returns the expected result if resolve fails with another error', async () => { + savedObjectsClient.resolve.mockRejectedValue( + SavedObjectsErrorHelpers.createIndexAliasNotFoundError('Oh no!') + ); + const result = await transformDefaultIndex({ + savedObjectsClient, + configAttributes: { defaultIndex: 'some-index' } as UpgradeableConfigAttributes, // We don't care about the other attributes + }); + + expect(savedObjectsClient.resolve).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.resolve).toHaveBeenCalledWith('index-pattern', 'some-index'); + expect(result).toEqual({ isDefaultIndexMigrated: false, defaultIndex: 'some-index' }); + }); + }); +}); diff --git a/src/core/server/ui_settings/saved_objects/transforms.ts b/src/core/server/ui_settings/saved_objects/transforms.ts new file mode 100644 index 00000000000000..cabf14a2e6469e --- /dev/null +++ b/src/core/server/ui_settings/saved_objects/transforms.ts @@ -0,0 +1,96 @@ +/* + * 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 { SavedObjectsErrorHelpers } from '../../saved_objects'; +import type { SavedObjectsClientContract } from '../../types'; +import type { UpgradeableConfigAttributes } from '../create_or_upgrade_saved_config'; + +/** + * The params needed to execute each transform function. + */ +interface TransformParams { + savedObjectsClient: SavedObjectsClientContract; + configAttributes: UpgradeableConfigAttributes | undefined; +} + +/** + * The resulting attributes that should be used when upgrading the config object. + * This should be a union of all transform function return types (A | B | C | ...). + */ +type TransformReturnType = TransformDefaultIndexReturnType; + +/** + * The return type for `transformDefaultIndex`. + * If this config object has already been upgraded, it returns `null` because it doesn't need to set different default attributes. + * Otherwise, it always sets a default for the `isDefaultIndexMigrated` attribute, and it optionally sets the `defaultIndex` attribute + * depending on the outcome. + */ +type TransformDefaultIndexReturnType = { + isDefaultIndexMigrated: boolean; + defaultIndex?: string; +} | null; + +export type TransformConfigFn = (params: TransformParams) => Promise; + +/** + * Any transforms that should be applied during `createOrUpgradeSavedConfig` need to be included in this array. + */ +export const transforms: TransformConfigFn[] = [transformDefaultIndex]; + +/** + * This optionally transforms the `defaultIndex` attribute of a config saved object. The `defaultIndex` attribute points to a data view ID, + * but those saved object IDs were regenerated in the 8.0 upgrade. That resulted in a bug where the `defaultIndex` would be broken in custom + * spaces. + * + * We are fixing this bug after the fact in 8.3, and we can't retroactively change a saved object that's already been migrated, so we use + * this transformation instead to ensure that the `defaultIndex` attribute is not broken. + * + * Note that what used to be called "index patterns" prior to 8.0 have been renamed to "data views", but the object type cannot be changed, + * so that type remains `index-pattern`. + * + * Note also that this function is only exported for unit testing. It is also included in the `transforms` export above, which is how it is + * applied during `createOrUpgradeSavedConfig`. + */ +export async function transformDefaultIndex( + params: TransformParams +): Promise { + const { savedObjectsClient, configAttributes } = params; + if (configAttributes?.isDefaultIndexMigrated) { + // This config object has already been migrated, return null because we don't need to set different defaults for the new config object. + return null; + } + if (!configAttributes?.defaultIndex) { + // If configAttributes is undefined (there's no config object being upgraded), OR if configAttributes is defined but the defaultIndex + // attribute is not set, set isDefaultIndexMigrated to true and return. This means there was no defaultIndex to upgrade, so we will just + // avoid attempting to transform this again in the future. + return { isDefaultIndexMigrated: true }; + } + + let defaultIndex = configAttributes.defaultIndex; // Retain the existing defaultIndex attribute in case we run into a resolve error + let isDefaultIndexMigrated: boolean; + try { + // The defaultIndex for this config object was created prior to 8.3, and it might refer to a data view ID that is no longer valid. + // We should try to resolve the data view and change the defaultIndex to the new ID, if necessary. + const resolvedDataView = await savedObjectsClient.resolve('index-pattern', defaultIndex); + if (resolvedDataView.outcome === 'aliasMatch' || resolvedDataView.outcome === 'conflict') { + // This resolved to an aliasMatch or conflict outcome; that means we should change the defaultIndex to the data view's new ID. + // Note, the alias_target_id field is guaranteed to exist iff the resolve outcome is aliasMatch or conflict. + defaultIndex = resolvedDataView.alias_target_id!; + } + isDefaultIndexMigrated = true; // Regardless of the resolve outcome, we now consider this defaultIndex attribute to be migrated + } catch (err) { + // If the defaultIndex is not found at all, it will throw a Not Found error and we should mark the defaultIndex attribute as upgraded. + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + isDefaultIndexMigrated = true; + } else { + // For any other error, explicitly set isDefaultIndexMigrated to false so we can try this upgrade again in the future. + isDefaultIndexMigrated = false; + } + } + return { isDefaultIndexMigrated, defaultIndex }; +} diff --git a/src/core/server/ui_settings/saved_objects/ui_settings.ts b/src/core/server/ui_settings/saved_objects/ui_settings.ts index 9db1402dc2d263..184ec5d41987a1 100644 --- a/src/core/server/ui_settings/saved_objects/ui_settings.ts +++ b/src/core/server/ui_settings/saved_objects/ui_settings.ts @@ -9,6 +9,14 @@ import { SavedObjectsType } from '../../saved_objects'; import { migrations } from './migrations'; +/** + * The `config` object type contains many attributes that are defined by consumers. + */ +export interface ConfigAttributes { + buildNum: number; + [key: string]: unknown; +} + export const uiSettingsType: SavedObjectsType = { name: 'config', hidden: false, diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.mock.ts b/src/core/server/ui_settings/ui_settings_client.test.mock.ts similarity index 52% rename from src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.mock.ts rename to src/core/server/ui_settings/ui_settings_client.test.mock.ts index cb3fb360befeb4..d2a96352b36ea4 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.mock.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.mock.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -export const getUpgradeableConfigMock = jest.fn(); -jest.doMock('./get_upgradeable_config', () => ({ - getUpgradeableConfig: getUpgradeableConfigMock, +import type { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; + +export const mockCreateOrUpgradeSavedConfig = jest.fn() as jest.MockedFunction< + typeof createOrUpgradeSavedConfig +>; +jest.mock('./create_or_upgrade_saved_config', () => ({ + createOrUpgradeSavedConfig: mockCreateOrUpgradeSavedConfig, })); diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index 99ab87610db90e..844e17e53ab89a 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -10,7 +10,7 @@ import Chance from 'chance'; import { schema } from '@kbn/config-schema'; import { loggingSystemMock } from '../logging/logging_system.mock'; -import { createOrUpgradeSavedConfigMock } from './create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock'; +import { mockCreateOrUpgradeSavedConfig } from './ui_settings_client.test.mock'; import { SavedObjectsClient } from '../saved_objects'; import { savedObjectsClientMock } from '../saved_objects/service/saved_objects_client.mock'; @@ -47,12 +47,9 @@ describe('ui settings', () => { log: logger, }); - const createOrUpgradeSavedConfig = createOrUpgradeSavedConfigMock; - return { uiSettings, savedObjectsClient, - createOrUpgradeSavedConfig, }; } @@ -84,7 +81,7 @@ describe('ui settings', () => { }); it('automatically creates the savedConfig if it is missing', async () => { - const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); + const { uiSettings, savedObjectsClient } = setup(); savedObjectsClient.update .mockRejectedValueOnce(SavedObjectsClient.errors.createGenericNotFoundError()) .mockResolvedValueOnce({} as any); @@ -92,14 +89,14 @@ describe('ui settings', () => { await uiSettings.setMany({ foo: 'bar' }); expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledWith( expect.objectContaining({ handleWriteErrors: false }) ); }); it('only tried to auto create once and throws NotFound', async () => { - const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); + const { uiSettings, savedObjectsClient } = setup(); savedObjectsClient.update.mockRejectedValue( SavedObjectsClient.errors.createGenericNotFoundError() ); @@ -112,8 +109,8 @@ describe('ui settings', () => { } expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledWith( expect.objectContaining({ handleWriteErrors: false }) ); }); @@ -374,7 +371,7 @@ describe('ui settings', () => { }); it('automatically creates the savedConfig if it is missing and returns empty object', async () => { - const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); + const { uiSettings, savedObjectsClient } = setup(); savedObjectsClient.get = jest .fn() .mockRejectedValueOnce(SavedObjectsClient.errors.createGenericNotFoundError()) @@ -384,15 +381,15 @@ describe('ui settings', () => { expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledWith( expect.objectContaining({ handleWriteErrors: true }) ); }); it('returns result of savedConfig creation in case of notFound error', async () => { - const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); - createOrUpgradeSavedConfig.mockResolvedValue({ foo: 'bar ' }); + const { uiSettings, savedObjectsClient } = setup(); + mockCreateOrUpgradeSavedConfig.mockResolvedValue({ foo: 'bar ' }); savedObjectsClient.get.mockRejectedValue( SavedObjectsClient.errors.createGenericNotFoundError() ); @@ -401,23 +398,23 @@ describe('ui settings', () => { }); it('returns an empty object on Forbidden responses', async () => { - const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); + const { uiSettings, savedObjectsClient } = setup(); const error = SavedObjectsClient.errors.decorateForbiddenError(new Error()); savedObjectsClient.get.mockRejectedValue(error); expect(await uiSettings.getUserProvided()).toStrictEqual({}); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); }); it('returns an empty object on EsUnavailable responses', async () => { - const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); + const { uiSettings, savedObjectsClient } = setup(); const error = SavedObjectsClient.errors.decorateEsUnavailableError(new Error()); savedObjectsClient.get.mockRejectedValue(error); expect(await uiSettings.getUserProvided()).toStrictEqual({}); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); }); it('throws Unauthorized errors', async () => { diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts index 3c29824d96f964..16bf9920d0d963 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts @@ -12,6 +12,11 @@ import { UsageStats } from './types'; import { REDACTED_KEYWORD } from '../../../common/constants'; import { stackManagementSchema } from './schema'; +/** + * These config keys should be redacted from any usage data, they are only used for implementation details of the config saved object. + */ +const CONFIG_KEYS_TO_REDACT = ['buildNum', 'isDefaultIndexMigrated']; + export function createCollectorFetch(getUiSettingsClient: () => IUiSettingsClient | undefined) { return async function fetchUsageStats(): Promise { const uiSettingsClient = getUiSettingsClient(); @@ -21,7 +26,7 @@ export function createCollectorFetch(getUiSettingsClient: () => IUiSettingsClien const userProvided = await uiSettingsClient.getUserProvided(); const modifiedEntries = Object.entries(userProvided) - .filter(([key]) => key !== 'buildNum') + .filter(([key]) => !CONFIG_KEYS_TO_REDACT.includes(key)) .reduce((obj: Record, [key, { userValue }]) => { const sensitive = uiSettingsClient.isSensitive(key); obj[key] = sensitive ? REDACTED_KEYWORD : userValue;