Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MD] Improved error handling for the search API when a null value is passed for the dataSourceId. #5882

Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [BUG] Remove duplicate sample data as id 90943e30-9a47-11e8-b64d-95841ca0b247 ([5668](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5668))
- [BUG][Multiple Datasource] Fix datasource testing connection unexpectedly passed with wrong endpoint [#5663](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5663)
- [Table Visualization] Fix filter action buttons for split table aggregations ([#5619](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5619))
- [BUG] [Multiple Datasource] datasource id is required if there is no open-search hosts config ([#5882](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5882))
xinruiba marked this conversation as resolved.
Show resolved Hide resolved

### 🚞 Infrastructure

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { IOpenSearchSearchRequest } from '..';
export const decideClient = async (
context: RequestHandlerContext,
request: IOpenSearchSearchRequest,
withDataSourceEnabled: boolean = false,
withLongNumeralsSupport: boolean = false
): Promise<OpenSearchClient> => {
// if data source feature is disabled, return default opensearch client of current user
const client =
request.dataSourceId && context.dataSource
? await context.dataSource.opensearch.getClient(request.dataSourceId)
: withLongNumeralsSupport
? context.core.opensearch.client.asCurrentUserWithLongNumeralsSupport
: context.core.opensearch.client.asCurrentUser;
return client;
const defaultOpenSearchClient = withLongNumeralsSupport
? context.core.opensearch.client.asCurrentUserWithLongNumeralsSupport
: context.core.opensearch.client.asCurrentUser;

return withDataSourceEnabled && request.dataSourceId && context.dataSource
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we assume when data source is enabled, then context.dataSource is never undefined or vice versa?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 @xinruiba Do you have scenario where withDataSourceEnabled is true but context.dataSource is undefined?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comments~

Yes, we can assume when data source is enabled, then context.dataSource is never undefined or vice versa because:

  1. "dataSource" will always been registered as "registerRouteHandlerContext" in dataSource plugin setup which means once dataSource plugin's setup function get called, context.dataSource will never be undefined.
  2. If dataSource.enabled turns on, dataSource plugin's setup function will always get called.

In that case, withDataSourceEnabled is not needed in this decide client function. I will remove it.

Thanks

? await context.dataSource.opensearch.getClient(request.dataSourceId)
: defaultOpenSearchClient;
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,34 @@
*/

import { RequestHandlerContext } from '../../../../../core/server';
import { pluginInitializerContextConfigMock } from '../../../../../core/server/mocks';
import {
opensearchServiceMock,
pluginInitializerContextConfigMock,
} from '../../../../../core/server/mocks';
import { opensearchSearchStrategyProvider } from './opensearch_search_strategy';
import { DataSourceError } from '../../../../data_source/server/lib/error';
import { DataSourcePluginSetup } from '../../../../data_source/server';
import { SearchUsage } from '../collectors';

describe('OpenSearch search strategy', () => {
const mockLogger: any = {
debug: () => {},
};
const mockSearchUsage: SearchUsage = {
trackError(): Promise<void> {
return Promise.resolve(undefined);
},
trackSuccess(duration: number): Promise<void> {
return Promise.resolve(undefined);
},
};
const mockDataSourcePluginSetupWithDataSourceEnabled: DataSourcePluginSetup = {
createDataSourceError(err: any): DataSourceError {
return new DataSourceError({});
},
dataSourceEnabled: jest.fn(() => true),
registerCredentialProvider: jest.fn(),
};
const body = {
body: {
_shards: {
Expand Down Expand Up @@ -131,20 +150,96 @@ describe('OpenSearch search strategy', () => {
expect(response).toHaveProperty('rawResponse');
});

it('dataSource enabled, send request with dataSourceId get data source client', async () => {
const opensearchSearch = await opensearchSearchStrategyProvider(mockConfig$, mockLogger);
it('dataSource enabled, config host exist, send request with dataSourceId should get data source client', async () => {
const mockOpenSearchServiceSetup = opensearchServiceMock.createSetup();
mockOpenSearchServiceSetup.legacy.client = {
callAsInternalUser: jest.fn(),
asScoped: jest.fn(),
config: {
hosts: ['some host'],
},
};

const opensearchSearch = opensearchSearchStrategyProvider(
mockConfig$,
mockLogger,
mockSearchUsage,
mockDataSourcePluginSetupWithDataSourceEnabled,
mockOpenSearchServiceSetup
);

await opensearchSearch.search(
(mockDataSourceEnabledContext as unknown) as RequestHandlerContext,
{
dataSourceId,
}
);

expect(mockDataSourceApiCaller).toBeCalled();
expect(mockOpenSearchApiCaller).not.toBeCalled();
});

it('dataSource enabled, config host exist, send request without dataSourceId should get default client', async () => {
xinruiba marked this conversation as resolved.
Show resolved Hide resolved
const mockOpenSearchServiceSetup = opensearchServiceMock.createSetup();
mockOpenSearchServiceSetup.legacy.client = {
callAsInternalUser: jest.fn(),
asScoped: jest.fn(),
config: {
hosts: ['some host'],
},
};

const opensearchSearch = opensearchSearchStrategyProvider(
mockConfig$,
mockLogger,
mockSearchUsage,
mockDataSourcePluginSetupWithDataSourceEnabled,
mockOpenSearchServiceSetup
);

await opensearchSearch.search(
(mockDataSourceEnabledContext as unknown) as RequestHandlerContext,
{
dataSourceId: 'Some dataSourceId',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that you are testing send request without dataSourceid, but pass dataSourceId: 'Some dataSourceId'

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test cases updated, thanks for the comment.

}
);

expect(mockDataSourceApiCaller).toBeCalled();
expect(mockOpenSearchApiCaller).not.toBeCalled();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you correct this according to test description?

Copy link
Member Author

@xinruiba xinruiba Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

});

it('dataSource disabled, send request with dataSourceId get default client', async () => {
it('dataSource enabled, config host exist, send request without dataSourceId should throw exception', async () => {
const mockOpenSearchServiceSetup = opensearchServiceMock.createSetup();
mockOpenSearchServiceSetup.legacy.client = {
callAsInternalUser: jest.fn(),
asScoped: jest.fn(),
config: {
hosts: [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are passing host empty. Can you correct test case description? Also do we have test case when host is not defined, means it is commented? In that case it should take default value and call opensearch client

Copy link
Member Author

@xinruiba xinruiba Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

},
};

try {
const opensearchSearch = opensearchSearchStrategyProvider(
mockConfig$,
mockLogger,
mockSearchUsage,
mockDataSourcePluginSetupWithDataSourceEnabled,
mockOpenSearchServiceSetup
);

await opensearchSearch.search(
(mockDataSourceEnabledContext as unknown) as RequestHandlerContext,
{
dataSourceId: '',
}
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to pass empty? What if we don't pass at all? Can we cover both case pass empty or undefined value and doesn't pass at all

Copy link
Member Author

@xinruiba xinruiba Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} catch (e) {
expect(e).toBeTruthy();
expect(e).toBeInstanceOf(DataSourceError);
}
});

it('dataSource disabled, send request with dataSourceId should get default client', async () => {
const opensearchSearch = await opensearchSearchStrategyProvider(mockConfig$, mockLogger);

await opensearchSearch.search((mockContext as unknown) as RequestHandlerContext, {
Expand All @@ -154,11 +249,17 @@ describe('OpenSearch search strategy', () => {
expect(mockDataSourceApiCaller).not.toBeCalled();
});

it('dataSource enabled, send request without dataSourceId get default client', async () => {
it('dataSource disabled, send request without dataSourceId should get default client', async () => {
const opensearchSearch = await opensearchSearchStrategyProvider(mockConfig$, mockLogger);

await opensearchSearch.search((mockContext as unknown) as RequestHandlerContext, {});
expect(mockOpenSearchApiCaller).toBeCalled();
expect(mockDataSourceApiCaller).not.toBeCalled();
const dataSourceIdToBeTested = [undefined, ''];

for (const testDataSourceId of dataSourceIdToBeTested) {
await opensearchSearch.search((mockContext as unknown) as RequestHandlerContext, {
dataSourceId: testDataSourceId,
});
expect(mockOpenSearchApiCaller).toBeCalled();
expect(mockDataSourceApiCaller).not.toBeCalled();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*/

import { first } from 'rxjs/operators';
import { SharedGlobalConfig, Logger } from 'opensearch-dashboards/server';
import { SharedGlobalConfig, Logger, OpenSearchServiceSetup } from 'opensearch-dashboards/server';
import { SearchResponse } from 'elasticsearch';
import { Observable } from 'rxjs';
import { ApiResponse } from '@opensearch-project/opensearch';
Expand All @@ -50,6 +50,7 @@
logger: Logger,
usage?: SearchUsage,
dataSource?: DataSourcePluginSetup,
openSearchServiceSetup?: OpenSearchServiceSetup,
withLongNumeralsSupport?: boolean
): ISearchStrategy => {
return {
Expand All @@ -73,7 +74,19 @@
});

try {
const client = await decideClient(context, request, withLongNumeralsSupport);
const isOpenSearchHostsEmpty =
openSearchServiceSetup?.legacy.client.config.hosts.length === 0;

Check warning on line 78 in src/plugins/data/server/search/opensearch_search/opensearch_search_strategy.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/server/search/opensearch_search/opensearch_search_strategy.ts#L78

Added line #L78 was not covered by tests

if (dataSource?.dataSourceEnabled() && isOpenSearchHostsEmpty && !request.dataSourceId) {
throw new Error(`Data source id is required when no openseach hosts config provided`);

Check warning on line 81 in src/plugins/data/server/search/opensearch_search/opensearch_search_strategy.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/server/search/opensearch_search/opensearch_search_strategy.ts#L81

Added line #L81 was not covered by tests
}

const client = await decideClient(

Check warning on line 84 in src/plugins/data/server/search/opensearch_search/opensearch_search_strategy.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/server/search/opensearch_search/opensearch_search_strategy.ts#L84

Added line #L84 was not covered by tests
context,
request,
dataSource?.dataSourceEnabled(),
xinruiba marked this conversation as resolved.
Show resolved Hide resolved
withLongNumeralsSupport
);
const promise = shimAbortSignal(client.search(params), options?.abortSignal);

const { body: rawResponse } = (await promise) as ApiResponse<SearchResponse<any>>;
Expand All @@ -92,7 +105,7 @@
} catch (e) {
if (usage) usage.trackError();

if (dataSource && request.dataSourceId) {
if (dataSource?.dataSourceEnabled()) {
bandinib-amzn marked this conversation as resolved.
Show resolved Hide resolved
throw dataSource.createDataSourceError(e);
}
throw e;
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/data/server/search/search_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
this.initializerContext.config.legacy.globalConfig$,
this.logger,
usage,
dataSource
dataSource,
core.opensearch
bandinib-amzn marked this conversation as resolved.
Show resolved Hide resolved
)
);

Expand All @@ -141,6 +142,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
this.logger,
usage,
dataSource,
core.opensearch,
bandinib-amzn marked this conversation as resolved.
Show resolved Hide resolved
true
)
);
Expand Down
1 change: 1 addition & 0 deletions src/plugins/data_source/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
createDataSourceError: (e: any) => createDataSourceError(e),
registerCredentialProvider,
registerCustomApiSchema: (schema: any) => this.customApiSchemaRegistry.register(schema),
dataSourceEnabled: () => config.enabled,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/plugins/data_source/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export interface DataSourcePluginSetup {
createDataSourceError: (err: any) => DataSourceError;
registerCredentialProvider: (method: AuthenticationMethod) => void;
registerCustomApiSchema: (schema: any) => void;
dataSourceEnabled: () => boolean;
}

export interface DataSourcePluginStart {
Expand Down
Loading