Skip to content

Commit 1c09ca6

Browse files
authored
[Multiple Datasource] Support Amazon OpenSearch Serverless (#3957) (#4066)
* [Multiple Datasource]Support Amazon OpenSearch Serverless in SigV4 * remove experimental text in yml * Refactor create data source form for authentication Signed-off-by: Su <szhongna@amazon.com> (cherry picked from commit e737790)
1 parent e455e48 commit 1c09ca6

21 files changed

+461
-195
lines changed

config/opensearch_dashboards.yml

+1-2
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,7 @@
229229
# functionality in Visualization.
230230
# vis_builder.enabled: false
231231

232-
# Set the value of this setting to true to enable the experimental multiple data source
233-
# support feature. Use with caution.
232+
# Set the value of this setting to true to enable multiple data source feature.
234233
#data_source.enabled: false
235234
# Set the value of these settings to customize crypto materials to encryption saved credentials
236235
# in data sources.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@
139139
"@hapi/vision": "^6.1.0",
140140
"@hapi/wreck": "^17.1.0",
141141
"@opensearch-project/opensearch": "^1.1.0",
142-
"@opensearch-project/opensearch-next": "npm:@opensearch-project/opensearch@^2.1.0",
142+
"@opensearch-project/opensearch-next": "npm:@opensearch-project/opensearch@^2.2.0",
143143
"@osd/ace": "1.0.0",
144144
"@osd/analytics": "1.0.0",
145145
"@osd/apm-config-loader": "1.0.0",
@@ -172,7 +172,7 @@
172172
"dns-sync": "^0.2.1",
173173
"elastic-apm-node": "^3.7.0",
174174
"elasticsearch": "^16.7.0",
175-
"http-aws-es": "6.0.0",
175+
"http-aws-es": "npm:@zhongnansu/http-aws-es@6.0.1",
176176
"execa": "^4.0.2",
177177
"expiry-js": "0.1.7",
178178
"fast-deep-equal": "^3.1.1",

src/plugins/data_source/common/data_sources/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface SigV4Content extends SavedObjectAttributes {
2525
accessKey: string;
2626
secretKey: string;
2727
region: string;
28+
service?: SigV4ServiceName;
2829
}
2930

3031
export interface UsernamePasswordTypedContent extends SavedObjectAttributes {
@@ -37,3 +38,8 @@ export enum AuthType {
3738
UsernamePasswordType = 'username_password',
3839
SigV4 = 'sigv4',
3940
}
41+
42+
export enum SigV4ServiceName {
43+
OpenSearch = 'es',
44+
OpenSearchServerless = 'aoss',
45+
}

src/plugins/data_source/opensearch_dashboards.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"server": true,
66
"ui": true,
77
"requiredPlugins": [],
8-
"optionalPlugins": []
8+
"optionalPlugins": [],
9+
"extraPublicDirs": ["common/data_sources"]
910
}

src/plugins/data_source/server/client/configure_client.test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,30 @@ describe('configureClient', () => {
167167
expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(2);
168168
});
169169

170+
test('configure client with auth.type == sigv4, service == aoss, should successfully call new Client()', async () => {
171+
savedObjectsMock.get.mockReset().mockResolvedValueOnce({
172+
id: DATA_SOURCE_ID,
173+
type: DATA_SOURCE_SAVED_OBJECT_TYPE,
174+
attributes: {
175+
...dataSourceAttr,
176+
auth: {
177+
type: AuthType.SigV4,
178+
credentials: { ...sigV4AuthContent, service: 'aoss' },
179+
},
180+
},
181+
references: [],
182+
});
183+
184+
jest.spyOn(cryptographyMock, 'decodeAndDecrypt').mockResolvedValue({
185+
decryptedText: 'accessKey',
186+
encryptionContext: { endpoint: 'http://localhost' },
187+
});
188+
189+
await configureClient(dataSourceClientParams, clientPoolSetup, config, logger);
190+
191+
expect(ClientMock).toHaveBeenCalledTimes(1);
192+
});
193+
170194
test('configure test client for non-exist datasource should not call saved object api, nor decode any credential', async () => {
171195
const decodeAndDecryptSpy = jest.spyOn(cryptographyMock, 'decodeAndDecrypt').mockResolvedValue({
172196
decryptedText: 'password',

src/plugins/data_source/server/client/configure_client.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ const getBasicAuthClient = (
160160
};
161161

162162
const getAWSClient = (credential: SigV4Content, clientOptions: ClientOptions): Client => {
163-
const { accessKey, secretKey, region } = credential;
163+
const { accessKey, secretKey, region, service } = credential;
164164

165165
const credentialProvider = (): Promise<Credentials> => {
166166
return new Promise((resolve) => {
@@ -172,6 +172,7 @@ const getAWSClient = (credential: SigV4Content, clientOptions: ClientOptions): C
172172
...AwsSigv4Signer({
173173
region,
174174
getCredentials: credentialProvider,
175+
service,
175176
}),
176177
...clientOptions,
177178
});

src/plugins/data_source/server/client/configure_client_utils.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export const getAWSCredential = async (
9191
cryptography: CryptographyServiceSetup
9292
): Promise<SigV4Content> => {
9393
const { endpoint } = dataSource;
94-
const { accessKey, secretKey, region } = dataSource.auth.credentials! as SigV4Content;
94+
const { accessKey, secretKey, region, service } = dataSource.auth.credentials! as SigV4Content;
9595

9696
const {
9797
decryptedText: accessKeyText,
@@ -122,6 +122,7 @@ export const getAWSCredential = async (
122122
region,
123123
accessKey: accessKeyText,
124124
secretKey: secretKeyText,
125+
service,
125126
};
126127

127128
return credential;

src/plugins/data_source/server/legacy/configure_legacy_client.test.ts

+44-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { SavedObjectsClientContract } from '../../../../core/server';
77
import { loggingSystemMock, savedObjectsClientMock } from '../../../../core/server/mocks';
88
import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../common';
9-
import { AuthType, DataSourceAttributes } from '../../common/data_sources';
9+
import { AuthType, DataSourceAttributes, SigV4Content } from '../../common/data_sources';
1010
import { DataSourcePluginConfigType } from '../../config';
1111
import { cryptographyServiceSetupMock } from '../cryptography_service.mocks';
1212
import { CryptographyServiceSetup } from '../cryptography_service';
@@ -27,6 +27,7 @@ describe('configureLegacyClient', () => {
2727
let clientPoolSetup: OpenSearchClientPoolSetup;
2828
let configOptions: ConfigOptions;
2929
let dataSourceAttr: DataSourceAttributes;
30+
let sigV4AuthContent: SigV4Content;
3031

3132
let mockOpenSearchClientInstance: {
3233
close: jest.Mock;
@@ -71,6 +72,12 @@ describe('configureLegacyClient', () => {
7172
},
7273
} as DataSourceAttributes;
7374

75+
sigV4AuthContent = {
76+
region: 'us-east-1',
77+
accessKey: 'accessKey',
78+
secretKey: 'secretKey',
79+
};
80+
7481
clientPoolSetup = {
7582
getClientFromPool: jest.fn(),
7683
addClientToPool: jest.fn(),
@@ -157,6 +164,42 @@ describe('configureLegacyClient', () => {
157164
expect(mockResult).toBeDefined();
158165
});
159166

167+
test('configure client with auth.type == sigv4 and service param, should call new Client() with service param', async () => {
168+
savedObjectsMock.get.mockReset().mockResolvedValueOnce({
169+
id: DATA_SOURCE_ID,
170+
type: DATA_SOURCE_SAVED_OBJECT_TYPE,
171+
attributes: {
172+
...dataSourceAttr,
173+
auth: {
174+
type: AuthType.SigV4,
175+
credentials: { ...sigV4AuthContent, service: 'aoss' },
176+
},
177+
},
178+
references: [],
179+
});
180+
181+
parseClientOptionsMock.mockReturnValue(configOptions);
182+
183+
jest.spyOn(cryptographyMock, 'decodeAndDecrypt').mockResolvedValue({
184+
decryptedText: 'accessKey',
185+
encryptionContext: { endpoint: 'http://localhost' },
186+
});
187+
188+
await configureLegacyClient(
189+
dataSourceClientParams,
190+
callApiParams,
191+
clientPoolSetup,
192+
config,
193+
logger
194+
);
195+
196+
expect(parseClientOptionsMock).toHaveBeenCalled();
197+
expect(ClientMock).toHaveBeenCalledTimes(1);
198+
expect(ClientMock).toHaveBeenCalledWith(expect.objectContaining({ service: 'aoss' }));
199+
200+
expect(savedObjectsMock.get).toHaveBeenCalledTimes(1);
201+
});
202+
160203
test('configure client with auth.type == username_password and password contaminated', async () => {
161204
const decodeAndDecryptSpy = jest
162205
.spyOn(cryptographyMock, 'decodeAndDecrypt')

src/plugins/data_source/server/legacy/configure_legacy_client.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55

66
import { Client } from '@opensearch-project/opensearch-next';
77
import { Client as LegacyClient, ConfigOptions } from 'elasticsearch';
8-
import { Credentials } from 'aws-sdk';
8+
import { Credentials, Config } from 'aws-sdk';
99
import { get } from 'lodash';
1010
import HttpAmazonESConnector from 'http-aws-es';
11-
import { Config } from 'aws-sdk';
1211
import {
1312
Headers,
1413
LegacyAPICaller,
@@ -27,7 +26,7 @@ import { CryptographyServiceSetup } from '../cryptography_service';
2726
import { DataSourceClientParams, LegacyClientCallAPIParams } from '../types';
2827
import { OpenSearchClientPoolSetup } from '../client';
2928
import { parseClientOptions } from './client_config';
30-
import { createDataSourceError, DataSourceError } from '../lib/error';
29+
import { createDataSourceError } from '../lib/error';
3130
import {
3231
getRootClient,
3332
getAWSCredential,
@@ -195,13 +194,14 @@ const getBasicAuthClient = async (
195194
};
196195

197196
const getAWSClient = (credential: SigV4Content, clientOptions: ConfigOptions): LegacyClient => {
198-
const { accessKey, secretKey, region } = credential;
197+
const { accessKey, secretKey, region, service } = credential;
199198
const client = new LegacyClient({
200199
connectionClass: HttpAmazonESConnector,
201200
awsConfig: new Config({
202201
region,
203202
credentials: new Credentials({ accessKeyId: accessKey, secretAccessKey: secretKey }),
204203
}),
204+
service,
205205
...clientOptions,
206206
});
207207
return client;

src/plugins/data_source/server/routes/data_source_connection_validator.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@
55

66
import { OpenSearchClient } from 'opensearch-dashboards/server';
77
import { createDataSourceError } from '../lib/error';
8-
8+
import { SigV4ServiceName } from '../../common/data_sources';
99
export class DataSourceConnectionValidator {
10-
constructor(private readonly callDataCluster: OpenSearchClient) {}
10+
constructor(
11+
private readonly callDataCluster: OpenSearchClient,
12+
private readonly dataSourceAttr: any
13+
) {}
1114

1215
async validate() {
1316
try {
14-
return await this.callDataCluster.info<OpenSearchClient>();
17+
// Amazon OpenSearch Serverless does not support .info() API
18+
if (this.dataSourceAttr.auth?.credentials?.service === SigV4ServiceName.OpenSearchServerless)
19+
return await this.callDataCluster.cat.indices();
20+
return await this.callDataCluster.info();
1521
} catch (e) {
1622
throw createDataSourceError(e);
1723
}

src/plugins/data_source/server/routes/test_connection.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { schema } from '@osd/config-schema';
77
import { IRouter, OpenSearchClient } from 'opensearch-dashboards/server';
8-
import { AuthType, DataSourceAttributes } from '../../common/data_sources';
8+
import { AuthType, DataSourceAttributes, SigV4ServiceName } from '../../common/data_sources';
99
import { DataSourceConnectionValidator } from './data_source_connection_validator';
1010
import { DataSourceServiceSetup } from '../data_source_service';
1111
import { CryptographyServiceSetup } from '../cryptography_service';
@@ -40,6 +40,10 @@ export const registerTestConnectionRoute = (
4040
region: schema.string(),
4141
accessKey: schema.string(),
4242
secretKey: schema.string(),
43+
service: schema.oneOf([
44+
schema.literal(SigV4ServiceName.OpenSearch),
45+
schema.literal(SigV4ServiceName.OpenSearchServerless),
46+
]),
4347
}),
4448
])
4549
),
@@ -61,9 +65,13 @@ export const registerTestConnectionRoute = (
6165
testClientDataSourceAttr: dataSourceAttr as DataSourceAttributes,
6266
}
6367
);
64-
const dsValidator = new DataSourceConnectionValidator(dataSourceClient);
6568

66-
await dsValidator.validate();
69+
const dataSourceValidator = new DataSourceConnectionValidator(
70+
dataSourceClient,
71+
dataSourceAttr
72+
);
73+
74+
await dataSourceValidator.validate();
6775

6876
return response.ok({
6977
body: {

src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ export class DataSourceSavedObjectsClientWrapper {
301301
);
302302
}
303303

304-
const { accessKey, secretKey, region } = credentials as SigV4Content;
304+
const { accessKey, secretKey, region, service } = credentials as SigV4Content;
305305

306306
if (!accessKey) {
307307
throw SavedObjectsErrorHelpers.createBadRequestError(
@@ -320,6 +320,12 @@ export class DataSourceSavedObjectsClientWrapper {
320320
'"auth.credentials.region" attribute is required'
321321
);
322322
}
323+
324+
if (!service) {
325+
throw SavedObjectsErrorHelpers.createBadRequestError(
326+
'"auth.credentials.service" attribute is required'
327+
);
328+
}
323329
break;
324330
default:
325331
throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid auth type: '${type}'`);
@@ -457,7 +463,7 @@ export class DataSourceSavedObjectsClientWrapper {
457463

458464
private async encryptSigV4Credential<T = unknown>(auth: T, encryptionContext: EncryptionContext) {
459465
const {
460-
credentials: { accessKey, secretKey, region },
466+
credentials: { accessKey, secretKey, region, service },
461467
} = auth;
462468

463469
return {
@@ -466,6 +472,7 @@ export class DataSourceSavedObjectsClientWrapper {
466472
region,
467473
accessKey: await this.cryptography.encryptAndEncode(accessKey, encryptionContext),
468474
secretKey: await this.cryptography.encryptAndEncode(secretKey, encryptionContext),
475+
service,
469476
},
470477
};
471478
}

src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ describe('Datasource Management: Create Datasource form', () => {
2727
let component: ReactWrapper<any, Readonly<{}>, React.Component<{}, {}, any>>;
2828
const mockSubmitHandler = jest.fn();
2929
const mockTestConnectionHandler = jest.fn();
30+
const mockCancelHandler = jest.fn();
3031

3132
const getFields = (comp: ReactWrapper<any, Readonly<{}>, React.Component<{}, {}, any>>) => {
3233
return {
@@ -65,6 +66,7 @@ describe('Datasource Management: Create Datasource form', () => {
6566
<CreateDataSourceForm
6667
handleTestConnection={mockTestConnectionHandler}
6768
handleSubmit={mockSubmitHandler}
69+
handleCancel={mockCancelHandler}
6870
existingDatasourceNamesList={['dup20']}
6971
/>
7072
),

0 commit comments

Comments
 (0)