mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
(cherry picked from commit d732ebec91
)
# Conflicts:
# src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts
This commit is contained in:
parent
180e41ff00
commit
963b98c4c2
15 changed files with 395 additions and 79 deletions
|
@ -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.
|
||||
|
|
|
@ -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<TransformConfigFn>;
|
||||
jest.mock('../saved_objects', () => ({
|
||||
transforms: [mockTransform],
|
||||
}));
|
||||
|
||||
export const mockGetUpgradeableConfig = jest.fn() as jest.MockedFunction<
|
||||
typeof getUpgradeableConfig
|
||||
>;
|
||||
jest.mock('./get_upgradeable_config', () => ({
|
||||
getUpgradeableConfig: mockGetUpgradeableConfig,
|
||||
}));
|
||||
|
|
|
@ -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 log a message for upgrades', async () => {
|
||||
const { getUpgradeableConfig, logger, run } = setup();
|
||||
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,
|
||||
});
|
||||
|
||||
getUpgradeableConfig.mockResolvedValue({
|
||||
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 { logger, run } = setup();
|
||||
|
||||
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'));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<UpgradeableConfigAttributes>({
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
*/
|
||||
|
||||
export { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config';
|
||||
export type { UpgradeableConfigAttributes } from './get_upgradeable_config';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
115
src/core/server/ui_settings/saved_objects/transforms.test.ts
Normal file
115
src/core/server/ui_settings/saved_objects/transforms.test.ts
Normal file
|
@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
96
src/core/server/ui_settings/saved_objects/transforms.ts
Normal file
96
src/core/server/ui_settings/saved_objects/transforms.ts
Normal file
|
@ -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<TransformReturnType>;
|
||||
|
||||
/**
|
||||
* 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<TransformDefaultIndexReturnType> {
|
||||
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 };
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}));
|
|
@ -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 () => {
|
||||
|
|
|
@ -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<UsageStats | undefined> {
|
||||
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<string, unknown>, [key, { userValue }]) => {
|
||||
const sensitive = uiSettingsClient.isSensitive(key);
|
||||
obj[key] = sensitive ? REDACTED_KEYWORD : userValue;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue