Migrate defaultIndex attribute for config saved object (#133339) (#133805)

(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:
Joe Portner 2022-06-07 13:32:55 -04:00 committed by GitHub
parent 33468db104
commit ff3c806f1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 395 additions and 79 deletions

View file

@ -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.

View file

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

View file

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

View file

@ -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 {

View file

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

View file

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

View file

@ -7,3 +7,4 @@
*/
export { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config';
export type { UpgradeableConfigAttributes } from './get_upgradeable_config';

View file

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

View file

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

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

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

View file

@ -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,

View file

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

View file

@ -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 () => {

View file

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