Skip to content

Commit

Permalink
Add new core service to expose functionality to verify plugin compati…
Browse files Browse the repository at this point in the history
…bility with OpenSearch plugins

Signed-off-by: Manasvini B Suryanarayana <[email protected]>
  • Loading branch information
manasvinibs committed Oct 11, 2023
1 parent a23e485 commit bbfc88b
Show file tree
Hide file tree
Showing 22 changed files with 336 additions and 129 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [Theme] Use themes' definitions to render the initial view ([#4936](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4936))
- [Theme] Make `next` theme the default ([#4854](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4854))
- [Discover] Update embeddable for saved searches ([#5081](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5081))
- [Decouple] Add new cross compatibility check core service which export functionality for plugins to verify if their OpenSearch plugin counterpart is installed on the cluster or has incompatible version to configure the plugin behavior([#4710](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4710))

### 🐛 Bug Fixes

Expand Down
1 change: 1 addition & 0 deletions src/core/public/plugins/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function createManifest(
requiredPlugins: required,
optionalPlugins: optional,
requiredBundles: [],
requiredEnginePlugins: { 'plugin-1': 'some-version' },
} as DiscoveredPlugin;
}

Expand Down
2 changes: 1 addition & 1 deletion src/core/public/plugins/plugins_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ function createManifest(
version: 'some-version',
configPath: ['path'],
requiredPlugins: required,
requiredEnginePlugins: optional,
requiredEnginePlugins: {},
optionalPlugins: optional,
requiredBundles: [],
};
Expand Down
17 changes: 17 additions & 0 deletions src/core/server/cross_compatibility/cross_compatibility.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { CrossCompatibilityServiceStart } from './types';

const createStartContractMock = () => {
const startContract: jest.Mocked<CrossCompatibilityServiceStart> = {
verifyOpenSearchPluginsState: jest.fn().mockReturnValue(Promise.resolve({})),
};
return startContract;
};

export const crossCompatibilityServiceMock = {
createStartContract: createStartContractMock,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { CrossCompatibilityService } from './cross_compatibility_service';
import { CompatibleEnginePluginVersions } from '../plugins/types';
import { mockCoreContext } from '../core_context.mock';
import { opensearchServiceMock } from '../opensearch/opensearch_service.mock';

describe('CrossCompatibilityService', () => {
let service: CrossCompatibilityService;
let opensearch: any;
const plugins = new Map<string, CompatibleEnginePluginVersions>();

beforeEach(() => {
opensearch = opensearchServiceMock.createStart();
opensearch.client.asInternalUser.cat.plugins.mockResolvedValue({
body: [
{
name: 'node1',
component: 'os-plugin',
version: '1.1.0.0',
},
],
} as any);

plugins?.set('foo', { 'os-plugin': '1.0.0 - 2.0.0' });
plugins?.set('bar', { 'os-plugin': '^3.0.0' });
plugins?.set('test', {});
service = new CrossCompatibilityService(mockCoreContext.create());
});

it('should start the cross compatibility service', async () => {
const startDeps = { opensearch, plugins };
const startResult = await service.start(startDeps);
expect(startResult).toEqual({
verifyOpenSearchPluginsState: expect.any(Function),
});
});

it('should return an array of CrossCompatibilityResult objects if plugin dependencies are specified', async () => {
const pluginName = 'foo';
const startDeps = { opensearch, plugins };
const startResult = await service.start(startDeps);
const results = await startResult.verifyOpenSearchPluginsState(pluginName);
expect(results).not.toBeUndefined();
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(opensearch.client.asInternalUser.cat.plugins).toHaveBeenCalledTimes(1);
});

it('should return an empty array if no plugin dependencies are specified', async () => {
const pluginName = 'test';
const startDeps = { opensearch, plugins };
const startResult = await service.start(startDeps);
const results = await startResult.verifyOpenSearchPluginsState(pluginName);
expect(results).not.toBeUndefined();
expect(results.length).toEqual(0);
expect(opensearch.client.asInternalUser.cat.plugins).toHaveBeenCalledTimes(1);
});

it('should return an array of CrossCompatibilityResult objects with the incompatible reason if the plugin is not installed', async () => {
const pluginName = 'bar';
const startDeps = { opensearch, plugins };
const startResult = await service.start(startDeps);
const results = await startResult.verifyOpenSearchPluginsState(pluginName);
expect(results).not.toBeUndefined();
expect(results.length).toEqual(1);
expect(results[0].pluginName).toEqual('os-plugin');
expect(results[0].isCompatible).toEqual(false);
expect(results[0].incompatibleReason).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(opensearch.client.asInternalUser.cat.plugins).toHaveBeenCalledTimes(1);
});
});
115 changes: 115 additions & 0 deletions src/core/server/cross_compatibility/cross_compatibility_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { CatPluginsResponse } from '@opensearch-project/opensearch/api/types';
import semver from 'semver';
import { CrossCompatibilityResult, CrossCompatibilityServiceStart } from './types';
import { CoreContext } from '../core_context';
import { Logger } from '../logging';
import { OpenSearchServiceStart } from '../opensearch';
import { CompatibleEnginePluginVersions, PluginName } from '../plugins/types';

export interface StartDeps {
opensearch: OpenSearchServiceStart;
plugins: Map<PluginName, CompatibleEnginePluginVersions>;
}

export class CrossCompatibilityService {
private readonly log: Logger;

constructor(coreContext: CoreContext) {
this.log = coreContext.logger.get('version-compatibility-service');
}

start({ opensearch, plugins }: StartDeps): CrossCompatibilityServiceStart {
this.log.warn('Starting version compatibility service');
return {
verifyOpenSearchPluginsState: (pluginName: string) => {
const pluginOpenSearchDeps = plugins.get(pluginName) || {};
return this.verifyOpenSearchPluginsState(opensearch, pluginOpenSearchDeps);
},
};
}

public async getOpenSearchPlugins(opensearch: OpenSearchServiceStart) {
// Makes cat.plugin api call to fetch list of OpenSearch plugins installed on the cluster
try {
const { body } = await opensearch.client.asInternalUser.cat.plugins<any[]>({
format: 'JSON',
});
return body;
} catch (error) {
this.log.warn(
`Cat API call to OpenSearch to get list of plugins installed on the cluster has failed: ${error}`
);
return [];
}
}

public async checkPluginVersionCompatibility(
pluginOpenSearchDeps: CompatibleEnginePluginVersions,
opensearchInstalledPlugins: CatPluginsResponse
) {
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(
opensearchInstalledPlugins,
pluginName,
versionRange
);
results.push({
pluginName,
isCompatible: !isCompatible ? false : true,
incompatibleReason: !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,
});

if (!isCompatible) {
this.log.warn(
`OpenSearch plugin "${pluginName}" is not installed on the cluster for the OpenSearch Dashboards plugin to function as expected.`
);
}
}
return results;
}

private async verifyOpenSearchPluginsState(
opensearch: OpenSearchServiceStart,
pluginOpenSearchDeps: CompatibleEnginePluginVersions
): Promise<CrossCompatibilityResult[]> {
this.log.warn('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(
pluginOpenSearchDeps,
opensearchInstalledPlugins
);
return results;
}

private async isVersionCompatibleOSPluginInstalled(
opensearchInstalledPlugins: CatPluginsResponse,
depPluginName: string,
depPluginVersionRange: string
) {
let isCompatible = false;
const installedPluginVersions = new Set<string>();
opensearchInstalledPlugins.forEach((obj) => {
if (obj.component === depPluginName && obj.version) {
installedPluginVersions.add(obj.version);
if (semver.satisfies(semver.coerce(obj.version)!.version, depPluginVersionRange)) {
isCompatible = true;
}
}
});
return { isCompatible, installedPluginVersions };
}
}
7 changes: 7 additions & 0 deletions src/core/server/cross_compatibility/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { CrossCompatibilityService } from './cross_compatibility_service';
export { CrossCompatibilityResult, CrossCompatibilityServiceStart } from './types';
22 changes: 22 additions & 0 deletions src/core/server/cross_compatibility/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { CrossCompatibilityResult } from '../../types/cross_compatibility';

/**
* API to check if the OpenSearch Dashboards plugin version is compatible with the installed OpenSearch plugin.
*
* @public
*/
export interface CrossCompatibilityServiceStart {
/**
* Checks if the OpenSearch Dashboards plugin version is compatible with the installed OpenSearch plugin.
*
* @returns {Promise<CrossCompatibilityResult[]>}
*/
verifyOpenSearchPluginsState: (pluginName: string) => Promise<CrossCompatibilityResult[]>;
}

export { CrossCompatibilityResult };
4 changes: 4 additions & 0 deletions src/core/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { StatusServiceSetup } from './status';
import { Auditor, AuditTrailSetup, AuditTrailStart } from './audit_trail';
import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging';
import { CoreUsageDataStart } from './core_usage_data';
import { CrossCompatibilityServiceStart } from './cross_compatibility/types';

// Because of #79265 we need to explicity import, then export these types for
// scripts/telemetry_check.js to work as expected
Expand Down Expand Up @@ -482,6 +483,8 @@ export interface CoreStart {
auditTrail: AuditTrailStart;
/** @internal {@link CoreUsageDataStart} */
coreUsageData: CoreUsageDataStart;
/** {@link CrossCompatibilityServiceStart} */
crossCompatibility: CrossCompatibilityServiceStart;
}

export {
Expand All @@ -493,6 +496,7 @@ export {
PluginsServiceStart,
PluginOpaqueId,
AuditTrailStart,
CrossCompatibilityServiceStart,
};

/**
Expand Down
2 changes: 2 additions & 0 deletions src/core/server/internal_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { InternalStatusServiceSetup } from './status';
import { AuditTrailSetup, AuditTrailStart } from './audit_trail';
import { InternalLoggingServiceSetup } from './logging';
import { CoreUsageDataStart } from './core_usage_data';
import { CrossCompatibilityServiceStart } from './cross_compatibility';

/** @internal */
export interface InternalCoreSetup {
Expand Down Expand Up @@ -78,6 +79,7 @@ export interface InternalCoreStart {
uiSettings: InternalUiSettingsServiceStart;
auditTrail: AuditTrailStart;
coreUsageData: CoreUsageDataStart;
crossCompatibility: CrossCompatibilityServiceStart;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/core/server/legacy/legacy_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ export class LegacyService implements CoreService {
throw new Error('core.start.coreUsageData.getCoreUsageData is unsupported in legacy');
},
},
crossCompatibility: startDeps.core.crossCompatibility,
};

const router = setupDeps.core.http.createRouter('', this.legacyId);
Expand Down
4 changes: 4 additions & 0 deletions src/core/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { environmentServiceMock } from './environment/environment_service.mock';
import { statusServiceMock } from './status/status_service.mock';
import { auditTrailServiceMock } from './audit_trail/audit_trail_service.mock';
import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock';
import { crossCompatibilityServiceMock } from './cross_compatibility/cross_compatibility.mock';

export { configServiceMock } from './config/mocks';
export { httpServerMock } from './http/http_server.mocks';
Expand All @@ -69,6 +70,7 @@ export { statusServiceMock } from './status/status_service.mock';
export { contextServiceMock } from './context/context_service.mock';
export { capabilitiesServiceMock } from './capabilities/capabilities_service.mock';
export { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock';
export { crossCompatibilityServiceMock } from './cross_compatibility/cross_compatibility.mock';

export function pluginInitializerContextConfigMock<T>(config: T) {
const globalConfig: SharedGlobalConfig = {
Expand Down Expand Up @@ -172,6 +174,7 @@ function createCoreStartMock() {
savedObjects: savedObjectsServiceMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
coreUsageData: coreUsageDataServiceMock.createStartContract(),
crossCompatibility: crossCompatibilityServiceMock.createStartContract(),
};

return mock;
Expand Down Expand Up @@ -206,6 +209,7 @@ function createInternalCoreStartMock() {
uiSettings: uiSettingsServiceMock.createStartContract(),
auditTrail: auditTrailServiceMock.createStartContract(),
coreUsageData: coreUsageDataServiceMock.createStartContract(),
crossCompatibility: crossCompatibilityServiceMock.createStartContract(),
};
return startDeps;
}
Expand Down
1 change: 1 addition & 0 deletions src/core/server/plugins/plugin_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,5 +270,6 @@ export function createPluginStartContext<TPlugin, TPluginDependencies>(
},
auditTrail: deps.auditTrail,
coreUsageData: deps.coreUsageData,
crossCompatibility: deps.crossCompatibility,
};
}
1 change: 1 addition & 0 deletions src/core/server/plugins/plugins_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ describe('PluginsService', () => {
requiredPlugins: [],
requiredBundles: [],
optionalPlugins: [],
requiredEnginePlugins: {},
},
];

Expand Down
Loading

0 comments on commit bbfc88b

Please sign in to comment.