[PluginService] Register all deprecations before running isEnabledPath (#121084) (#121159)

Co-authored-by: Alejandro Fernández Haro <alejandro.haro@elastic.co>
This commit is contained in:
Kibana Machine 2021-12-14 04:50:31 -05:00 committed by GitHub
parent 6af011eb8b
commit 35ffb8b88c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 45 deletions

View file

@ -340,7 +340,7 @@ test('throws if reading "enabled" when it is not present in the schema', async (
})
);
expect(
await expect(
async () => await configService.isEnabledAtPath('foo')
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[config validation of [foo].enabled]: definition for this key is missing"`
@ -357,7 +357,7 @@ test('throws if reading "enabled" when no schema exists', async () => {
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
expect(
await expect(
async () => await configService.isEnabledAtPath('foo')
).rejects.toThrowErrorMatchingInlineSnapshot(`"No validation schema has been defined for [foo]"`);
});
@ -372,7 +372,7 @@ test('throws if reading any config value when no schema exists', async () => {
const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig });
const configService = new ConfigService(rawConfigProvider, defaultEnv, logger);
expect(
await expect(
async () => await configService.isEnabledAtPath('foo')
).rejects.toThrowErrorMatchingInlineSnapshot(`"No validation schema has been defined for [foo]"`);
});

View file

@ -25,7 +25,7 @@ import { PluginsService } from './plugins_service';
import { PluginsSystem } from './plugins_system';
import { config } from './plugins_config';
import { take } from 'rxjs/operators';
import { DiscoveredPlugin, PluginType } from './types';
import { DiscoveredPlugin, PluginConfigDescriptor, PluginType } from './types';
const MockPluginsSystem: jest.Mock<PluginsSystem<PluginType>> = PluginsSystem as any;
@ -1065,6 +1065,65 @@ describe('PluginsService', () => {
});
});
test('"root" deprecations from one plugin should be applied before accessing other plugins config', async () => {
const pluginA = createPlugin('plugin-1-deprecations', {
type: PluginType.standard,
path: 'plugin-1-deprecations',
version: 'version-1',
});
const pluginB = createPlugin('plugin-2-deprecations', {
type: PluginType.standard,
path: 'plugin-2-deprecations',
version: 'version-2',
});
jest.doMock(
join(pluginA.path, 'server'),
() => ({
config: {
schema: schema.object({
enabled: schema.maybe(schema.boolean({ defaultValue: true })),
}),
},
}),
{ virtual: true }
);
jest.doMock(
join(pluginB.path, 'server'),
(): { config: PluginConfigDescriptor } => ({
config: {
schema: schema.object({
enabled: schema.maybe(schema.boolean({ defaultValue: true })),
renamed: schema.string(), // Mandatory string to make sure that the field is actually renamed by deprecations
}),
deprecations: ({ renameFromRoot }) => [
renameFromRoot('plugin-1-deprecations.toBeRenamed', 'plugin-2-deprecations.renamed'),
],
},
}),
{ virtual: true }
);
mockDiscover.mockReturnValue({
error$: from([]),
plugin$: from([pluginA, pluginB]),
});
config$.next({
'plugin-1-deprecations': {
toBeRenamed: 'renamed',
},
});
await expect(
pluginsService.discover({
environment: environmentPreboot,
})
).resolves.not.toThrow(); // If the rename is not applied, it'll fail
});
describe('plugin initialization', () => {
beforeEach(() => {
const prebootPlugins = [

View file

@ -8,7 +8,7 @@
import Path from 'path';
import { Observable } from 'rxjs';
import { concatMap, filter, first, map, tap, toArray } from 'rxjs/operators';
import { filter, first, map, tap, toArray } from 'rxjs/operators';
import { getFlattenedObject, pick } from '@kbn/std';
import { CoreService } from '../../types';
@ -271,50 +271,56 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
PluginName,
{ plugin: PluginWrapper; isEnabled: boolean }
>();
await plugin$
.pipe(
concatMap(async (plugin) => {
const configDescriptor = plugin.getConfigDescriptor();
if (configDescriptor) {
this.pluginConfigDescriptors.set(plugin.name, configDescriptor);
if (configDescriptor.deprecations) {
this.coreContext.configService.addDeprecationProvider(
plugin.configPath,
configDescriptor.deprecations
);
}
if (configDescriptor.exposeToUsage) {
this.pluginConfigUsageDescriptors.set(
Array.isArray(plugin.configPath) ? plugin.configPath.join('.') : plugin.configPath,
getFlattenedObject(configDescriptor.exposeToUsage)
);
}
this.coreContext.configService.setSchema(plugin.configPath, configDescriptor.schema);
}
const isEnabled = await this.coreContext.configService.isEnabledAtPath(plugin.configPath);
const plugins = await plugin$.pipe(toArray()).toPromise();
if (pluginEnableStatuses.has(plugin.name)) {
throw new Error(`Plugin with id "${plugin.name}" is already registered!`);
}
// Register config descriptors and deprecations
for (const plugin of plugins) {
const configDescriptor = plugin.getConfigDescriptor();
if (configDescriptor) {
this.pluginConfigDescriptors.set(plugin.name, configDescriptor);
if (configDescriptor.deprecations) {
this.coreContext.configService.addDeprecationProvider(
plugin.configPath,
configDescriptor.deprecations
);
}
if (configDescriptor.exposeToUsage) {
this.pluginConfigUsageDescriptors.set(
Array.isArray(plugin.configPath) ? plugin.configPath.join('.') : plugin.configPath,
getFlattenedObject(configDescriptor.exposeToUsage)
);
}
this.coreContext.configService.setSchema(plugin.configPath, configDescriptor.schema);
}
}
if (plugin.includesUiPlugin) {
const uiPluginInternalInfo =
plugin.manifest.type === PluginType.preboot
? this.prebootUiPluginInternalInfo
: this.standardUiPluginInternalInfo;
uiPluginInternalInfo.set(plugin.name, {
requiredBundles: plugin.requiredBundles,
version: plugin.manifest.version,
publicTargetDir: Path.resolve(plugin.path, 'target/public'),
publicAssetsDir: Path.resolve(plugin.path, 'public/assets'),
});
}
// Validate config and handle enabled statuses.
// NOTE: We can't do both in the same previous loop because some plugins' deprecations may affect others.
// Hence, we need all the deprecations to be registered before accessing any config parameter.
for (const plugin of plugins) {
const isEnabled = await this.coreContext.configService.isEnabledAtPath(plugin.configPath);
pluginEnableStatuses.set(plugin.name, { plugin, isEnabled });
})
)
.toPromise();
if (pluginEnableStatuses.has(plugin.name)) {
throw new Error(`Plugin with id "${plugin.name}" is already registered!`);
}
if (plugin.includesUiPlugin) {
const uiPluginInternalInfo =
plugin.manifest.type === PluginType.preboot
? this.prebootUiPluginInternalInfo
: this.standardUiPluginInternalInfo;
uiPluginInternalInfo.set(plugin.name, {
requiredBundles: plugin.requiredBundles,
version: plugin.manifest.version,
publicTargetDir: Path.resolve(plugin.path, 'target/public'),
publicAssetsDir: Path.resolve(plugin.path, 'public/assets'),
});
}
pluginEnableStatuses.set(plugin.name, { plugin, isEnabled });
}
// Add the plugins to the Plugin System if enabled and its dependencies are met
for (const [pluginName, { plugin, isEnabled }] of pluginEnableStatuses) {
this.validatePluginDependencies(plugin, pluginEnableStatuses);