Skip to content

Commit

Permalink
Add api registry and allow it to be added into client config in data …
Browse files Browse the repository at this point in the history
…source plugin (opensearch-project#5895)

* add api registry and allow it to be added into client config

Signed-off-by: Lu Yu <nluyu@amazon.com>

* add changelog

Signed-off-by: Lu Yu <nluyu@amazon.com>

* add documentation for multi data source plugin api registry

Signed-off-by: Lu Yu <nluyu@amazon.com>

* change to resolve promise before calling getQueryClient

Signed-off-by: Lu Yu <nluyu@amazon.com>

---------

Signed-off-by: Lu Yu <nluyu@amazon.com>
  • Loading branch information
BionIT authored Feb 19, 2024
1 parent 55443f7 commit eff7cb5
Show file tree
Hide file tree
Showing 13 changed files with 129 additions and 9 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/)
- [Multiple Datasource] Add datasource picker to import saved object flyout when multiple data source is enabled ([#5781](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5781))
- [Multiple Datasource] Add interfaces to register add-on authentication method from plug-in module ([#5851](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5851))
- [Multiple Datasource] Able to Hide "Local Cluster" option from datasource DropDown ([#5827](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5827))
- [Multiple Datasource] Add api registry and allow it to be added into client config in data source plugin ([#5895](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5895))

### 🐛 Bug Fixes

Expand Down
26 changes: 25 additions & 1 deletion docs/multi-datasource/client_management_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This design is part of the OpenSearch Dashboards multi data source project [[RFC
2. How to expose data source clients to callers through clean interfaces?
3. How to maintain backwards compatibility if user turn off this feature?
4. How to manage multiple clients/connection efficiently, and not consume all the memory?
5. Where should we implement the core logic?
6. How to register custom API schema and add into the client during initialization?

## 2. Requirements

Expand Down Expand Up @@ -87,6 +89,20 @@ Current `opensearch service` exists in core. The module we'll implement has simi
2. We don't mess up with OpenSearch Dashboards core, since this is an experimental feature, the potential risk of breaking existing behavior will be lowered if we use plugin. Worst case, user could just uninstall the plugin.
3. Complexity wise, it's about the same amount of work.

**6.How to register custom API schema and add into the client during initialization?**
Currently, OpenSearch Dashboards plugins uses the following to initialize instance of Cluster client and register the custom API schema via the plugins configuration option.
```ts
core.opensearch.legacy.createClient(
'exampleName',
{
plugins: [ExamplePlugin],
}
);
```
The downside of this approach is the schema is defined inside the plugin and there is no centralized registry for the schema making it not easy to access. This will be resolved by implementing a centralized API schema registry, and consumers can add data source plugin as dependency and be able to consume all the registered schema, eg. `dataSource.registerCustomApiSchema(sqlPlugin)`.

The schema will be added into the client configuration when multi data source client is initiated.

### 4.1 Data Source Plugin

Create a data source plugin that only has server side code, to hold most core logic of data source feature. Including data service, crypto service, and client management. A plugin will have all setup, start and stop as lifecycle.
Expand Down Expand Up @@ -146,12 +162,20 @@ context.core.opensearch.legacy.client.callAsCurrentUser;
context.core.opensearch.client.asCurrentUser;
```

Since deprecating legacy client could be a bigger scope of project, multiple data source feature still need to implement a substitute for it as for now. Implementation should be done in a way that's decoupled with data source client as much as possible, for easier deprecation. Similar to [opensearch legacy service](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/core/server/opensearch/legacy) in core.
Since deprecating legacy client could be a bigger scope of project, multiple data source feature still need to implement a substitute for it as for now. Implementation should be done in a way that's decoupled with data source client as much as possible, for easier deprecation. Similar to [opensearch legacy service](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/core/server/opensearch/legacy) in core. See how to intialize the data source client below:

```ts
context.dataSource.opensearch.legacy.getClient(dataSourceId);
```

If using Legacy cluster client with asScoped and callAsCurrentUser, the following is the equivalent when using data source client:
```ts
//legacy cluster client
const response = client.asScoped(request).callAsCurrentUser(format, params);
//equivalent when using data source client instead
const response = client.callAPI(format, params);
```

### 4.3 Register datasource client to core context

This is for plugin to access data source client via request handler. For example, by `core.client.search(params)`. It’s a very common use case for plugin to access cluster while handling request. In fact data plugin uses it in its search module to get client, and I’ll talk about it in details in next section.
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/data_source/server/client/client_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ import { DataSourcePluginConfigType } from '../../config';
export function parseClientOptions(
// TODO: will use client configs, that comes from a merge result of user config and default opensearch client config,
config: DataSourcePluginConfigType,
endpoint: string
endpoint: string,
registeredSchema: any[]
): ClientOptions {
const clientOptions: ClientOptions = {
node: endpoint,
ssl: {
requestCert: true,
rejectUnauthorized: true,
},
plugins: registeredSchema,
};

return clientOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { opensearchClientMock } from '../../../../core/server/opensearch/client/
import { cryptographyServiceSetupMock } from '../cryptography_service.mocks';
import { CryptographyServiceSetup } from '../cryptography_service';
import { DataSourceClientParams } from '../types';
import { CustomApiSchemaRegistry } from '../schema_registry';

const DATA_SOURCE_ID = 'a54b76ec86771ee865a0f74a305dfff8';

Expand All @@ -38,12 +39,14 @@ describe('configureClient', () => {
let dataSourceClientParams: DataSourceClientParams;
let usernamePasswordAuthContent: UsernamePasswordTypedContent;
let sigV4AuthContent: SigV4Content;
let customApiSchemaRegistry: CustomApiSchemaRegistry;

beforeEach(() => {
dsClient = opensearchClientMock.createInternalClient();
logger = loggingSystemMock.createLogger();
savedObjectsMock = savedObjectsClientMock.create();
cryptographyMock = cryptographyServiceSetupMock.create();
customApiSchemaRegistry = new CustomApiSchemaRegistry();

config = {
enabled: true,
Expand Down Expand Up @@ -95,6 +98,7 @@ describe('configureClient', () => {
dataSourceId: DATA_SOURCE_ID,
savedObjects: savedObjectsMock,
cryptography: cryptographyMock,
customApiSchemaRegistryPromise: Promise.resolve(customApiSchemaRegistry),
};

ClientMock.mockImplementation(() => dsClient);
Expand Down
15 changes: 13 additions & 2 deletions src/plugins/data_source/server/client/configure_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ import {
} from './configure_client_utils';

export const configureClient = async (
{ dataSourceId, savedObjects, cryptography, testClientDataSourceAttr }: DataSourceClientParams,
{
dataSourceId,
savedObjects,
cryptography,
testClientDataSourceAttr,
customApiSchemaRegistryPromise,
}: DataSourceClientParams,
openSearchClientPoolSetup: OpenSearchClientPoolSetup,
config: DataSourcePluginConfigType,
logger: Logger
Expand Down Expand Up @@ -64,10 +70,13 @@ export const configureClient = async (
dataSourceId
) as Client;

const registeredSchema = (await customApiSchemaRegistryPromise).getAll();

return await getQueryClient(
dataSource,
openSearchClientPoolSetup.addClientToPool,
config,
registeredSchema,
cryptography,
rootClient,
dataSourceId,
Expand All @@ -87,6 +96,7 @@ export const configureClient = async (
*
* @param rootClient root client for the given data source.
* @param dataSourceAttr data source saved object attributes
* @param registeredSchema registered API schema
* @param cryptography cryptography service for password encryption / decryption
* @param config data source config
* @param addClientToPool function to add client to client pool
Expand All @@ -98,6 +108,7 @@ const getQueryClient = async (
dataSourceAttr: DataSourceAttributes,
addClientToPool: (endpoint: string, authType: AuthType, client: Client | LegacyClient) => void,
config: DataSourcePluginConfigType,
registeredSchema: any[],
cryptography?: CryptographyServiceSetup,
rootClient?: Client,
dataSourceId?: string,
Expand All @@ -107,7 +118,7 @@ const getQueryClient = async (
auth: { type },
endpoint,
} = dataSourceAttr;
const clientOptions = parseClientOptions(config, endpoint);
const clientOptions = parseClientOptions(config, endpoint, registeredSchema);
const cacheKey = generateCacheKey(dataSourceAttr, dataSourceId);

switch (type) {
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/data_source/server/legacy/client_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import { DataSourcePluginConfigType } from '../../config';
export function parseClientOptions(
// TODO: will use client configs, that comes from a merge result of user config and default legacy client config,
config: DataSourcePluginConfigType,
endpoint: string
endpoint: string,
registeredSchema: any[]
): ConfigOptions {
const configOptions: ConfigOptions = {
host: endpoint,
ssl: {
rejectUnauthorized: true,
},
plugins: registeredSchema,
};

return configOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { OpenSearchClientPoolSetup } from '../client';
import { ConfigOptions } from 'elasticsearch';
import { ClientMock, parseClientOptionsMock } from './configure_legacy_client.test.mocks';
import { configureLegacyClient } from './configure_legacy_client';
import { CustomApiSchemaRegistry } from '../schema_registry';

const DATA_SOURCE_ID = 'a54b76ec86771ee865a0f74a305dfff8';

Expand All @@ -35,6 +36,7 @@ describe('configureLegacyClient', () => {
};
let dataSourceClientParams: DataSourceClientParams;
let callApiParams: LegacyClientCallAPIParams;
const customApiSchemaRegistry = new CustomApiSchemaRegistry();

const mockResponse = { data: 'ping' };

Expand Down Expand Up @@ -98,6 +100,7 @@ describe('configureLegacyClient', () => {
dataSourceId: DATA_SOURCE_ID,
savedObjects: savedObjectsMock,
cryptography: cryptographyMock,
customApiSchemaRegistryPromise: Promise.resolve(customApiSchemaRegistry),
};

ClientMock.mockImplementation(() => mockOpenSearchClientInstance);
Expand Down
14 changes: 12 additions & 2 deletions src/plugins/data_source/server/legacy/configure_legacy_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ import {
} from '../client/configure_client_utils';

export const configureLegacyClient = async (
{ dataSourceId, savedObjects, cryptography }: DataSourceClientParams,
{
dataSourceId,
savedObjects,
cryptography,
customApiSchemaRegistryPromise,
}: DataSourceClientParams,
callApiParams: LegacyClientCallAPIParams,
openSearchClientPoolSetup: OpenSearchClientPoolSetup,
config: DataSourcePluginConfigType,
Expand All @@ -50,12 +55,15 @@ export const configureLegacyClient = async (
dataSourceId
) as LegacyClient;

const registeredSchema = (await customApiSchemaRegistryPromise).getAll();

return await getQueryClient(
dataSourceAttr,
cryptography,
callApiParams,
openSearchClientPoolSetup.addClientToPool,
config,
registeredSchema,
rootClient,
dataSourceId
);
Expand All @@ -75,6 +83,7 @@ export const configureLegacyClient = async (
* @param dataSourceAttr data source saved object attributes
* @param cryptography cryptography service for password encryption / decryption
* @param config data source config
* @param registeredSchema registered API schema
* @param addClientToPool function to add client to client pool
* @param dataSourceId id of data source saved Object
* @returns child client.
Expand All @@ -85,14 +94,15 @@ const getQueryClient = async (
{ endpoint, clientParams, options }: LegacyClientCallAPIParams,
addClientToPool: (endpoint: string, authType: AuthType, client: Client | LegacyClient) => void,
config: DataSourcePluginConfigType,
registeredSchema: any[],
rootClient?: LegacyClient,
dataSourceId?: string
) => {
const {
auth: { type },
endpoint: nodeUrl,
} = dataSourceAttr;
const clientOptions = parseClientOptions(config, nodeUrl);
const clientOptions = parseClientOptions(config, nodeUrl, registeredSchema);
const cacheKey = generateCacheKey(dataSourceAttr, dataSourceId);

switch (type) {
Expand Down
17 changes: 15 additions & 2 deletions src/plugins/data_source/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { ensureRawRequest } from '../../../../src/core/server/http/router';
import { createDataSourceError } from './lib/error';
import { registerTestConnectionRoute } from './routes/test_connection';
import { AuthenticationMethodRegistery, IAuthenticationMethodRegistery } from './auth_registry';
import { CustomApiSchemaRegistry } from './schema_registry';

export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourcePluginStart> {
private readonly logger: Logger;
Expand All @@ -39,6 +40,7 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
private readonly config$: Observable<DataSourcePluginConfigType>;
private started = false;
private authMethodsRegistry = new AuthenticationMethodRegistery();
private customApiSchemaRegistry = new CustomApiSchemaRegistry();

constructor(private initializerContext: PluginInitializerContext<DataSourcePluginConfigType>) {
this.logger = this.initializerContext.logger.get();
Expand Down Expand Up @@ -104,6 +106,11 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
return dataSourcePluginStart.getAuthenticationMethodRegistery();
});

const customApiSchemaRegistryPromise = core.getStartServices().then(([, , selfStart]) => {
const dataSourcePluginStart = selfStart as DataSourcePluginStart;
return dataSourcePluginStart.getCustomApiSchemaRegistry();
});

// Register data source plugin context to route handler context
core.http.registerRouteHandlerContext(
'dataSource',
Expand All @@ -112,7 +119,8 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
cryptographyServiceSetup,
this.logger,
auditTrailPromise,
authRegistryPromise
authRegistryPromise,
customApiSchemaRegistryPromise
)
);

Expand All @@ -135,6 +143,7 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
return {
createDataSourceError: (e: any) => createDataSourceError(e),
registerCredentialProvider,
registerCustomApiSchema: (schema: any) => this.customApiSchemaRegistry.register(schema),
};
}

Expand All @@ -143,6 +152,7 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
this.started = true;
return {
getAuthenticationMethodRegistery: () => this.authMethodsRegistry,
getCustomApiSchemaRegistry: () => this.customApiSchemaRegistry,
};
}

Expand All @@ -155,7 +165,8 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
cryptography: CryptographyServiceSetup,
logger: Logger,
auditTrailPromise: Promise<AuditorFactory>,
authRegistryPromise: Promise<IAuthenticationMethodRegistery>
authRegistryPromise: Promise<IAuthenticationMethodRegistery>,
customApiSchemaRegistryPromise: Promise<CustomApiSchemaRegistry>
): IContextProvider<RequestHandler<unknown, unknown, unknown>, 'dataSource'> => {
return (context, req) => {
return {
Expand All @@ -169,6 +180,7 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
dataSourceId,
savedObjects: context.core.savedObjects.client,
cryptography,
customApiSchemaRegistryPromise,
});
},
legacy: {
Expand All @@ -177,6 +189,7 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
dataSourceId,
savedObjects: context.core.savedObjects.client,
cryptography,
customApiSchemaRegistryPromise,
});
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { CustomApiSchemaRegistry } from './custom_api_schema_registry';

describe('CustomApiSchemaRegistry', () => {
let registry: CustomApiSchemaRegistry;

beforeEach(() => {
registry = new CustomApiSchemaRegistry();
});

it('allows to register and get api schema', () => {
const sqlPlugin = () => {};
registry.register(sqlPlugin);
expect(registry.getAll()).toEqual([sqlPlugin]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
export class CustomApiSchemaRegistry {
private readonly schemaRegistry: any[];

constructor() {
this.schemaRegistry = new Array();
}

public register(schema: any) {
this.schemaRegistry.push(schema);
}

public getAll(): any[] {
return this.schemaRegistry;
}
}
6 changes: 6 additions & 0 deletions src/plugins/data_source/server/schema_registry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { CustomApiSchemaRegistry } from './custom_api_schema_registry';
Loading

0 comments on commit eff7cb5

Please sign in to comment.