Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not mutate config in place during deprecations #99629

Merged
merged 7 commits into from
May 11, 2021
Merged
60 changes: 51 additions & 9 deletions packages/kbn-config/src/deprecation/apply_deprecations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,16 @@ describe('applyDeprecations', () => {
const addDeprecation = jest.fn();
const createAddDeprecation = jest.fn().mockReturnValue(addDeprecation);
const initialConfig = { foo: 'bar', deprecated: 'deprecated' };
const alteredConfig = { foo: 'bar' };

const handlerA = jest.fn().mockReturnValue(alteredConfig);
const handlerB = jest.fn().mockImplementation((conf) => conf);
const handlerA = jest.fn().mockReturnValue({ unset: [{ path: 'deprecated' }] });
const handlerB = jest.fn().mockReturnValue(undefined);

applyDeprecations(
initialConfig,
[wrapHandler(handlerA, 'pathA'), wrapHandler(handlerB, 'pathB')],
createAddDeprecation
);

expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', addDeprecation);
expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', addDeprecation);
expect(createAddDeprecation).toBeCalledTimes(2);
expect(createAddDeprecation).toHaveBeenNthCalledWith(1, 'pathA');
expect(createAddDeprecation).toHaveBeenNthCalledWith(2, 'pathB');
Expand All @@ -60,17 +57,26 @@ describe('applyDeprecations', () => {
const initialConfig = { foo: 'bar', deprecated: 'deprecated' };
const alteredConfig = { foo: 'bar' };

const handlerA = jest.fn().mockReturnValue(alteredConfig);
const handlerB = jest.fn().mockImplementation((conf) => conf);
const configs: Array<{ fn: string; config: Record<string, any> }> = [];
const handlerA = jest.fn().mockImplementation((config) => {
// the first argument is mutated between calls, we store a copy of it
configs.push({ fn: 'handlerA', config: { ...config } });
return { unset: [{ path: 'deprecated' }] };
});
const handlerB = jest.fn().mockImplementation((config) => {
configs.push({ fn: 'handlerB', config: { ...config } });
});

applyDeprecations(
initialConfig,
[wrapHandler(handlerA, 'pathA'), wrapHandler(handlerB, 'pathB')],
createAddDeprecation
);

expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', addDeprecation);
expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', addDeprecation);
expect(configs).toEqual([
{ fn: 'handlerA', config: initialConfig },
{ fn: 'handlerB', config: alteredConfig },
]);
});

it('returns the migrated config', () => {
Expand All @@ -94,4 +100,40 @@ describe('applyDeprecations', () => {
expect(initialConfig).toEqual({ foo: 'bar', deprecated: 'deprecated' });
expect(migrated).toEqual({ foo: 'bar' });
});

it('ignores a command for unknown path', () => {
const addDeprecation = jest.fn();
const createAddDeprecation = jest.fn().mockReturnValue(addDeprecation);
const initialConfig = { foo: 'bar', deprecated: 'deprecated' };

const handler = jest.fn().mockImplementation((config) => {
return { unset: [{ path: 'unknown' }] };
});

const migrated = applyDeprecations(
initialConfig,
[wrapHandler(handler, 'pathA')],
createAddDeprecation
);

expect(migrated).toEqual(initialConfig);
});

it('ignores an unknown command', () => {
const addDeprecation = jest.fn();
const createAddDeprecation = jest.fn().mockReturnValue(addDeprecation);
const initialConfig = { foo: 'bar', deprecated: 'deprecated' };

const handler = jest.fn().mockImplementation((config) => {
return { rewrite: [{ path: 'foo' }] };
});

const migrated = applyDeprecations(
initialConfig,
[wrapHandler(handler, 'pathA')],
createAddDeprecation
);

expect(migrated).toEqual(initialConfig);
});
});
21 changes: 17 additions & 4 deletions packages/kbn-config/src/deprecation/apply_deprecations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* Side Public License, v 1.
*/

import { cloneDeep } from 'lodash';
import { cloneDeep, unset } from 'lodash';
import { set } from '@elastic/safer-lodash-set';
import { ConfigDeprecationWithContext, AddConfigDeprecation } from './types';

const noopAddDeprecationFactory: () => AddConfigDeprecation = () => () => undefined;
Expand All @@ -22,9 +23,21 @@ export const applyDeprecations = (
deprecations: ConfigDeprecationWithContext[],
createAddDeprecation: (pluginId: string) => AddConfigDeprecation = noopAddDeprecationFactory
) => {
let processed = cloneDeep(config);
const result = cloneDeep(config);
deprecations.forEach(({ deprecation, path }) => {
processed = deprecation(processed, path, createAddDeprecation(path));
const commands = deprecation(result, path, createAddDeprecation(path));
Copy link
Contributor Author

@mshustov mshustov May 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Bamieh In a follow-up, we can store all the commands to report them later. Do we need original values? Or config keys only?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mshustov for the telemetry collector we'd only need the deprecated config keys

if (commands) {
if (commands.set) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use commands?.set instead of the two if statements?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I will change in a follow-up

commands.set.forEach(function ({ path: commandPath, value }) {
set(result, commandPath, value);
});
}
if (commands.unset) {
commands.unset.forEach(function ({ path: commandPath }) {
unset(result, commandPath);
});
}
}
});
return processed;
return result;
};