Skip to content

Commit 27f3147

Browse files
committed
Adds plugin manifest config to define OpenSearch plugin dependency and verifies if it is installed
Resolves Issue -opensearch-project#2799 Signed-off-by: Manasvini B Suryanarayana <manasvis@amazon.com>
1 parent 5764d6c commit 27f3147

12 files changed

+166
-3
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
4848
- [I18n] Register ru, ru-RU locale ([#2817](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2817))
4949
- Add yarn opensearch arg to setup plugin dependencies ([#2544](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2544))
5050
- [Multi DataSource] Test the connection to an external data source when creating or updating ([#2973](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2973))
51+
- Adds plugin manifest config to define OpenSearch plugin dependency and verifies if it is installed on the cluster ([#3116](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3116))
5152

5253
### 🐛 Bug Fixes
5354

src/core/public/plugins/plugins_service.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ function createManifest(
8484
version: 'some-version',
8585
configPath: ['path'],
8686
requiredPlugins: required,
87+
requiredOpenSearchPlugins: optional,
8788
optionalPlugins: optional,
8889
requiredBundles: [],
8990
};

src/core/server/plugins/discovery/plugin_manifest_parser.test.ts

+6
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ test('set defaults for all missing optional fields', async () => {
339339
opensearchDashboardsVersion: '7.0.0',
340340
optionalPlugins: [],
341341
requiredPlugins: [],
342+
requiredOpenSearchPlugins: [],
342343
requiredBundles: [],
343344
server: true,
344345
ui: false,
@@ -357,6 +358,7 @@ test('return all set optional fields as they are in manifest', async () => {
357358
opensearchDashboardsVersion: '7.0.0',
358359
requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'],
359360
optionalPlugins: ['some-optional-plugin'],
361+
requiredOpenSearchPlugins: ['test-opensearch-plugin-1', 'test-opensearch-plugin-2'],
360362
ui: true,
361363
})
362364
)
@@ -371,6 +373,7 @@ test('return all set optional fields as they are in manifest', async () => {
371373
optionalPlugins: ['some-optional-plugin'],
372374
requiredBundles: [],
373375
requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'],
376+
requiredOpenSearchPlugins: ['test-opensearch-plugin-1', 'test-opensearch-plugin-2'],
374377
server: false,
375378
ui: true,
376379
});
@@ -387,6 +390,7 @@ test('return manifest when plugin expected OpenSearch Dashboards version matches
387390
version: 'some-version',
388391
opensearchDashboardsVersion: '7.0.0-alpha2',
389392
requiredPlugins: ['some-required-plugin'],
393+
requiredOpenSearchPlugins: [],
390394
server: true,
391395
})
392396
)
@@ -400,6 +404,7 @@ test('return manifest when plugin expected OpenSearch Dashboards version matches
400404
opensearchDashboardsVersion: '7.0.0-alpha2',
401405
optionalPlugins: [],
402406
requiredPlugins: ['some-required-plugin'],
407+
requiredOpenSearchPlugins: [],
403408
requiredBundles: [],
404409
server: true,
405410
ui: false,
@@ -430,6 +435,7 @@ test('return manifest when plugin expected OpenSearch Dashboards version is `ope
430435
opensearchDashboardsVersion: 'opensearchDashboards',
431436
optionalPlugins: [],
432437
requiredPlugins: ['some-required-plugin'],
438+
requiredOpenSearchPlugins: [],
433439
requiredBundles: [],
434440
server: true,
435441
ui: true,

src/core/server/plugins/discovery/plugin_manifest_parser.ts

+25
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const KNOWN_MANIFEST_FIELDS = (() => {
6565
version: true,
6666
configPath: true,
6767
requiredPlugins: true,
68+
requiredOpenSearchPlugins: true,
6869
optionalPlugins: true,
6970
ui: true,
7071
server: true,
@@ -160,6 +161,27 @@ export async function parseManifest(
160161
);
161162
}
162163

164+
if (
165+
manifest.requiredOpenSearchPlugins !== undefined &&
166+
!Array.isArray(manifest.requiredOpenSearchPlugins)
167+
) {
168+
throw PluginDiscoveryError.invalidManifest(
169+
manifestPath,
170+
new Error(
171+
`The "requiredOpenSearchPlugins" in plugin manifest for "${manifest.id}" should either be a string or an array of strings.`
172+
)
173+
);
174+
}
175+
176+
if (
177+
manifest.requiredOpenSearchPlugins !== undefined &&
178+
Array.isArray(manifest.requiredOpenSearchPlugins)
179+
) {
180+
log.warn(
181+
`Plugin ${manifest.id} has a dependency on backend OpenSearch plugin. You need to install it on the cluster for data source.`
182+
);
183+
}
184+
163185
const expectedOpenSearchDashboardsVersion =
164186
typeof manifest.opensearchDashboardsVersion === 'string' && manifest.opensearchDashboardsVersion
165187
? manifest.opensearchDashboardsVersion
@@ -202,6 +224,9 @@ export async function parseManifest(
202224
opensearchDashboardsVersion: expectedOpenSearchDashboardsVersion,
203225
configPath: manifest.configPath || snakeCase(manifest.id),
204226
requiredPlugins: Array.isArray(manifest.requiredPlugins) ? manifest.requiredPlugins : [],
227+
requiredOpenSearchPlugins: Array.isArray(manifest.requiredOpenSearchPlugins)
228+
? manifest.requiredOpenSearchPlugins
229+
: [],
205230
optionalPlugins: Array.isArray(manifest.optionalPlugins) ? manifest.optionalPlugins : [],
206231
requiredBundles: Array.isArray(manifest.requiredBundles) ? manifest.requiredBundles : [],
207232
ui: includesUiPlugin,

src/core/server/plugins/integration_tests/plugins_service.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe('PluginsService', () => {
5757
disabled = false,
5858
version = 'some-version',
5959
requiredPlugins = [],
60+
requiredOpenSearchPlugins = [],
6061
requiredBundles = [],
6162
optionalPlugins = [],
6263
opensearchDashboardsVersion = '7.0.0',
@@ -68,6 +69,7 @@ describe('PluginsService', () => {
6869
disabled?: boolean;
6970
version?: string;
7071
requiredPlugins?: string[];
72+
requiredOpenSearchPlugins?: string[];
7173
requiredBundles?: string[];
7274
optionalPlugins?: string[];
7375
opensearchDashboardsVersion?: string;
@@ -84,6 +86,7 @@ describe('PluginsService', () => {
8486
configPath: `${configPath}${disabled ? '-disabled' : ''}`,
8587
opensearchDashboardsVersion,
8688
requiredPlugins,
89+
requiredOpenSearchPlugins,
8790
requiredBundles,
8891
optionalPlugins,
8992
server,

src/core/server/plugins/plugin.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ function createPluginManifest(manifestProps: Partial<PluginManifest> = {}): Plug
6969
configPath: 'path',
7070
opensearchDashboardsVersion: '7.0.0',
7171
requiredPlugins: ['some-required-dep'],
72+
requiredOpenSearchPlugins: ['some-os-plugins'],
7273
optionalPlugins: ['some-optional-dep'],
7374
requiredBundles: [],
7475
server: true,

src/core/server/plugins/plugin.ts

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export class PluginWrapper<
6666
public readonly configPath: PluginManifest['configPath'];
6767
public readonly requiredPlugins: PluginManifest['requiredPlugins'];
6868
public readonly optionalPlugins: PluginManifest['optionalPlugins'];
69+
public readonly requiredOpenSearchPlugins: PluginManifest['requiredOpenSearchPlugins'];
6970
public readonly requiredBundles: PluginManifest['requiredBundles'];
7071
public readonly includesServerPlugin: PluginManifest['server'];
7172
public readonly includesUiPlugin: PluginManifest['ui'];
@@ -95,6 +96,7 @@ export class PluginWrapper<
9596
this.configPath = params.manifest.configPath;
9697
this.requiredPlugins = params.manifest.requiredPlugins;
9798
this.optionalPlugins = params.manifest.optionalPlugins;
99+
this.requiredOpenSearchPlugins = params.manifest.requiredOpenSearchPlugins;
98100
this.requiredBundles = params.manifest.requiredBundles;
99101
this.includesServerPlugin = params.manifest.server;
100102
this.includesUiPlugin = params.manifest.ui;

src/core/server/plugins/plugin_context.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ function createPluginManifest(manifestProps: Partial<PluginManifest> = {}): Plug
5656
configPath: 'path',
5757
opensearchDashboardsVersion: '7.0.0',
5858
requiredPlugins: ['some-required-dep'],
59+
requiredOpenSearchPlugins: ['some-backend-plugin'],
5960
requiredBundles: [],
6061
optionalPlugins: ['some-optional-dep'],
6162
server: true,

src/core/server/plugins/plugins_service.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const createPlugin = (
7878
disabled = false,
7979
version = 'some-version',
8080
requiredPlugins = [],
81+
requiredOpenSearchPlugins = [],
8182
requiredBundles = [],
8283
optionalPlugins = [],
8384
opensearchDashboardsVersion = '7.0.0',
@@ -89,6 +90,7 @@ const createPlugin = (
8990
disabled?: boolean;
9091
version?: string;
9192
requiredPlugins?: string[];
93+
requiredOpenSearchPlugins?: string[];
9294
requiredBundles?: string[];
9395
optionalPlugins?: string[];
9496
opensearchDashboardsVersion?: string;
@@ -105,6 +107,7 @@ const createPlugin = (
105107
configPath: `${configPath}${disabled ? '-disabled' : ''}`,
106108
opensearchDashboardsVersion,
107109
requiredPlugins,
110+
requiredOpenSearchPlugins,
108111
requiredBundles,
109112
optionalPlugins,
110113
server,

src/core/server/plugins/plugins_system.test.ts

+88-2
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,16 @@ function createPlugin(
5353
{
5454
required = [],
5555
optional = [],
56+
requiredOSPlugin = [],
5657
server = true,
5758
ui = true,
58-
}: { required?: string[]; optional?: string[]; server?: boolean; ui?: boolean } = {}
59+
}: {
60+
required?: string[];
61+
optional?: string[];
62+
requiredOSPlugin?: string[];
63+
server?: boolean;
64+
ui?: boolean;
65+
} = {}
5966
) {
6067
return new PluginWrapper({
6168
path: 'some-path',
@@ -65,6 +72,7 @@ function createPlugin(
6572
configPath: 'path',
6673
opensearchDashboardsVersion: '7.0.0',
6774
requiredPlugins: required,
75+
requiredOpenSearchPlugins: requiredOSPlugin,
6876
optionalPlugins: optional,
6977
requiredBundles: [],
7078
server,
@@ -187,7 +195,7 @@ test('correctly orders plugins and returns exposed values for "setup" and "start
187195
}
188196
const plugins = new Map([
189197
[
190-
createPlugin('order-4', { required: ['order-2'] }),
198+
createPlugin('order-4', { required: ['order-2'], requiredOSPlugin: ['test-plugin'] }),
191199
{
192200
setup: { 'order-2': 'added-as-2' },
193201
start: { 'order-2': 'started-as-2' },
@@ -244,6 +252,17 @@ test('correctly orders plugins and returns exposed values for "setup" and "start
244252
startContextMap.get(plugin.name)
245253
);
246254

255+
const opensearch = startDeps.opensearch;
256+
opensearch.client.asInternalUser.cat.plugins.mockResolvedValueOnce({
257+
body: [
258+
{
259+
name: 'node-1',
260+
component: 'test-plugin',
261+
version: 'v1',
262+
},
263+
],
264+
} as any);
265+
247266
expect([...(await pluginsSystem.setupPlugins(setupDeps))]).toMatchInlineSnapshot(`
248267
Array [
249268
Array [
@@ -517,4 +536,71 @@ describe('start', () => {
517536
const log = logger.get.mock.results[0].value as jest.Mocked<Logger>;
518537
expect(log.info).toHaveBeenCalledWith(`Starting [2] plugins: [order-1,order-0]`);
519538
});
539+
540+
it('validates opensearch plugin installation', async () => {
541+
[
542+
createPlugin('order-1', { requiredOSPlugin: ['test-plugin'] }),
543+
createPlugin('order-2'),
544+
].forEach((plugin, index) => {
545+
jest.spyOn(plugin, 'setup').mockResolvedValue(`setup-as-${index}`);
546+
jest.spyOn(plugin, 'start').mockResolvedValue(`started-as-${index}`);
547+
pluginsSystem.addPlugin(plugin);
548+
});
549+
550+
const opensearch = startDeps.opensearch;
551+
opensearch.client.asInternalUser.cat.plugins.mockResolvedValueOnce({
552+
body: [
553+
{
554+
name: 'node-1',
555+
component: 'test-plugin',
556+
version: 'v1',
557+
},
558+
],
559+
} as any);
560+
await pluginsSystem.setupPlugins(setupDeps);
561+
await pluginsSystem.startPlugins(startDeps);
562+
expect(opensearch.client.asInternalUser.cat.plugins).toHaveBeenCalledTimes(1);
563+
});
564+
565+
it('validates opensearch plugin installation and errors out when plugin is not installed', async () => {
566+
[
567+
createPlugin('id-1', { requiredOSPlugin: ['missing-opensearch-dep'] }),
568+
createPlugin('id-2'),
569+
].forEach((plugin, index) => {
570+
jest.spyOn(plugin, 'setup').mockResolvedValue(`setup-as-${index}`);
571+
jest.spyOn(plugin, 'start').mockResolvedValue(`started-as-${index}`);
572+
pluginsSystem.addPlugin(plugin);
573+
});
574+
const opensearch = startDeps.opensearch;
575+
opensearch.client.asInternalUser.cat.plugins.mockResolvedValueOnce({
576+
body: [
577+
{
578+
name: 'node-1',
579+
component: 'test-plugin1',
580+
version: 'v1',
581+
},
582+
],
583+
} as any);
584+
await pluginsSystem.setupPlugins(setupDeps);
585+
586+
await expect(pluginsSystem.startPlugins(startDeps)).rejects.toMatchInlineSnapshot(
587+
`[Error: Plugin "id-1" has a dependency on OpenSearch data source plugin "missing-opensearch-dep" which needs to be installed.]`
588+
);
589+
});
590+
591+
it('validates opensearch plugin installation and does not errors out when there is no dependency', async () => {
592+
[createPlugin('id-1'), createPlugin('id-2')].forEach((plugin, index) => {
593+
jest.spyOn(plugin, 'setup').mockResolvedValue(`setup-as-${index}`);
594+
jest.spyOn(plugin, 'start').mockResolvedValue(`started-as-${index}`);
595+
pluginsSystem.addPlugin(plugin);
596+
});
597+
const opensearch = startDeps.opensearch;
598+
opensearch.client.asInternalUser.cat.plugins.mockResolvedValueOnce({
599+
body: [],
600+
} as any);
601+
await pluginsSystem.setupPlugins(setupDeps);
602+
const pluginsStart = await pluginsSystem.startPlugins(startDeps);
603+
expect(pluginsStart).toBeInstanceOf(Map);
604+
expect(opensearch.client.asInternalUser.cat.plugins).toHaveBeenCalledTimes(1);
605+
});
520606
});

src/core/server/plugins/plugins_system.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,41 @@ export class PluginsSystem {
158158
timeout: 30 * Sec,
159159
errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`,
160160
});
161-
162161
contracts.set(pluginName, contract);
163162
}
163+
await this.healthCheckOpenSearchPlugins(deps);
164164

165165
return contracts;
166166
}
167167

168+
private async healthCheckOpenSearchPlugins(deps: PluginsServiceStartDeps) {
169+
// make _cat/plugins?format=json call on http://localhost:9200/
170+
const opensearchPlugins = await this.getOpensearchPlugins(deps);
171+
for (const pluginName of this.satupPlugins) {
172+
this.log.debug(`For plugin "${pluginName}"...`);
173+
const plugin = this.plugins.get(pluginName)!;
174+
const pluginBackendDeps = new Set([...plugin.requiredOpenSearchPlugins]);
175+
pluginBackendDeps.forEach((opensearchPlugin) => {
176+
if (!opensearchPlugins.find((obj) => obj.component === opensearchPlugin)) {
177+
this.log.error(
178+
`OpenSearch plugin dependency "${opensearchPlugin}" is not installed on the cluster for the OpenSearch Dashboard plugin to function as expected.`
179+
);
180+
throw new Error(
181+
`Plugin "${pluginName}" has a dependency on OpenSearch data source plugin "${opensearchPlugin}" which needs to be installed.`
182+
);
183+
}
184+
});
185+
}
186+
}
187+
188+
private async getOpensearchPlugins(deps: PluginsServiceStartDeps) {
189+
// Makes cat.plugin api call to fetch list of OpenSearch plugins installed on the cluster
190+
const { body } = await deps.opensearch.client.asInternalUser.cat.plugins<any[]>({
191+
format: 'JSON',
192+
});
193+
return body;
194+
}
195+
168196
public async stopPlugins() {
169197
if (this.plugins.size === 0 || this.satupPlugins.length === 0) {
170198
return;

src/core/server/plugins/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ export interface PluginManifest {
154154
*/
155155
readonly requiredPlugins: readonly PluginName[];
156156

157+
/**
158+
* An optional list of component names of the data source OpenSearch plugins that **must be** installed on the cluster
159+
* for this plugin to function properly.
160+
*/
161+
readonly requiredOpenSearchPlugins: readonly PluginName[];
162+
157163
/**
158164
* List of plugin ids that this plugin's UI code imports modules from that are
159165
* not in `requiredPlugins`.

0 commit comments

Comments
 (0)