Skip to content

Commit

Permalink
Migrate defaultIndex attribute for config saved object (#133339) (#13…
Browse files Browse the repository at this point in the history
…3807)

(cherry picked from commit d732ebe)

# Conflicts:
#	src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts
  • Loading branch information
jportner authored Jun 7, 2022
1 parent 180e41f commit 963b98c
Show file tree
Hide file tree
Showing 15 changed files with 394 additions and 78 deletions.
10 changes: 10 additions & 0 deletions dev_docs/tutorials/advanced_settings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}));
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
Expand All @@ -58,7 +57,6 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () {
run,
version,
savedObjectsClient,
getUpgradeableConfig,
};
}

Expand All @@ -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',
Expand All @@ -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();
Expand All @@ -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'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
*/

export { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config';
export type { UpgradeableConfigAttributes } from './get_upgradeable_config';
Loading

0 comments on commit 963b98c

Please sign in to comment.