diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index d146f2d4c87e..178c2295ba2a 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -45,7 +45,7 @@ function createManifest( requiredPlugins: required, optionalPlugins: optional, requiredBundles: [], - requiredEnginePlugins: { 'plugin-1': 'some-version' }, + requiredEnginePlugins: {}, } as DiscoveredPlugin; } diff --git a/src/core/server/cross_compatibility/README.md b/src/core/server/cross_compatibility/README.md new file mode 100644 index 000000000000..88b4aaefe424 --- /dev/null +++ b/src/core/server/cross_compatibility/README.md @@ -0,0 +1,78 @@ +## Cross Compatibility Service + +The cross compatibility service provides a way for OpenSearch Dashboards plugins to check if they are compatible with the installed OpenSearch plugins. This allows plugins to gracefully degrade their functionality or disable themselves if they are not compatible with the current OpenSearch plugin version. + +### Overview + +OpenSearch Dashboards plugins depend on specific versions of OpenSearch plugins. When a plugin is installed, OpenSearch Dashboards checks to make sure that the required OpenSearch plugins are installed and compatible. If a required plugin is not installed or is not compatible, OpenSearch Dashboards will log a warning but will still allow the plugin to start. + +The cross compatibility service provides a way for plugins to check for compatibility with their OpenSearch counterparts. This allows plugins to make informed decisions about how to behave when they are not compatible. For example, a plugin could disable itself, limit its functionality, or notify the user that they are using an incompatible plugin. + +### Usage + +To use the Cross Compatibility service, plugins can call the `verifyOpenSearchPluginsState()` API. This API checks the compatibility of the plugin with the installed OpenSearch plugins. The API returns a list of `CrossCompatibilityResult` objects, which contain information about the compatibility of each plugin. + +The `CrossCompatibilityResult` object has the following properties: + +`pluginName`: The OpenSearch Plugin name. +`isCompatible`: A boolean indicating whether the plugin is compatible. +`incompatibilityReason`: The reason the OpenSearch Plugin version is not compatible with the plugin. +`installedVersions`: The version of the plugin that is installed. + +Plugins can use the information in the `CrossCompatibilityResult` object to decide how to behave. For example, a plugin could disable itself if the `isCompatible` property is false. + +The `verifyOpenSearchPluginsState()` API should be called from the `start()` lifecycle method. This allows plugins to check for compatibility before they start. + +### Example usage inside DashboardsSample Plugin + +``` +export class DashboardsSamplePlugin implements Plugin { + + public setup(core: CoreSetup) { + this.logger.debug('Dashboard sample plugin setup'); + this.capabilitiesService = core.capabilities; + return {}; + } + public start(core: CoreStart) { + this.logger.debug('Dashboard sample plugin: Started'); + exampleCompatibilityCheck(core); + return {}; + } + ...... + + // Example capability provider + export const capabilitiesProvider = () => ({ + exampleDashboardsPlugin: { + show: true, + createShortUrl: true, + }, + }); + + function exampleCompatibilityCheck(core: CoreStart) { + const pluginName = 'exampleDashboardsPlugin'; + const result = await core.versionCompatibility.verifyOpenSearchPluginsState(pluginName); + result.forEach((mustHavePlugin) => { + if (!mustHavePlugin.isCompatible) { + // use capabilities provider API to register plugin's capability to enable/disbale plugin + this.capabilitiesService.registerProvider(capabilitiesProvider); + } + else { // feature to enable when plugin has compatible version installed } + }); + ...... + } + ..... +} + +``` +The `exampleCompatibilityCheck()` function uses the `verifyOpenSearchPluginsState()` API to check for compatibility with the `DashboardsSample` plugin. If the plugin is compatible, the function enables the plugin's features. If the plugin is not compatible, the function gracefully degrades the plugin's functionality. + +### Use cases: + +The cross compatibility service can be used by plugins to: + +* Disable themselves if they are not compatible with the installed OpenSearch plugins. +* Limit their functionality if they are not fully compatible with the installed OpenSearch plugins. +* Notify users if they are using incompatible plugins. +* Provide information to users about how to upgrade their plugins. + +The cross compatibility service is a valuable tool for developers who are building plugins for OpenSearch Dashboards. It allows plugins to be more resilient to changes in the OpenSearch ecosystem. \ No newline at end of file diff --git a/src/core/server/cross_compatibility/cross_compatibility_service.test.ts b/src/core/server/cross_compatibility/cross_compatibility_service.test.ts index f34e819a5418..076af55c21ca 100644 --- a/src/core/server/cross_compatibility/cross_compatibility_service.test.ts +++ b/src/core/server/cross_compatibility/cross_compatibility_service.test.ts @@ -26,7 +26,7 @@ describe('CrossCompatibilityService', () => { } as any); plugins?.set('foo', { 'os-plugin': '1.0.0 - 2.0.0' }); - plugins?.set('bar', { 'os-plugin': '^3.0.0' }); + plugins?.set('incompatiblePlugin', { 'os-plugin': '^3.0.0' }); plugins?.set('test', {}); service = new CrossCompatibilityService(mockCoreContext.create()); }); @@ -48,8 +48,8 @@ describe('CrossCompatibilityService', () => { expect(results.length).toEqual(1); expect(results[0].pluginName).toEqual('os-plugin'); expect(results[0].isCompatible).toEqual(true); - expect(results[0].incompatibleReason).toEqual(''); - expect(results[0].installedVersions).toEqual(new Set(['1.1.0.0'])); + expect(results[0].incompatibilityReason).toEqual(''); + expect(results[0].installedVersions).toEqual(['1.1.0.0']); expect(opensearch.client.asInternalUser.cat.plugins).toHaveBeenCalledTimes(1); }); @@ -64,7 +64,7 @@ describe('CrossCompatibilityService', () => { }); it('should return an array of CrossCompatibilityResult objects with the incompatible reason if the plugin is not installed', async () => { - const pluginName = 'bar'; + const pluginName = 'incompatiblePlugin'; const startDeps = { opensearch, plugins }; const startResult = await service.start(startDeps); const results = await startResult.verifyOpenSearchPluginsState(pluginName); @@ -72,10 +72,10 @@ describe('CrossCompatibilityService', () => { expect(results.length).toEqual(1); expect(results[0].pluginName).toEqual('os-plugin'); expect(results[0].isCompatible).toEqual(false); - expect(results[0].incompatibleReason).toEqual( + expect(results[0].incompatibilityReason).toEqual( 'OpenSearch plugin "os-plugin" in the version range "^3.0.0" is not installed on the OpenSearch for the OpenSearch Dashboards plugin to function as expected.' ); - expect(results[0].installedVersions).toEqual(new Set(['1.1.0.0'])); + expect(results[0].installedVersions).toEqual(['1.1.0.0']); expect(opensearch.client.asInternalUser.cat.plugins).toHaveBeenCalledTimes(1); }); }); diff --git a/src/core/server/cross_compatibility/cross_compatibility_service.ts b/src/core/server/cross_compatibility/cross_compatibility_service.ts index 370c51fbf555..cb088e7feac7 100644 --- a/src/core/server/cross_compatibility/cross_compatibility_service.ts +++ b/src/core/server/cross_compatibility/cross_compatibility_service.ts @@ -20,15 +20,15 @@ export class CrossCompatibilityService { private readonly log: Logger; constructor(coreContext: CoreContext) { - this.log = coreContext.logger.get('version-compatibility-service'); + this.log = coreContext.logger.get('cross-compatibility-service'); } start({ opensearch, plugins }: StartDeps): CrossCompatibilityServiceStart { - this.log.warn('Starting version compatibility service'); + this.log.warn('Starting cross compatibility service'); return { verifyOpenSearchPluginsState: (pluginName: string) => { const pluginOpenSearchDeps = plugins.get(pluginName) || {}; - return this.verifyOpenSearchPluginsState(opensearch, pluginOpenSearchDeps); + return this.verifyOpenSearchPluginsState(opensearch, pluginOpenSearchDeps, pluginName); }, }; } @@ -48,17 +48,15 @@ export class CrossCompatibilityService { } } - public async checkPluginVersionCompatibility( + public checkPluginVersionCompatibility( pluginOpenSearchDeps: CompatibleEnginePluginVersions, - opensearchInstalledPlugins: CatPluginsResponse + opensearchInstalledPlugins: CatPluginsResponse, + dashboardsPluginName: string ) { const results: CrossCompatibilityResult[] = []; for (const [pluginName, versionRange] of Object.entries(pluginOpenSearchDeps)) { // add check to see if the Dashboards plugin version is compatible with installed OpenSearch plugin - const { - isCompatible, - installedPluginVersions, - } = await this.isVersionCompatibleOSPluginInstalled( + const { isCompatible, installedPluginVersions } = this.isVersionCompatibleOSPluginInstalled( opensearchInstalledPlugins, pluginName, versionRange @@ -66,7 +64,7 @@ export class CrossCompatibilityService { results.push({ pluginName, isCompatible: !isCompatible ? false : true, - incompatibleReason: !isCompatible + incompatibilityReason: !isCompatible ? `OpenSearch plugin "${pluginName}" in the version range "${versionRange}" is not installed on the OpenSearch for the OpenSearch Dashboards plugin to function as expected.` : '', installedVersions: installedPluginVersions, @@ -74,7 +72,7 @@ export class CrossCompatibilityService { if (!isCompatible) { this.log.warn( - `OpenSearch plugin "${pluginName}" is not installed on the cluster for the OpenSearch Dashboards plugin to function as expected.` + `OpenSearch plugin "${pluginName}" is not installed on the cluster for the OpenSearch Dashboards plugin "${dashboardsPluginName}" to function as expected.` ); } } @@ -83,19 +81,21 @@ export class CrossCompatibilityService { private async verifyOpenSearchPluginsState( opensearch: OpenSearchServiceStart, - pluginOpenSearchDeps: CompatibleEnginePluginVersions + pluginOpenSearchDeps: CompatibleEnginePluginVersions, + pluginName: string ): Promise { - this.log.warn('Checking OpenSearch Plugin version compatibility'); + this.log.info('Checking OpenSearch Plugin version compatibility'); // make _cat/plugins?format=json call to the OpenSearch instance const opensearchInstalledPlugins = await this.getOpenSearchPlugins(opensearch); - const results = await this.checkPluginVersionCompatibility( + const results = this.checkPluginVersionCompatibility( pluginOpenSearchDeps, - opensearchInstalledPlugins + opensearchInstalledPlugins, + pluginName ); return results; } - private async isVersionCompatibleOSPluginInstalled( + private isVersionCompatibleOSPluginInstalled( opensearchInstalledPlugins: CatPluginsResponse, depPluginName: string, depPluginVersionRange: string @@ -110,6 +110,6 @@ export class CrossCompatibilityService { } } }); - return { isCompatible, installedPluginVersions }; + return { isCompatible, installedPluginVersions: [...installedPluginVersions] }; } } diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 122da8f3316e..dc69dec97b45 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -176,9 +176,10 @@ export class PluginsSystem { for (const pluginName of this.satupPlugins) { this.log.debug(`For plugin "${pluginName}"...`); const plugin = this.plugins.get(pluginName)!; - await this.crossCompatibilityService.checkPluginVersionCompatibility( + this.crossCompatibilityService.checkPluginVersionCompatibility( plugin.requiredEnginePlugins, - opensearchInstalledPlugins + opensearchInstalledPlugins, + pluginName ); } } diff --git a/src/core/types/cross_compatibility.ts b/src/core/types/cross_compatibility.ts index e838b1159512..38cd061a5dce 100644 --- a/src/core/types/cross_compatibility.ts +++ b/src/core/types/cross_compatibility.ts @@ -15,11 +15,11 @@ export interface CrossCompatibilityResult { * The reason the OpenSearch Plugin version is not compatible with the plugin. * This will be `undefined` if the OpenSearch Plugin version is compatible. */ - incompatibleReason?: string; + incompatibilityReason?: string; /** - * The set of versions of dependency OpenSearch Plugin if any present on the cluster. - * This will be empty set if the OpenSearch Plugin is not present. + * The array of versions of dependency OpenSearch Plugin if any present on the cluster. + * This will be empty if the OpenSearch Plugin is not present. */ - installedVersions: Set; + installedVersions: string[]; }