diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index e9ea67e8a..1c57d8c03 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -7,7 +7,8 @@ ], "requiredPlugins": [ "navigation", - "opensearchDashboardsReact" + "opensearchDashboardsReact", + "dataSource" ], "optionalPlugins": [ "managementOverview" diff --git a/server/client/index.ts b/server/client/index.ts new file mode 100644 index 000000000..4320e7983 --- /dev/null +++ b/server/client/index.ts @@ -0,0 +1,111 @@ +import { get } from "lodash"; +import { + IContextProvider, + ILegacyScopedClusterClient, + OpenSearchDashboardsRequest, + RequestHandler, + RequestHandlerContext, +} from "opensearch-dashboards/server"; +import { IRequestHandlerContentWithDataSource, IGetClientProps, DashboardRequestEnhancedWithContext } from "./interface"; + +export const getClientSupportMDS = (props: IGetClientProps) => { + const originalAsScoped = props.client.asScoped; + const handler: IContextProvider, "core"> = ( + context: RequestHandlerContext, + request: OpenSearchDashboardsRequest + ) => { + (request as DashboardRequestEnhancedWithContext)[`${props.pluginId}_context`] = context as IRequestHandlerContentWithDataSource; + return {} as any; + }; + + /** + * asScoped can not get the request context, + * add _context to request + */ + props.core.http.registerRouteHandlerContext(`${props.pluginId}_MDS_CTX_SUPPORT` as "core", handler); + + /** + * it is not a good practice to rewrite the method like this + * but JS does not provide a method to copy a class instance + */ + props.client.asScoped = function (request: DashboardRequestEnhancedWithContext): ILegacyScopedClusterClient { + const context = request[`${props.pluginId}_context`]; + + /** + * If the context can not be found + * reject the request and add a log + */ + if (!context) { + const errorMessage = "There is some error between dashboards and your remote data source, please retry again."; + props.logger.error(errorMessage); + return { + callAsCurrentUser: () => Promise.reject(errorMessage), + callAsInternalUser: () => Promise.reject(errorMessage), + }; + } + + const dataSourceId = props.getDataSourceId?.(context, request); + /** + * If no dataSourceId provided + * use the original client + */ + if (!dataSourceId) { + props.logger.debug("No dataSourceId, using original client"); + return originalAsScoped.call(props.client, request); + } + + const callApi: ILegacyScopedClusterClient["callAsCurrentUser"] = async (...args) => { + const [endpoint, clientParams, options] = args; + return new Promise(async (resolve, reject) => { + props.logger.debug(`Call api using the data source: ${dataSourceId}`); + try { + const dataSourceClient = await context.dataSource.opensearch.getClient(dataSourceId); + + /** + * extend client if needed + **/ + Object.assign(dataSourceClient, { ...props.onExtendClient?.(dataSourceClient) }); + + /** + * Call the endpoint by providing client + * The logic is much the same as what callAPI does in Dashboards + */ + const clientPath = endpoint.split("."); + const api: any = get(dataSourceClient, clientPath); + let apiContext = clientPath.length === 1 ? dataSourceClient : get(dataSourceClient, clientPath.slice(0, -1)); + const request = api.call(apiContext, clientParams); + + /** + * In case the request is aborted + */ + if (options?.signal) { + options.signal.addEventListener("abort", () => { + request.abort(); + reject(new Error("Request was aborted")); + }); + } + const result = await request; + resolve(result.body || result); + } catch (e: any) { + /** + * TODO + * ask dashboard team to add original error to DataSourceError + * so that we can make the client behave exactly the same as legacy client + */ + reject(e); + } + }); + }; + + /** + * Return a legacy-client-like client + * so that the callers no need to change their code. + */ + const client: ILegacyScopedClusterClient = { + callAsCurrentUser: callApi, + callAsInternalUser: callApi, + }; + return client; + }; + return props.client; +}; diff --git a/server/client/interface.ts b/server/client/interface.ts new file mode 100644 index 000000000..dc3c99338 --- /dev/null +++ b/server/client/interface.ts @@ -0,0 +1,41 @@ +import { OpenSearchDashboardsClient } from "@opensearch-project/opensearch/api/opensearch_dashboards"; +import { + CoreSetup, + ILegacyCustomClusterClient, + LegacyCallAPIOptions, + Logger, + OpenSearchDashboardsRequest, + RequestHandlerContext, +} from "opensearch-dashboards/server"; + +export interface IRequestHandlerContentWithDataSource extends RequestHandlerContext { + dataSource: { + opensearch: { + getClient: (dataSourceId: string) => OpenSearchDashboardsClient; + legacy: { + getClient: ( + dataSourceId: string + ) => { + callAPI: (endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions) => Promise; + }; + }; + }; + }; +} + +export interface IGetClientProps { + core: CoreSetup; + /** + * We will rewrite the asScoped method of your client + * It would be better that create a new client before you pass in one + */ + client: ILegacyCustomClusterClient; + onExtendClient?: (client: OpenSearchDashboardsClient) => Record | undefined; + getDataSourceId?: (context: RequestHandlerContext, request: OpenSearchDashboardsRequest) => string | undefined; + pluginId: string; + logger: Logger; +} + +export type DashboardRequestEnhancedWithContext = OpenSearchDashboardsRequest & { + [contextKey: string]: IRequestHandlerContentWithDataSource; +};