From 5fdba1d45ed1f3743d3cd88818797ef21e802165 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Fri, 9 Feb 2024 02:26:03 +0000 Subject: [PATCH 01/47] Add application configuration service Signed-off-by: Tianle Huang --- config/opensearch_dashboards.yml | 4 + src/core/server/mocks.ts | 1 + .../server/opensearch_dashboards_config.ts | 1 + src/core/server/plugins/types.ts | 7 +- src/legacy/server/config/schema.js | 1 + src/plugins/application_config/.eslintrc.js | 7 ++ src/plugins/application_config/.i18nrc.json | 7 ++ src/plugins/application_config/README.md | 11 +++ .../application_config/common/index.ts | 2 + .../opensearch_dashboards.json | 9 ++ .../application_config/server/index.ts | 11 +++ .../server/opensearch_config_client.ts | 40 +++++++++ .../application_config/server/plugin.ts | 82 +++++++++++++++++++ .../application_config/server/routes/index.ts | 50 +++++++++++ .../application_config/server/types.ts | 9 ++ 15 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 src/plugins/application_config/.eslintrc.js create mode 100644 src/plugins/application_config/.i18nrc.json create mode 100755 src/plugins/application_config/README.md create mode 100644 src/plugins/application_config/common/index.ts create mode 100644 src/plugins/application_config/opensearch_dashboards.json create mode 100644 src/plugins/application_config/server/index.ts create mode 100644 src/plugins/application_config/server/opensearch_config_client.ts create mode 100644 src/plugins/application_config/server/plugin.ts create mode 100644 src/plugins/application_config/server/routes/index.ts create mode 100644 src/plugins/application_config/server/types.ts diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 4f86a8729a3e..780f5351dfb2 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -29,6 +29,10 @@ # dashboards. OpenSearch Dashboards creates a new index if the index doesn't already exist. #opensearchDashboards.index: ".opensearch_dashboards" +# OpenSearch Dashboards uses an index in OpenSearch to store dynamic configurations. +# This shall be a different index from opensearchDashboards.index. +# opensearchDashboards.config_index: ".opensearch_dashboards_config" + # The default application to load. #opensearchDashboards.defaultAppId: "home" diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 2a6114013b22..5120598689df 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -77,6 +77,7 @@ export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { opensearchDashboards: { index: '.opensearch_dashboards_tests', + config_index: '.opensearch_dashboards_config_tests', autocompleteTerminateAfter: duration(100000), autocompleteTimeout: duration(1000), }, diff --git a/src/core/server/opensearch_dashboards_config.ts b/src/core/server/opensearch_dashboards_config.ts index 107d02ea3377..715454c8cdee 100644 --- a/src/core/server/opensearch_dashboards_config.ts +++ b/src/core/server/opensearch_dashboards_config.ts @@ -48,6 +48,7 @@ export const config = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), index: schema.string({ defaultValue: '.kibana' }), + config_index: schema.string({ defaultValue: '.opensearch_dashboards_config' }), autocompleteTerminateAfter: schema.duration({ defaultValue: 100000 }), autocompleteTimeout: schema.duration({ defaultValue: 1000 }), branding: schema.object({ diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index b7667b5bd2d2..aad5c742c5bf 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -287,7 +287,12 @@ export interface Plugin< export const SharedGlobalConfigKeys = { // We can add more if really needed - opensearchDashboards: ['index', 'autocompleteTerminateAfter', 'autocompleteTimeout'] as const, + opensearchDashboards: [ + 'index', + 'config_index', + 'autocompleteTerminateAfter', + 'autocompleteTimeout', + ] as const, opensearch: ['shardTimeout', 'requestTimeout', 'pingTimeout'] as const, path: ['data'] as const, savedObjects: ['maxImportPayloadBytes'] as const, diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 5cf8e9ac1901..0084e41a72af 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -227,6 +227,7 @@ export default () => opensearchDashboards: Joi.object({ enabled: Joi.boolean().default(true), index: Joi.string().default('.kibana'), + config_index: Joi.string().default('.opensearch_dashboards_config'), autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000), // TODO Also allow units here like in opensearch config once this is moved to the new platform autocompleteTimeout: Joi.number().integer().min(1).default(1000), diff --git a/src/plugins/application_config/.eslintrc.js b/src/plugins/application_config/.eslintrc.js new file mode 100644 index 000000000000..b16a8b23a08e --- /dev/null +++ b/src/plugins/application_config/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], + rules: { + '@osd/eslint/require-license-header': 'off', + }, +}; diff --git a/src/plugins/application_config/.i18nrc.json b/src/plugins/application_config/.i18nrc.json new file mode 100644 index 000000000000..06267a67f042 --- /dev/null +++ b/src/plugins/application_config/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "applicationConfig", + "paths": { + "applicationConfig": "." + }, + "translations": ["translations/ja-JP.json"] +} diff --git a/src/plugins/application_config/README.md b/src/plugins/application_config/README.md new file mode 100755 index 000000000000..cd2ed9a5abf3 --- /dev/null +++ b/src/plugins/application_config/README.md @@ -0,0 +1,11 @@ +# applicationConfig + +A OpenSearch Dashboards plugin + +--- + +## Development + +See the [OpenSearch Dashboards contributing +guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/CONTRIBUTING.md) for instructions +setting up your development environment. diff --git a/src/plugins/application_config/common/index.ts b/src/plugins/application_config/common/index.ts new file mode 100644 index 000000000000..288098f1b2ca --- /dev/null +++ b/src/plugins/application_config/common/index.ts @@ -0,0 +1,2 @@ +export const PLUGIN_ID = 'applicationConfig'; +export const PLUGIN_NAME = 'applicationConfig'; diff --git a/src/plugins/application_config/opensearch_dashboards.json b/src/plugins/application_config/opensearch_dashboards.json new file mode 100644 index 000000000000..6b1494902628 --- /dev/null +++ b/src/plugins/application_config/opensearch_dashboards.json @@ -0,0 +1,9 @@ +{ + "id": "applicationConfig", + "version": "1.0.0", + "opensearchDashboardsVersion": "opensearchDashboards", + "server": true, + "ui": false, + "requiredPlugins": ["navigation"], + "optionalPlugins": [] +} diff --git a/src/plugins/application_config/server/index.ts b/src/plugins/application_config/server/index.ts new file mode 100644 index 000000000000..cda1cad5d2e7 --- /dev/null +++ b/src/plugins/application_config/server/index.ts @@ -0,0 +1,11 @@ +import { PluginInitializerContext } from '../../../core/server'; +import { ApplicationConfigPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, OpenSearch Dashboards Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new ApplicationConfigPlugin(initializerContext); +} + +export { ApplicationConfigPluginSetup, ApplicationConfigPluginStart } from './types'; diff --git a/src/plugins/application_config/server/opensearch_config_client.ts b/src/plugins/application_config/server/opensearch_config_client.ts new file mode 100644 index 000000000000..956dd01ccca5 --- /dev/null +++ b/src/plugins/application_config/server/opensearch_config_client.ts @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IScopedClusterClient, Logger } from '../../../../src/core/server'; + +import { ConfigurationClient } from './types'; + +export class OpenSearchConfigurationClient implements ConfigurationClient { + private client: IScopedClusterClient; + private configurationIndexName: string; + private readonly logger: Logger; + + constructor( + inputOpenSearchClient: IScopedClusterClient, + inputConfigurationIndexName: string, + inputLogger: Logger + ) { + this.client = inputOpenSearchClient; + this.configurationIndexName = inputConfigurationIndexName; + this.logger = inputLogger; + } + + async existsConfig(): Promise { + try { + const exists = await this.client.asInternalUser.indices.exists({ + index: this.configurationIndexName, + }); + + return exists.body; + } catch (e) { + const errorMessage = `Failed to call existsConfig due to error ${e}`; + + this.logger.error(errorMessage); + + throw e; + } + } +} diff --git a/src/plugins/application_config/server/plugin.ts b/src/plugins/application_config/server/plugin.ts new file mode 100644 index 000000000000..08363278ccf3 --- /dev/null +++ b/src/plugins/application_config/server/plugin.ts @@ -0,0 +1,82 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; + +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, + IScopedClusterClient, + SharedGlobalConfig, +} from '../../../core/server'; + +import { + ApplicationConfigPluginSetup, + ApplicationConfigPluginStart, + ConfigurationClient, +} from './types'; +import { defineRoutes } from './routes'; +import { OpenSearchConfigurationClient } from './opensearch_config_client'; + +export class ApplicationConfigPlugin + implements Plugin { + private readonly logger: Logger; + private readonly config$: Observable; + + private configurationClient: ConfigurationClient; + private configurationIndexName: string; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + this.config$ = initializerContext.config.legacy.globalConfig$; + this.configurationIndexName = ''; + } + + private setConfigurationClient(inputConfigurationClient: ConfigurationClient) { + this.configurationClient = inputConfigurationClient; + } + + private getConfigurationClient(inputOpenSearchClient: IScopedClusterClient) { + if (this.configurationClient) { + return this.configurationClient; + } + + const openSearchConfigurationClient = new OpenSearchConfigurationClient( + inputOpenSearchClient, + this.configurationIndexName, + this.logger + ); + + this.setConfigurationClient(openSearchConfigurationClient); + return this.configurationClient; + } + + public async setup(core: CoreSetup) { + this.logger.debug('applicationConfig: Setup'); + const router = core.http.createRouter(); + + const config = await this.config$.pipe(first()).toPromise(); + + this.configurationIndexName = config.opensearchDashboards.config_index; + + // Register server side APIs + defineRoutes(router, this.getConfigurationClient.bind(this), this.logger); + + return { + setConfigurationClient: this.setConfigurationClient.bind(this), + }; + } + + public start(core: CoreStart) { + this.logger.debug('applicationConfig: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/application_config/server/routes/index.ts b/src/plugins/application_config/server/routes/index.ts new file mode 100644 index 000000000000..f1612ea96dcd --- /dev/null +++ b/src/plugins/application_config/server/routes/index.ts @@ -0,0 +1,50 @@ +import { + IRouter, + IScopedClusterClient, + Logger, + OpenSearchDashboardsResponseFactory, +} from '../../../../core/server'; +import { ConfigurationClient } from '../types'; + +export function defineRoutes( + router: IRouter, + getConfigurationClient: (inputOpenSearchClient: IScopedClusterClient) => ConfigurationClient, + logger: Logger +) { + router.get( + { + path: '/api/config/exists', + validate: false, + }, + async (context, request, response) => { + const client = getConfigurationClient(context.core.opensearch.client); + + return await handleExistsConfig(client, response, logger); + } + ); +} + +export async function handleExistsConfig( + client: ConfigurationClient, + response: OpenSearchDashboardsResponseFactory, + logger: Logger +) { + try { + const result = await client.existsConfig(); + return response.ok({ + body: { + exists: result, + }, + }); + } catch (e) { + logger.error(e); + return errorResponse(response, e); + } +} + +export function errorResponse(response: OpenSearchDashboardsResponseFactory, error: any) { + return response.custom({ + statusCode: error?.statusCode || 500, + body: error, + }); +} diff --git a/src/plugins/application_config/server/types.ts b/src/plugins/application_config/server/types.ts new file mode 100644 index 000000000000..61fe279e6aea --- /dev/null +++ b/src/plugins/application_config/server/types.ts @@ -0,0 +1,9 @@ +export interface ApplicationConfigPluginSetup { + setConfigurationClient: (inputConfigurationClient: ConfigurationClient) => void; +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ApplicationConfigPluginStart {} + +export interface ConfigurationClient { + existsConfig(): Promise; +} From 91b2d5667fca232c6988b2d0c1192b9da670ce9a Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Thu, 15 Feb 2024 02:17:57 +0000 Subject: [PATCH 02/47] update API path name Signed-off-by: Tianle Huang --- src/plugins/application_config/server/routes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/application_config/server/routes/index.ts b/src/plugins/application_config/server/routes/index.ts index f1612ea96dcd..99d0d64f3f58 100644 --- a/src/plugins/application_config/server/routes/index.ts +++ b/src/plugins/application_config/server/routes/index.ts @@ -13,7 +13,7 @@ export function defineRoutes( ) { router.get( { - path: '/api/config/exists', + path: '/api/appconfig/exists', validate: false, }, async (context, request, response) => { From 7c9b6acee59e2011b1ec817ed6ab63245991c91b Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Thu, 15 Feb 2024 06:44:02 +0000 Subject: [PATCH 03/47] implement two APIs/interfaces Signed-off-by: Tianle Huang --- .../server/opensearch_config_client.ts | 26 +++++++++++-- .../application_config/server/plugin.ts | 5 +-- .../application_config/server/routes/index.ts | 39 ++++++++++++++++--- .../application_config/server/types.ts | 6 ++- 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/plugins/application_config/server/opensearch_config_client.ts b/src/plugins/application_config/server/opensearch_config_client.ts index 956dd01ccca5..815f665ac2f2 100644 --- a/src/plugins/application_config/server/opensearch_config_client.ts +++ b/src/plugins/application_config/server/opensearch_config_client.ts @@ -22,19 +22,37 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { this.logger = inputLogger; } - async existsConfig(): Promise { + async createConfig(): Promise { try { - const exists = await this.client.asInternalUser.indices.exists({ + await this.client.asCurrentUser.indices.create({ index: this.configurationIndexName, }); - return exists.body; + return this.configurationIndexName; } catch (e) { - const errorMessage = `Failed to call existsConfig due to error ${e}`; + const errorMessage = `Failed to call createConfig due to error ${e}`; this.logger.error(errorMessage); throw e; } } + + async getConfig(): Promise { + try { + const data = await this.client.asInternalUser.search({ + index: this.configurationIndexName, + }); + + return JSON.stringify(data.body.hits.hits); + } catch (e) { + const errorMessage = `Failed to call getConfig due to error ${e}`; + + this.logger.error(errorMessage); + + throw e; + } + } + + } diff --git a/src/plugins/application_config/server/plugin.ts b/src/plugins/application_config/server/plugin.ts index 08363278ccf3..ed6cde352801 100644 --- a/src/plugins/application_config/server/plugin.ts +++ b/src/plugins/application_config/server/plugin.ts @@ -53,8 +53,7 @@ export class ApplicationConfigPlugin this.logger ); - this.setConfigurationClient(openSearchConfigurationClient); - return this.configurationClient; + return openSearchConfigurationClient; } public async setup(core: CoreSetup) { @@ -78,5 +77,5 @@ export class ApplicationConfigPlugin return {}; } - public stop() {} + public stop() { } } diff --git a/src/plugins/application_config/server/routes/index.ts b/src/plugins/application_config/server/routes/index.ts index 99d0d64f3f58..7f535ec95800 100644 --- a/src/plugins/application_config/server/routes/index.ts +++ b/src/plugins/application_config/server/routes/index.ts @@ -13,27 +13,56 @@ export function defineRoutes( ) { router.get( { - path: '/api/appconfig/exists', + path: '/api/appconfig', validate: false, }, async (context, request, response) => { const client = getConfigurationClient(context.core.opensearch.client); - return await handleExistsConfig(client, response, logger); + return await handleGetConfig(client, response, logger); } ); + router.post( + { + path: '/api/appconfig', + validate: false, + }, + async (context, request, response) => { + const client = getConfigurationClient(context.core.opensearch.client); + + return await handleCreateConfig(client, response, logger); + } + ); +} + +export async function handleCreateConfig( + client: ConfigurationClient, + response: OpenSearchDashboardsResponseFactory, + logger: Logger +) { + try { + const result = await client.createConfig(); + return response.ok({ + body: { + createdIndexName: result, + }, + }); + } catch (e) { + logger.error(e); + return errorResponse(response, e); + } } -export async function handleExistsConfig( +export async function handleGetConfig( client: ConfigurationClient, response: OpenSearchDashboardsResponseFactory, logger: Logger ) { try { - const result = await client.existsConfig(); + const result = await client.getConfig(); return response.ok({ body: { - exists: result, + config: result, }, }); } catch (e) { diff --git a/src/plugins/application_config/server/types.ts b/src/plugins/application_config/server/types.ts index 61fe279e6aea..615da87ee205 100644 --- a/src/plugins/application_config/server/types.ts +++ b/src/plugins/application_config/server/types.ts @@ -2,8 +2,10 @@ export interface ApplicationConfigPluginSetup { setConfigurationClient: (inputConfigurationClient: ConfigurationClient) => void; } // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ApplicationConfigPluginStart {} +export interface ApplicationConfigPluginStart { } export interface ConfigurationClient { - existsConfig(): Promise; + getConfig(): Promise; + + createConfig(): void; } From e98600fb9a8e06e75daf05d1ebd9033956c11547 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Thu, 15 Feb 2024 22:13:22 +0000 Subject: [PATCH 04/47] expose get function for other plugins to use Signed-off-by: Tianle Huang --- .../server/opensearch_config_client.ts | 2 -- src/plugins/application_config/server/plugin.ts | 10 ++++++---- src/plugins/application_config/server/types.ts | 7 +++++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/plugins/application_config/server/opensearch_config_client.ts b/src/plugins/application_config/server/opensearch_config_client.ts index 815f665ac2f2..479f1244d588 100644 --- a/src/plugins/application_config/server/opensearch_config_client.ts +++ b/src/plugins/application_config/server/opensearch_config_client.ts @@ -53,6 +53,4 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { throw e; } } - - } diff --git a/src/plugins/application_config/server/plugin.ts b/src/plugins/application_config/server/plugin.ts index ed6cde352801..6d5618061e85 100644 --- a/src/plugins/application_config/server/plugin.ts +++ b/src/plugins/application_config/server/plugin.ts @@ -38,11 +38,12 @@ export class ApplicationConfigPlugin this.configurationIndexName = ''; } - private setConfigurationClient(inputConfigurationClient: ConfigurationClient) { + private registerConfigurationClient(inputConfigurationClient: ConfigurationClient) { + this.logger.info('Register a configuration client.'); this.configurationClient = inputConfigurationClient; } - private getConfigurationClient(inputOpenSearchClient: IScopedClusterClient) { + private getConfigurationClient(inputOpenSearchClient: IScopedClusterClient): ConfigurationClient { if (this.configurationClient) { return this.configurationClient; } @@ -68,7 +69,8 @@ export class ApplicationConfigPlugin defineRoutes(router, this.getConfigurationClient.bind(this), this.logger); return { - setConfigurationClient: this.setConfigurationClient.bind(this), + getConfigurationClient: this.getConfigurationClient.bind(this), + registerConfigurationClient: this.registerConfigurationClient.bind(this), }; } @@ -77,5 +79,5 @@ export class ApplicationConfigPlugin return {}; } - public stop() { } + public stop() {} } diff --git a/src/plugins/application_config/server/types.ts b/src/plugins/application_config/server/types.ts index 615da87ee205..d1dfcf84d79c 100644 --- a/src/plugins/application_config/server/types.ts +++ b/src/plugins/application_config/server/types.ts @@ -1,8 +1,11 @@ +import { IScopedClusterClient } from 'src/core/server'; + export interface ApplicationConfigPluginSetup { - setConfigurationClient: (inputConfigurationClient: ConfigurationClient) => void; + getConfigurationClient: (inputOpenSearchClient: IScopedClusterClient) => ConfigurationClient; + registerConfigurationClient: (inputConfigurationClient: ConfigurationClient) => void; } // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ApplicationConfigPluginStart { } +export interface ApplicationConfigPluginStart {} export interface ConfigurationClient { getConfig(): Promise; From 37e740b0d273ee712d9c8001fa3fb9bc6bc5606d Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Fri, 16 Feb 2024 03:28:28 +0000 Subject: [PATCH 05/47] update interfaces Signed-off-by: Tianle Huang --- .../server/opensearch_config_client.ts | 20 ++++++++----------- .../application_config/server/types.ts | 6 +++++- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/plugins/application_config/server/opensearch_config_client.ts b/src/plugins/application_config/server/opensearch_config_client.ts index 479f1244d588..900329e6308a 100644 --- a/src/plugins/application_config/server/opensearch_config_client.ts +++ b/src/plugins/application_config/server/opensearch_config_client.ts @@ -22,20 +22,16 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { this.logger = inputLogger; } - async createConfig(): Promise { - try { - await this.client.asCurrentUser.indices.create({ - index: this.configurationIndexName, - }); - - return this.configurationIndexName; - } catch (e) { - const errorMessage = `Failed to call createConfig due to error ${e}`; + async getFeildConfig(documentName: any, fieldName: any) { + throw new Error('Method not implemented.'); + } - this.logger.error(errorMessage); + updateFeildConfig(documentName: any, fieldName: any, newValue: any) { + throw new Error('Method not implemented.'); + } - throw e; - } + deleteFeildConfig(documentName: any, fieldName: any) { + throw new Error('Method not implemented.'); } async getConfig(): Promise { diff --git a/src/plugins/application_config/server/types.ts b/src/plugins/application_config/server/types.ts index d1dfcf84d79c..a7bcf1eb6374 100644 --- a/src/plugins/application_config/server/types.ts +++ b/src/plugins/application_config/server/types.ts @@ -10,5 +10,9 @@ export interface ApplicationConfigPluginStart {} export interface ConfigurationClient { getConfig(): Promise; - createConfig(): void; + getFeildConfig(documentName, fieldName); + + updateFeildConfig(documentName, fieldName, newValue); + + deleteFeildConfig(documentName, fieldName); } From 237dd22465e7e94bcdc252cbf329b483344f85f1 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Sat, 17 Feb 2024 02:14:30 +0000 Subject: [PATCH 06/47] implement the APIs and interfaces Signed-off-by: Tianle Huang --- .../server/opensearch_config_client.ts | 81 ++++++++++++++-- .../application_config/server/routes/index.ts | 92 +++++++++++++++++-- .../application_config/server/types.ts | 8 +- 3 files changed, 162 insertions(+), 19 deletions(-) diff --git a/src/plugins/application_config/server/opensearch_config_client.ts b/src/plugins/application_config/server/opensearch_config_client.ts index 900329e6308a..a896b809ce89 100644 --- a/src/plugins/application_config/server/opensearch_config_client.ts +++ b/src/plugins/application_config/server/opensearch_config_client.ts @@ -22,25 +22,79 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { this.logger = inputLogger; } - async getFeildConfig(documentName: any, fieldName: any) { - throw new Error('Method not implemented.'); + async getEntityConfig(entity: string) { + try { + const data = await this.client.asInternalUser.get({ + index: this.configurationIndexName, + id: entity, + }); + + this.logger.info(`*** result is ${JSON.stringify(data)}`); + + return data?.body?._source?.value || ''; + } catch (e) { + const errorMessage = `Failed to get entity ${entity} due to error ${e}`; + + this.logger.error(errorMessage); + + throw e; + } } - updateFeildConfig(documentName: any, fieldName: any, newValue: any) { - throw new Error('Method not implemented.'); + async updateEntityConfig(entity: string, newValue: string) { + try { + await this.client.asCurrentUser.index({ + index: this.configurationIndexName, + id: entity, + body: { + value: newValue, + }, + }); + + return newValue; + } catch (e) { + const errorMessage = `Failed to update entity ${entity} with newValue ${newValue} due to error ${e}`; + + this.logger.error(errorMessage); + + throw e; + } } - deleteFeildConfig(documentName: any, fieldName: any) { - throw new Error('Method not implemented.'); + async deleteEntityConfig(entity: string) { + try { + await this.client.asCurrentUser.delete({ + index: this.configurationIndexName, + id: entity, + }); + + return entity; + } catch (e) { + if (e?.body?.error?.type === 'index_not_found_exception') { + this.logger.info('Attemp to delete a not found index.'); + return entity; + } + + if (e?.body?.result === 'not_found') { + this.logger.info('Attemp to delete a not found document.'); + return entity; + } + + const errorMessage = `Failed to delete entity ${entity} due to error ${e}`; + + this.logger.error(errorMessage); + + throw e; + } } - async getConfig(): Promise { + async getConfig(): Promise { try { const data = await this.client.asInternalUser.search({ index: this.configurationIndexName, }); - return JSON.stringify(data.body.hits.hits); + return this.transformIndexSearchResponse(data.body.hits.hits); } catch (e) { const errorMessage = `Failed to call getConfig due to error ${e}`; @@ -49,4 +103,15 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { throw e; } } + + transformIndexSearchResponse(hits) { + const configurations = {}; + + for (let i = 0; i < hits.length; i++) { + const doc = hits[i]; + configurations[doc._id] = doc?._source?.value; + } + + return configurations; + } } diff --git a/src/plugins/application_config/server/routes/index.ts b/src/plugins/application_config/server/routes/index.ts index 7f535ec95800..ada0e9789a51 100644 --- a/src/plugins/application_config/server/routes/index.ts +++ b/src/plugins/application_config/server/routes/index.ts @@ -1,7 +1,9 @@ +import { schema } from '@osd/config-schema'; import { IRouter, IScopedClusterClient, Logger, + OpenSearchDashboardsRequest, OpenSearchDashboardsResponseFactory, } from '../../../../core/server'; import { ConfigurationClient } from '../types'; @@ -22,29 +24,105 @@ export function defineRoutes( return await handleGetConfig(client, response, logger); } ); + router.get( + { + path: '/api/appconfig/{entity}', + validate: { + params: schema.object({ + entity: schema.string(), + }), + }, + }, + async (context, request, response) => { + const client = getConfigurationClient(context.core.opensearch.client); + + return await handleGetEntityConfig(client, request, response, logger); + } + ); router.post( { - path: '/api/appconfig', - validate: false, + path: '/api/appconfig/{entity}', + validate: { + params: schema.object({ + entity: schema.string(), + }), + body: schema.object({ + newValue: schema.string(), + }), + }, }, async (context, request, response) => { const client = getConfigurationClient(context.core.opensearch.client); - return await handleCreateConfig(client, response, logger); + return await handleUpdateEntityConfig(client, request, response, logger); } ); + router.delete( + { + path: '/api/appconfig/{entity}', + validate: { + params: schema.object({ + entity: schema.string(), + }), + }, + }, + async (context, request, response) => { + const client = getConfigurationClient(context.core.opensearch.client); + + return await handleDeleteEntityConfig(client, request, response, logger); + } + ); +} + +export async function handleGetEntityConfig( + client: ConfigurationClient, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory, + logger: Logger +) { + try { + const result = await client.getEntityConfig(request.params.entity); + return response.ok({ + body: { + value: result, + }, + }); + } catch (e) { + logger.error(e); + return errorResponse(response, e); + } +} + +export async function handleUpdateEntityConfig( + client: ConfigurationClient, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory, + logger: Logger +) { + try { + const result = await client.updateEntityConfig(request.params.entity, request.body.newValue); + return response.ok({ + body: { + newValue: result, + }, + }); + } catch (e) { + logger.error(e); + return errorResponse(response, e); + } } -export async function handleCreateConfig( +export async function handleDeleteEntityConfig( client: ConfigurationClient, + request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory, logger: Logger ) { try { - const result = await client.createConfig(); + const result = await client.deleteEntityConfig(request.params.entity); return response.ok({ body: { - createdIndexName: result, + deletedEntity: result, }, }); } catch (e) { @@ -62,7 +140,7 @@ export async function handleGetConfig( const result = await client.getConfig(); return response.ok({ body: { - config: result, + value: result, }, }); } catch (e) { diff --git a/src/plugins/application_config/server/types.ts b/src/plugins/application_config/server/types.ts index a7bcf1eb6374..9f50cbb019f5 100644 --- a/src/plugins/application_config/server/types.ts +++ b/src/plugins/application_config/server/types.ts @@ -8,11 +8,11 @@ export interface ApplicationConfigPluginSetup { export interface ApplicationConfigPluginStart {} export interface ConfigurationClient { - getConfig(): Promise; + getConfig(): Promise>; - getFeildConfig(documentName, fieldName); + getEntityConfig(entity: string): Promise; - updateFeildConfig(documentName, fieldName, newValue); + updateEntityConfig(entity: string, newValue: string): Promise; - deleteFeildConfig(documentName, fieldName); + deleteEntityConfig(entity: string): Promise; } From dffb866dbb78f983ab24079b85d2eedcf62a1dbe Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Sat, 17 Feb 2024 02:40:22 +0000 Subject: [PATCH 07/47] add license and jsdoc Signed-off-by: Tianle Huang --- .../application_config/common/index.ts | 5 +++ .../application_config/server/index.ts | 5 +++ .../application_config/server/routes/index.ts | 5 +++ .../application_config/server/types.ts | 36 ++++++++++++++++++- 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/plugins/application_config/common/index.ts b/src/plugins/application_config/common/index.ts index 288098f1b2ca..beaa29371234 100644 --- a/src/plugins/application_config/common/index.ts +++ b/src/plugins/application_config/common/index.ts @@ -1,2 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + export const PLUGIN_ID = 'applicationConfig'; export const PLUGIN_NAME = 'applicationConfig'; diff --git a/src/plugins/application_config/server/index.ts b/src/plugins/application_config/server/index.ts index cda1cad5d2e7..2f46a43d9add 100644 --- a/src/plugins/application_config/server/index.ts +++ b/src/plugins/application_config/server/index.ts @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import { PluginInitializerContext } from '../../../core/server'; import { ApplicationConfigPlugin } from './plugin'; diff --git a/src/plugins/application_config/server/routes/index.ts b/src/plugins/application_config/server/routes/index.ts index ada0e9789a51..1fe312979245 100644 --- a/src/plugins/application_config/server/routes/index.ts +++ b/src/plugins/application_config/server/routes/index.ts @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import { schema } from '@osd/config-schema'; import { IRouter, diff --git a/src/plugins/application_config/server/types.ts b/src/plugins/application_config/server/types.ts index 9f50cbb019f5..49fc11d99c53 100644 --- a/src/plugins/application_config/server/types.ts +++ b/src/plugins/application_config/server/types.ts @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import { IScopedClusterClient } from 'src/core/server'; export interface ApplicationConfigPluginSetup { @@ -7,12 +12,41 @@ export interface ApplicationConfigPluginSetup { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ApplicationConfigPluginStart {} +/** + * The interface defines the operations against the application configurations at both entity level and whole level. + * + */ export interface ConfigurationClient { + /** + * Get all the configurations. + * + * @param {array} array of connections + * @returns {ConnectionPool} + */ getConfig(): Promise>; - getEntityConfig(entity: string): Promise; + /** + * Get the value for the input entity. + * + * @param {entity} name of the entity + * @returns {string} value of the entity + */ + getEntityConfig(entity: string): Promise; + /** + * Update the input entity with a new value. + * + * @param {entity} name of the entity + * @param {newValue} new configuration value of the entity + * @returns {string} updated configuration value of the entity + */ updateEntityConfig(entity: string, newValue: string): Promise; + /** + * Delete the input entity from configurations. + * + * @param {entity} name of the entity + * @returns {string} name of the deleted entity + */ deleteEntityConfig(entity: string): Promise; } From d352d1882da9bbaa803ecb2a61ac7d7075297ae2 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Tue, 20 Feb 2024 01:37:31 +0000 Subject: [PATCH 08/47] update docs Signed-off-by: Tianle Huang --- src/plugins/application_config/README.md | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/plugins/application_config/README.md b/src/plugins/application_config/README.md index cd2ed9a5abf3..4885e9d0127e 100755 --- a/src/plugins/application_config/README.md +++ b/src/plugins/application_config/README.md @@ -2,8 +2,82 @@ A OpenSearch Dashboards plugin +This plugin introduces the support of dynamic application configurations as opposed to the existing static configuration in OSD YAML file `opensearch_dashboards.yml`. It stores the configuration in an index whose default name is `.opensearch_dashboards_config` and could be customized through the key `opensearchDashboards.config_index` in OSD YAML file. Initially the new index does not exist. Only OSD users who need dynamic configurations will create it. + +It also provides an interface `ConfigurationClient` for future extensions of external configuration clients. A default implementation based on OpenSearch as database is used. + --- +## Configuration + +OSD users who want to set up application configurations can perform such through CURL the OSD APIs. + +(Note that the commands following could be first obtained from a copy as curl option from the network tab of a browser development tool and then replaced with the API names) + +Below is the CURL command to view all configurations. + +``` +curl '{osd endpoint}/api/appconfig' -X GET +``` + +Below is the CURL command to view the configuration of an entity. + +``` +curl '{osd endpoint}/api/appconfig/{entity}' -X GET + +``` + +Below is the CURL command to update the configuration of an entity. + +``` +curl '{osd endpoint}/api/appconfig/{entity}' -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -H 'osd-xsrf: osd-fetch' -H 'Sec-Fetch-Dest: empty' --data-raw '{"newValue":"{new value}"}' +``` + +Below is the CURL command to delete the configuration of an entity. + +``` +curl '{osd endpoint}/api/appconfig/{entity}' -X DELETE -H 'osd-xsrf: osd-fetch' -H 'Sec-Fetch-Dest: empty' + +``` + + +## External Configuration Clients + +While a default OpenSearch based client is implemented, OSD users can use external configuration clients through an OSD plugin (outside OSD). + +Let's call this plugin `MyConfigurationClientPlugin`. + +First, this plugin will need to implement a class `MyConfigurationClient` based on interface `ConfigurationClient` defined in the `types.ts` under directory `src/plugins/application_config/server/types.ts`. + +Second, this plugin needs to declare `applicationConfig` as its dependency by adding it to `requiredPlugins` in its own `opensearch_dashboards.json`. + +Third, the plugin will define a new type called `AppPluginSetupDependencies` as follows in its own `types.ts`. + +``` +export interface AppPluginSetupDependencies { + applicationConfig: ApplicationConfigPluginSetup; +} + +``` + +Then the plugin will import the new type `AppPluginSetupDependencies` and add to its own setup input. Below is the skeleton of the class `MyConfigurationClientPlugin`. + +``` +// MyConfigurationClientPlugin + public setup(core: CoreSetup, { applicationConfig }: AppPluginSetupDependencies) { + + ... + // The function createClient provides an instance of ConfigurationClient which + // could have a underlying DynamoDB or Postgres implementation. + const myConfigurationClient: ConfigurationClient = this.createClient(); + + applicationConfig.registerConfigurationClient(myConfigurationClient); + ... + return {}; + } + +``` + ## Development See the [OpenSearch Dashboards contributing From bf7342760688bfb0033abd24ec6a7e95642cd142 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Tue, 20 Feb 2024 02:19:32 +0000 Subject: [PATCH 09/47] add more docs Signed-off-by: Tianle Huang --- src/plugins/application_config/README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/plugins/application_config/README.md b/src/plugins/application_config/README.md index 4885e9d0127e..394c8dee5e1c 100755 --- a/src/plugins/application_config/README.md +++ b/src/plugins/application_config/README.md @@ -47,7 +47,17 @@ While a default OpenSearch based client is implemented, OSD users can use extern Let's call this plugin `MyConfigurationClientPlugin`. -First, this plugin will need to implement a class `MyConfigurationClient` based on interface `ConfigurationClient` defined in the `types.ts` under directory `src/plugins/application_config/server/types.ts`. +First, this plugin will need to implement a class `MyConfigurationClient` based on interface `ConfigurationClient` defined in the `types.ts` under directory `src/plugins/application_config/server/types.ts`. Below are the functions inside the interface. + +``` + getConfig(): Promise>; + + getEntityConfig(entity: string): Promise; + + updateEntityConfig(entity: string, newValue: string): Promise; + + deleteEntityConfig(entity: string): Promise; +``` Second, this plugin needs to declare `applicationConfig` as its dependency by adding it to `requiredPlugins` in its own `opensearch_dashboards.json`. @@ -78,6 +88,12 @@ Then the plugin will import the new type `AppPluginSetupDependencies` and add to ``` +## Onboarding Configurations + +Since the APIs and interfaces can take an entity, a new use case to this plugin could just pass their entity into the parameters. There is no need to implement new APIs or interfaces. To programmatically call the functions in `ConfigurationClient` from a plugin (the caller plugin), below is the code example. + +Similar to [section](#external-configuration-clients), a new type `AppPluginSetupDependencies` which encapsulates `ApplicationConfigPluginSetup` is needed. Then it can be imported into the `setup` function of the caller plugin. Then the caller plugin will have access to the `getConfigurationClient` and `registerConfigurationClient` exposed by `ApplicationConfigPluginSetup`. + ## Development See the [OpenSearch Dashboards contributing From 673d82dad4a4fb162e7460c01e44992e389dceab Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Tue, 20 Feb 2024 03:25:26 +0000 Subject: [PATCH 10/47] update variable name Signed-off-by: Tianle Huang --- .../server/opensearch_config_client.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/application_config/server/opensearch_config_client.ts b/src/plugins/application_config/server/opensearch_config_client.ts index a896b809ce89..990ba3bf1a0a 100644 --- a/src/plugins/application_config/server/opensearch_config_client.ts +++ b/src/plugins/application_config/server/opensearch_config_client.ts @@ -13,13 +13,13 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { private readonly logger: Logger; constructor( - inputOpenSearchClient: IScopedClusterClient, - inputConfigurationIndexName: string, - inputLogger: Logger + openSearchClient: IScopedClusterClient, + configurationIndexName: string, + logger: Logger ) { - this.client = inputOpenSearchClient; - this.configurationIndexName = inputConfigurationIndexName; - this.logger = inputLogger; + this.client = openSearchClient; + this.configurationIndexName = configurationIndexName; + this.logger = logger; } async getEntityConfig(entity: string) { From b92d72419b55b02ea30abe409463bddd79d87792 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Tue, 20 Feb 2024 03:45:56 +0000 Subject: [PATCH 11/47] remove unnecessary dependency Signed-off-by: Tianle Huang --- src/plugins/application_config/opensearch_dashboards.json | 4 ++-- src/plugins/application_config/server/plugin.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/application_config/opensearch_dashboards.json b/src/plugins/application_config/opensearch_dashboards.json index 6b1494902628..6dd45abae988 100644 --- a/src/plugins/application_config/opensearch_dashboards.json +++ b/src/plugins/application_config/opensearch_dashboards.json @@ -4,6 +4,6 @@ "opensearchDashboardsVersion": "opensearchDashboards", "server": true, "ui": false, - "requiredPlugins": ["navigation"], + "requiredPlugins": [], "optionalPlugins": [] -} +} \ No newline at end of file diff --git a/src/plugins/application_config/server/plugin.ts b/src/plugins/application_config/server/plugin.ts index 6d5618061e85..c314cd036518 100644 --- a/src/plugins/application_config/server/plugin.ts +++ b/src/plugins/application_config/server/plugin.ts @@ -38,18 +38,18 @@ export class ApplicationConfigPlugin this.configurationIndexName = ''; } - private registerConfigurationClient(inputConfigurationClient: ConfigurationClient) { + private registerConfigurationClient(configurationClient: ConfigurationClient) { this.logger.info('Register a configuration client.'); - this.configurationClient = inputConfigurationClient; + this.configurationClient = configurationClient; } - private getConfigurationClient(inputOpenSearchClient: IScopedClusterClient): ConfigurationClient { + private getConfigurationClient(configurationClient: IScopedClusterClient): ConfigurationClient { if (this.configurationClient) { return this.configurationClient; } const openSearchConfigurationClient = new OpenSearchConfigurationClient( - inputOpenSearchClient, + configurationClient, this.configurationIndexName, this.logger ); From 3be8782e772a661cc90633d1f42c2bd9f2b6ab4f Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Tue, 20 Feb 2024 03:49:24 +0000 Subject: [PATCH 12/47] format readme Signed-off-by: Tianle Huang --- src/plugins/application_config/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/plugins/application_config/README.md b/src/plugins/application_config/README.md index 394c8dee5e1c..83c6d2e56c7d 100755 --- a/src/plugins/application_config/README.md +++ b/src/plugins/application_config/README.md @@ -1,13 +1,15 @@ -# applicationConfig +# ApplicationConfig Plugin -A OpenSearch Dashboards plugin +An OpenSearch Dashboards plugin for application configuration service and OpenSearch implementation. + +--- + +## Introduction This plugin introduces the support of dynamic application configurations as opposed to the existing static configuration in OSD YAML file `opensearch_dashboards.yml`. It stores the configuration in an index whose default name is `.opensearch_dashboards_config` and could be customized through the key `opensearchDashboards.config_index` in OSD YAML file. Initially the new index does not exist. Only OSD users who need dynamic configurations will create it. It also provides an interface `ConfigurationClient` for future extensions of external configuration clients. A default implementation based on OpenSearch as database is used. ---- - ## Configuration OSD users who want to set up application configurations can perform such through CURL the OSD APIs. From 5c94f9ffa6b7294836cfa59d66c046e189bfa43b Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Tue, 20 Feb 2024 04:15:15 +0000 Subject: [PATCH 13/47] use osd version Signed-off-by: Tianle Huang --- src/plugins/application_config/opensearch_dashboards.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/application_config/opensearch_dashboards.json b/src/plugins/application_config/opensearch_dashboards.json index 6dd45abae988..728c282a2108 100644 --- a/src/plugins/application_config/opensearch_dashboards.json +++ b/src/plugins/application_config/opensearch_dashboards.json @@ -1,6 +1,6 @@ { "id": "applicationConfig", - "version": "1.0.0", + "version": "opensearchDashboards", "opensearchDashboardsVersion": "opensearchDashboards", "server": true, "ui": false, From 4bc6fa535973b3d331a0868a26862a5acc250774 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Wed, 21 Feb 2024 18:31:17 +0000 Subject: [PATCH 14/47] remove debugging info Signed-off-by: Tianle Huang --- .../application_config/server/opensearch_config_client.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/plugins/application_config/server/opensearch_config_client.ts b/src/plugins/application_config/server/opensearch_config_client.ts index 990ba3bf1a0a..41c4f4555610 100644 --- a/src/plugins/application_config/server/opensearch_config_client.ts +++ b/src/plugins/application_config/server/opensearch_config_client.ts @@ -29,8 +29,6 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { id: entity, }); - this.logger.info(`*** result is ${JSON.stringify(data)}`); - return data?.body?._source?.value || ''; } catch (e) { const errorMessage = `Failed to get entity ${entity} due to error ${e}`; From 60448f42490f6f983c2f508d06152e3ac96efeda Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Wed, 21 Feb 2024 18:32:20 +0000 Subject: [PATCH 15/47] update logging Signed-off-by: Tianle Huang --- src/plugins/application_config/server/plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/application_config/server/plugin.ts b/src/plugins/application_config/server/plugin.ts index c314cd036518..ab30d94aa34d 100644 --- a/src/plugins/application_config/server/plugin.ts +++ b/src/plugins/application_config/server/plugin.ts @@ -58,7 +58,7 @@ export class ApplicationConfigPlugin } public async setup(core: CoreSetup) { - this.logger.debug('applicationConfig: Setup'); + this.logger.info('applicationConfig: Setup'); const router = core.http.createRouter(); const config = await this.config$.pipe(first()).toPromise(); @@ -75,7 +75,7 @@ export class ApplicationConfigPlugin } public start(core: CoreStart) { - this.logger.debug('applicationConfig: Started'); + this.logger.info('applicationConfig: Started'); return {}; } From 845b1dc8ae9da0c317eed97476a666c2a74c01dc Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Thu, 22 Feb 2024 22:03:26 +0000 Subject: [PATCH 16/47] remove lint js Signed-off-by: Tianle Huang --- src/plugins/application_config/.eslintrc.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/plugins/application_config/.eslintrc.js diff --git a/src/plugins/application_config/.eslintrc.js b/src/plugins/application_config/.eslintrc.js deleted file mode 100644 index b16a8b23a08e..000000000000 --- a/src/plugins/application_config/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - root: true, - extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], - rules: { - '@osd/eslint/require-license-header': 'off', - }, -}; From 7d27aa5197003fee033443bf389c5232ba6cd970 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Thu, 22 Feb 2024 22:16:09 +0000 Subject: [PATCH 17/47] remove logs Signed-off-by: Tianle Huang --- src/plugins/application_config/common/index.ts | 2 +- src/plugins/application_config/server/plugin.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/plugins/application_config/common/index.ts b/src/plugins/application_config/common/index.ts index beaa29371234..57af4908f4a3 100644 --- a/src/plugins/application_config/common/index.ts +++ b/src/plugins/application_config/common/index.ts @@ -4,4 +4,4 @@ */ export const PLUGIN_ID = 'applicationConfig'; -export const PLUGIN_NAME = 'applicationConfig'; +export const PLUGIN_NAME = 'application_config'; diff --git a/src/plugins/application_config/server/plugin.ts b/src/plugins/application_config/server/plugin.ts index ab30d94aa34d..9bbe500beb89 100644 --- a/src/plugins/application_config/server/plugin.ts +++ b/src/plugins/application_config/server/plugin.ts @@ -58,7 +58,6 @@ export class ApplicationConfigPlugin } public async setup(core: CoreSetup) { - this.logger.info('applicationConfig: Setup'); const router = core.http.createRouter(); const config = await this.config$.pipe(first()).toPromise(); @@ -75,7 +74,6 @@ export class ApplicationConfigPlugin } public start(core: CoreStart) { - this.logger.info('applicationConfig: Started'); return {}; } From 9460c1123657894a048eb8f08a381891fe0a95ac Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Fri, 23 Feb 2024 00:08:24 +0000 Subject: [PATCH 18/47] update name style Signed-off-by: Tianle Huang --- config/opensearch_dashboards.yml | 2 +- src/core/server/mocks.ts | 2 +- src/core/server/opensearch_dashboards_config.ts | 2 +- src/core/server/plugins/types.ts | 2 +- src/legacy/server/config/schema.js | 2 +- src/plugins/application_config/README.md | 2 +- src/plugins/application_config/server/plugin.ts | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 780f5351dfb2..a6a7e399e4ed 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -31,7 +31,7 @@ # OpenSearch Dashboards uses an index in OpenSearch to store dynamic configurations. # This shall be a different index from opensearchDashboards.index. -# opensearchDashboards.config_index: ".opensearch_dashboards_config" +# opensearchDashboards.configIndex: ".opensearch_dashboards_config" # The default application to load. #opensearchDashboards.defaultAppId: "home" diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 5120598689df..687d408e40a6 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -77,7 +77,7 @@ export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { opensearchDashboards: { index: '.opensearch_dashboards_tests', - config_index: '.opensearch_dashboards_config_tests', + configIndex: '.opensearch_dashboards_config_tests', autocompleteTerminateAfter: duration(100000), autocompleteTimeout: duration(1000), }, diff --git a/src/core/server/opensearch_dashboards_config.ts b/src/core/server/opensearch_dashboards_config.ts index 715454c8cdee..47fa8a126501 100644 --- a/src/core/server/opensearch_dashboards_config.ts +++ b/src/core/server/opensearch_dashboards_config.ts @@ -48,7 +48,7 @@ export const config = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), index: schema.string({ defaultValue: '.kibana' }), - config_index: schema.string({ defaultValue: '.opensearch_dashboards_config' }), + configIndex: schema.string({ defaultValue: '.opensearch_dashboards_config' }), autocompleteTerminateAfter: schema.duration({ defaultValue: 100000 }), autocompleteTimeout: schema.duration({ defaultValue: 1000 }), branding: schema.object({ diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index aad5c742c5bf..59b9881279c3 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -289,7 +289,7 @@ export const SharedGlobalConfigKeys = { // We can add more if really needed opensearchDashboards: [ 'index', - 'config_index', + 'configIndex', 'autocompleteTerminateAfter', 'autocompleteTimeout', ] as const, diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 0084e41a72af..a102268effca 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -227,7 +227,7 @@ export default () => opensearchDashboards: Joi.object({ enabled: Joi.boolean().default(true), index: Joi.string().default('.kibana'), - config_index: Joi.string().default('.opensearch_dashboards_config'), + configIndex: Joi.string().default('.opensearch_dashboards_config'), autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000), // TODO Also allow units here like in opensearch config once this is moved to the new platform autocompleteTimeout: Joi.number().integer().min(1).default(1000), diff --git a/src/plugins/application_config/README.md b/src/plugins/application_config/README.md index 83c6d2e56c7d..8cffcc780ea3 100755 --- a/src/plugins/application_config/README.md +++ b/src/plugins/application_config/README.md @@ -6,7 +6,7 @@ An OpenSearch Dashboards plugin for application configuration service and OpenSe ## Introduction -This plugin introduces the support of dynamic application configurations as opposed to the existing static configuration in OSD YAML file `opensearch_dashboards.yml`. It stores the configuration in an index whose default name is `.opensearch_dashboards_config` and could be customized through the key `opensearchDashboards.config_index` in OSD YAML file. Initially the new index does not exist. Only OSD users who need dynamic configurations will create it. +This plugin introduces the support of dynamic application configurations as opposed to the existing static configuration in OSD YAML file `opensearch_dashboards.yml`. It stores the configuration in an index whose default name is `.opensearch_dashboards_config` and could be customized through the key `opensearchDashboards.configIndex` in OSD YAML file. Initially the new index does not exist. Only OSD users who need dynamic configurations will create it. It also provides an interface `ConfigurationClient` for future extensions of external configuration clients. A default implementation based on OpenSearch as database is used. diff --git a/src/plugins/application_config/server/plugin.ts b/src/plugins/application_config/server/plugin.ts index 9bbe500beb89..ad300aca4921 100644 --- a/src/plugins/application_config/server/plugin.ts +++ b/src/plugins/application_config/server/plugin.ts @@ -62,7 +62,7 @@ export class ApplicationConfigPlugin const config = await this.config$.pipe(first()).toPromise(); - this.configurationIndexName = config.opensearchDashboards.config_index; + this.configurationIndexName = config.opensearchDashboards.configIndex; // Register server side APIs defineRoutes(router, this.getConfigurationClient.bind(this), this.logger); @@ -77,5 +77,5 @@ export class ApplicationConfigPlugin return {}; } - public stop() {} + public stop() { } } From fefc17220df8d413ae279b62809c85494e364b45 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Fri, 23 Feb 2024 00:08:47 +0000 Subject: [PATCH 19/47] update Signed-off-by: Tianle Huang --- src/plugins/application_config/server/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/application_config/server/plugin.ts b/src/plugins/application_config/server/plugin.ts index ad300aca4921..c595e4c0739e 100644 --- a/src/plugins/application_config/server/plugin.ts +++ b/src/plugins/application_config/server/plugin.ts @@ -77,5 +77,5 @@ export class ApplicationConfigPlugin return {}; } - public stop() { } + public stop() {} } From d422a0daf81c13f677aa06d51eb7866995505dfb Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Fri, 23 Feb 2024 01:25:15 +0000 Subject: [PATCH 20/47] update function visibility and error function Signed-off-by: Tianle Huang --- src/plugins/application_config/server/routes/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/application_config/server/routes/index.ts b/src/plugins/application_config/server/routes/index.ts index 1fe312979245..7a059bf52f35 100644 --- a/src/plugins/application_config/server/routes/index.ts +++ b/src/plugins/application_config/server/routes/index.ts @@ -15,7 +15,7 @@ import { ConfigurationClient } from '../types'; export function defineRoutes( router: IRouter, - getConfigurationClient: (inputOpenSearchClient: IScopedClusterClient) => ConfigurationClient, + getConfigurationClient: (configurationClient: IScopedClusterClient) => ConfigurationClient, logger: Logger ) { router.get( @@ -154,8 +154,8 @@ export async function handleGetConfig( } } -export function errorResponse(response: OpenSearchDashboardsResponseFactory, error: any) { - return response.custom({ +function errorResponse(response: OpenSearchDashboardsResponseFactory, error: any) { + return response.customError({ statusCode: error?.statusCode || 500, body: error, }); From 01550dd857efba002d91f534421983e8e68a3628 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Fri, 23 Feb 2024 02:44:56 +0000 Subject: [PATCH 21/47] fix unit test failures Signed-off-by: Tianle Huang --- src/core/server/plugins/plugin_context.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 48c9eb6d6823..7a8ba042825b 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -98,6 +98,7 @@ describe('createPluginInitializerContext', () => { expect(configObject).toStrictEqual({ opensearchDashboards: { index: '.kibana', + configIndex: '.opensearch_dashboards_config', autocompleteTerminateAfter: duration(100000), autocompleteTimeout: duration(1000), }, From 1df50d2d5db26ab914b8530a2bca8e34fcbf0fa2 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Sat, 24 Feb 2024 00:01:30 +0000 Subject: [PATCH 22/47] add unit test Signed-off-by: Tianle Huang --- src/plugins/application_config/.eslintrc.js | 6 +++ .../server/opensearch_config_client.test.ts | 54 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/plugins/application_config/.eslintrc.js create mode 100644 src/plugins/application_config/server/opensearch_config_client.test.ts diff --git a/src/plugins/application_config/.eslintrc.js b/src/plugins/application_config/.eslintrc.js new file mode 100644 index 000000000000..05459e807dd4 --- /dev/null +++ b/src/plugins/application_config/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + root: true, + extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], + rules: { + }, +}; diff --git a/src/plugins/application_config/server/opensearch_config_client.test.ts b/src/plugins/application_config/server/opensearch_config_client.test.ts new file mode 100644 index 000000000000..e7af01f04e81 --- /dev/null +++ b/src/plugins/application_config/server/opensearch_config_client.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ScopedClusterClientMock, + opensearchClientMock, +} from '../../../core/server/opensearch/client/mocks'; +import { OpenSearchConfigurationClient } from './opensearch_config_client'; +import { MockedLogger, loggerMock } from '@osd/logging/target/mocks'; + +const INDEX_NAME = 'test_index'; + +describe('OpenSearch Configuration Client', () => { + let opensearchClient: ScopedClusterClientMock; + let logger: MockedLogger; + + beforeEach(() => { + opensearchClient = opensearchClientMock.createScopedClusterClient(); + logger = loggerMock.create(); + }); + + describe('getConfig', () => { + it('returns configurations from the index', async () => { + opensearchClient.asInternalUser.search.mockImplementation(() => { + return opensearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [ + { + _id: 'config1', + _source: { + value: 'value1', + }, + }, + { + _id: 'config2', + _source: { + value: 'value2', + }, + }, + ], + }, + }); + }); + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + const value = await client.getConfig(); + + expect(JSON.stringify(value)).toBe(JSON.stringify({ config1: 'value1', config2: 'value2' })); + }); + }); +}); From d88b7d1ffe965290dd7f196618ee01d954e589bd Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Sat, 24 Feb 2024 00:07:03 +0000 Subject: [PATCH 23/47] remove lint file Signed-off-by: Tianle Huang --- src/plugins/application_config/.eslintrc.js | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/plugins/application_config/.eslintrc.js diff --git a/src/plugins/application_config/.eslintrc.js b/src/plugins/application_config/.eslintrc.js deleted file mode 100644 index 05459e807dd4..000000000000 --- a/src/plugins/application_config/.eslintrc.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - root: true, - extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], - rules: { - }, -}; From eea3b1d98c6882823458131167b4c4c4c18c11bd Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Sat, 24 Feb 2024 01:01:21 +0000 Subject: [PATCH 24/47] add more tests Signed-off-by: Tianle Huang --- src/plugins/application_config/.eslintrc.js | 6 + .../server/opensearch_config_client.test.ts | 122 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 src/plugins/application_config/.eslintrc.js diff --git a/src/plugins/application_config/.eslintrc.js b/src/plugins/application_config/.eslintrc.js new file mode 100644 index 000000000000..05459e807dd4 --- /dev/null +++ b/src/plugins/application_config/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + root: true, + extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], + rules: { + }, +}; diff --git a/src/plugins/application_config/server/opensearch_config_client.test.ts b/src/plugins/application_config/server/opensearch_config_client.test.ts index e7af01f04e81..e28f82e3491c 100644 --- a/src/plugins/application_config/server/opensearch_config_client.test.ts +++ b/src/plugins/application_config/server/opensearch_config_client.test.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { ResponseError } from '@opensearch-project/opensearch/lib/errors'; import { ScopedClusterClientMock, opensearchClientMock, @@ -11,6 +12,7 @@ import { OpenSearchConfigurationClient } from './opensearch_config_client'; import { MockedLogger, loggerMock } from '@osd/logging/target/mocks'; const INDEX_NAME = 'test_index'; +const ERROR_MESSAGE = 'Service unavailable'; describe('OpenSearch Configuration Client', () => { let opensearchClient: ScopedClusterClientMock; @@ -50,5 +52,125 @@ describe('OpenSearch Configuration Client', () => { expect(JSON.stringify(value)).toBe(JSON.stringify({ config1: 'value1', config2: 'value2' })); }); + + it('throws error when opensearch errors happen', async () => { + const error = new Error(ERROR_MESSAGE); + + opensearchClient.asInternalUser.search.mockImplementation(() => { + return opensearchClientMock.createErrorTransportRequestPromise(error); + }); + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + await expect(client.getConfig()).rejects.toThrowError(ERROR_MESSAGE); + }); }); + + describe('getEntityConfig', () => { + it('return configuration value from the document in the index', async () => { + opensearchClient.asInternalUser.get.mockImplementation(() => { + return opensearchClientMock.createSuccessTransportRequestPromise({ + _source: { + value: 'value1', + }, + }); + }); + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + const value = await client.getEntityConfig('config1'); + + expect(value).toBe('value1'); + }); + + it('throws error when opensearch errors happen', async () => { + const error = new Error(ERROR_MESSAGE); + + opensearchClient.asInternalUser.get.mockImplementation(() => { + return opensearchClientMock.createErrorTransportRequestPromise(error); + }); + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + await expect(client.getEntityConfig('config1')).rejects.toThrowError(ERROR_MESSAGE); + }); + }); + + describe('deleteEntityConfig', () => { + it('return deleted entity when opensearch deletes successfully', async () => { + opensearchClient.asCurrentUser.delete.mockImplementation(() => { + return opensearchClientMock.createSuccessTransportRequestPromise({}); + }); + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + const value = await client.deleteEntityConfig('config1'); + + expect(value).toBe('config1'); + }); + + it('return deleted document entity when deletion fails due to index not found', async () => { + const error = new ResponseError({ + statusCode: 401, + body: { + error: { + type: 'index_not_found_exception', + }, + }, + warnings: [], + headers: { + 'WWW-Authenticate': 'content', + }, + meta: {} as any, + }); + + opensearchClient.asCurrentUser.delete.mockImplementation(() => { + return opensearchClientMock.createErrorTransportRequestPromise(error); + }); + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + const value = await client.deleteEntityConfig('config1'); + + expect(value).toBe('config1'); + }); + + it('return deleted document entity when deletion fails due to document not found', async () => { + const error = new ResponseError({ + statusCode: 401, + body: { + result: 'not_found', + }, + warnings: [], + headers: { + 'WWW-Authenticate': 'content', + }, + meta: {} as any, + }); + + opensearchClient.asCurrentUser.delete.mockImplementation(() => { + return opensearchClientMock.createErrorTransportRequestPromise(error); + }); + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + const value = await client.deleteEntityConfig('config1'); + + expect(value).toBe('config1'); + }); + + it('throws exception when opensearch throws exception', async () => { + const error = new Error(ERROR_MESSAGE); + + opensearchClient.asCurrentUser.delete.mockImplementation(() => { + return opensearchClientMock.createErrorTransportRequestPromise(error); + }); + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + await expect(client.deleteEntityConfig('config1')).rejects.toThrowError(ERROR_MESSAGE); + }); + }); + + describe('updateEntityConfig', () => {}); }); From 819628004de0c3a1e3cd6f37dff228d8870e2f74 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Sat, 24 Feb 2024 02:08:27 +0000 Subject: [PATCH 25/47] add unit tests for routes Signed-off-by: Tianle Huang --- .../server/opensearch_config_client.test.ts | 30 ++++++++++- .../server/routes/index.test.ts | 54 +++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 src/plugins/application_config/server/routes/index.test.ts diff --git a/src/plugins/application_config/server/opensearch_config_client.test.ts b/src/plugins/application_config/server/opensearch_config_client.test.ts index e28f82e3491c..5234e4d89c77 100644 --- a/src/plugins/application_config/server/opensearch_config_client.test.ts +++ b/src/plugins/application_config/server/opensearch_config_client.test.ts @@ -159,7 +159,7 @@ describe('OpenSearch Configuration Client', () => { expect(value).toBe('config1'); }); - it('throws exception when opensearch throws exception', async () => { + it('throws error when opensearch throws error', async () => { const error = new Error(ERROR_MESSAGE); opensearchClient.asCurrentUser.delete.mockImplementation(() => { @@ -172,5 +172,31 @@ describe('OpenSearch Configuration Client', () => { }); }); - describe('updateEntityConfig', () => {}); + describe('updateEntityConfig', () => { + it('returns updated value when opensearch updates successfully', async () => { + opensearchClient.asCurrentUser.index.mockImplementation(() => { + return opensearchClientMock.createSuccessTransportRequestPromise({}); + }); + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + const value = await client.updateEntityConfig('config1', 'newValue1'); + + expect(value).toBe('newValue1'); + }); + + it('throws error when opensearch throws error', async () => { + const error = new Error(ERROR_MESSAGE); + + opensearchClient.asCurrentUser.index.mockImplementation(() => { + return opensearchClientMock.createErrorTransportRequestPromise(error); + }); + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + await expect(client.updateEntityConfig('config1', 'newValue1')).rejects.toThrowError( + ERROR_MESSAGE + ); + }); + }); }); diff --git a/src/plugins/application_config/server/routes/index.test.ts b/src/plugins/application_config/server/routes/index.test.ts new file mode 100644 index 000000000000..f55aa484b928 --- /dev/null +++ b/src/plugins/application_config/server/routes/index.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { httpServiceMock } from '../../../../core/server/mocks'; +import { loggerMock } from '@osd/logging/target/mocks'; +import { defineRoutes } from '.'; + +describe('application config routes', () => { + describe('defineRoutes', () => { + it('check route paths are defined', () => { + const router = httpServiceMock.createRouter(); + const configurationClient = { + existsCspRules: jest.fn().mockReturnValue(true), + getCspRules: jest.fn().mockReturnValue(''), + }; + + const getConfigurationClient = jest.fn().mockReturnValue(configurationClient); + + const logger = loggerMock.create(); + + defineRoutes(router, getConfigurationClient, logger); + + expect(router.get).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/appconfig', + }), + expect.any(Function) + ); + + expect(router.get).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/appconfig/{entity}', + }), + expect.any(Function) + ); + + expect(router.post).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/appconfig/{entity}', + }), + expect.any(Function) + ); + + expect(router.delete).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/appconfig/{entity}', + }), + expect.any(Function) + ); + }); + }); +}); From a61041583a90d28053f9bcd75700444091b6871c Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Sat, 24 Feb 2024 03:00:04 +0000 Subject: [PATCH 26/47] add remaining unit tests Signed-off-by: Tianle Huang --- .../server/routes/index.test.ts | 301 +++++++++++++++++- 1 file changed, 300 insertions(+), 1 deletion(-) diff --git a/src/plugins/application_config/server/routes/index.test.ts b/src/plugins/application_config/server/routes/index.test.ts index f55aa484b928..086baa646d2b 100644 --- a/src/plugins/application_config/server/routes/index.test.ts +++ b/src/plugins/application_config/server/routes/index.test.ts @@ -5,7 +5,23 @@ import { httpServiceMock } from '../../../../core/server/mocks'; import { loggerMock } from '@osd/logging/target/mocks'; -import { defineRoutes } from '.'; +import { + defineRoutes, + handleDeleteEntityConfig, + handleGetConfig, + handleGetEntityConfig, + handleUpdateEntityConfig, +} from '.'; + +const ERROR_MESSAGE = 'Service unavailable'; + +const ERROR_RESPONSE = { + statusCode: 500, +}; + +const ENTITY_NAME = 'config1'; +const ENTITY_VALUE = 'value1'; +const ENTITY_NEW_VALUE = 'newValue1'; describe('application config routes', () => { describe('defineRoutes', () => { @@ -51,4 +67,287 @@ describe('application config routes', () => { ); }); }); + + describe('handleGetConfig', () => { + it('returns configurations when client returns', async () => { + const configurations = { + config1: 'value1', + config2: 'value2', + }; + + const client = { + getConfig: jest.fn().mockReturnValue(configurations), + }; + + const okResponse = { + statusCode: 200, + }; + + const response = { + ok: jest.fn().mockReturnValue(okResponse), + }; + + const logger = loggerMock.create(); + + const returnedResponse = await handleGetConfig(client, response, logger); + + expect(returnedResponse).toBe(okResponse); + + expect(response.ok).toBeCalledWith({ + body: { + value: configurations, + }, + }); + }); + + it('return error response when client throws error', async () => { + const error = new Error(ERROR_MESSAGE); + + const client = { + getConfig: jest.fn().mockImplementation(() => { + throw error; + }), + }; + + const response = { + customError: jest.fn().mockReturnValue(ERROR_RESPONSE), + }; + + const logger = loggerMock.create(); + + const returnedResponse = await handleGetConfig(client, response, logger); + + expect(returnedResponse).toBe(ERROR_RESPONSE); + + expect(client.getConfig).toBeCalledTimes(1); + + expect(response.customError).toBeCalledWith({ + body: error, + statusCode: 500, + }); + + expect(logger.error).toBeCalledWith(error); + }); + }); + + describe('handleGetEntityConfig', () => { + it('returns value when client returns value', async () => { + const client = { + getEntityConfig: jest.fn().mockReturnValue(ENTITY_VALUE), + }; + + const okResponse = { + statusCode: 200, + }; + + const request = { + params: { + entity: ENTITY_NAME, + }, + }; + + const response = { + ok: jest.fn().mockReturnValue(okResponse), + }; + + const logger = loggerMock.create(); + + const returnedResponse = await handleGetEntityConfig(client, request, response, logger); + + expect(returnedResponse).toBe(okResponse); + + expect(response.ok).toBeCalledWith({ + body: { + value: ENTITY_VALUE, + }, + }); + }); + + it('return error response when client throws error', async () => { + const error = new Error(ERROR_MESSAGE); + + const client = { + getEntityConfig: jest.fn().mockImplementation(() => { + throw error; + }), + }; + + const request = { + params: { + entity: ENTITY_NAME, + }, + }; + + const response = { + customError: jest.fn().mockReturnValue(ERROR_RESPONSE), + }; + + const logger = loggerMock.create(); + + const returnedResponse = await handleGetEntityConfig(client, request, response, logger); + + expect(returnedResponse).toBe(ERROR_RESPONSE); + + expect(client.getEntityConfig).toBeCalledTimes(1); + + expect(response.customError).toBeCalledWith({ + body: error, + statusCode: 500, + }); + + expect(logger.error).toBeCalledWith(error); + }); + }); + + describe('handleUpdateEntityConfig', () => { + it('return success when client succeeds', async () => { + const client = { + updateEntityConfig: jest.fn().mockReturnValue(ENTITY_NEW_VALUE), + }; + + const okResponse = { + statusCode: 200, + }; + + const request = { + params: { + entity: ENTITY_NAME, + }, + body: { + newValue: ENTITY_NEW_VALUE, + }, + }; + + const response = { + ok: jest.fn().mockReturnValue(okResponse), + }; + + const logger = loggerMock.create(); + + const returnedResponse = await handleUpdateEntityConfig(client, request, response, logger); + + expect(returnedResponse).toBe(okResponse); + + expect(client.updateEntityConfig).toBeCalledTimes(1); + + expect(response.ok).toBeCalledWith({ + body: { + newValue: ENTITY_NEW_VALUE, + }, + }); + + expect(logger.error).not.toBeCalled(); + }); + + it('return error response when client fails', async () => { + const error = new Error(ERROR_MESSAGE); + + const client = { + updateEntityConfig: jest.fn().mockImplementation(() => { + throw error; + }), + }; + + const request = { + params: { + entity: ENTITY_NAME, + }, + body: { + newValue: ENTITY_NEW_VALUE, + }, + }; + + const response = { + customError: jest.fn().mockReturnValue(ERROR_RESPONSE), + }; + + const logger = loggerMock.create(); + + const returnedResponse = await handleUpdateEntityConfig(client, request, response, logger); + + expect(returnedResponse).toBe(ERROR_RESPONSE); + + expect(client.updateEntityConfig).toBeCalledTimes(1); + + expect(response.customError).toBeCalledWith({ + body: error, + statusCode: 500, + }); + + expect(logger.error).toBeCalledWith(error); + }); + }); + + describe('handleDeleteEntityConfig', () => { + it('returns successful response when client succeeds', async () => { + const client = { + deleteEntityConfig: jest.fn().mockReturnValue(ENTITY_NAME), + }; + + const okResponse = { + statusCode: 200, + }; + + const request = { + params: { + entity: ENTITY_NAME, + }, + }; + + const response = { + ok: jest.fn().mockReturnValue(okResponse), + }; + + const logger = loggerMock.create(); + + const returnedResponse = await handleDeleteEntityConfig(client, request, response, logger); + + expect(returnedResponse).toBe(okResponse); + + expect(client.deleteEntityConfig).toBeCalledTimes(1); + + expect(response.ok).toBeCalledWith({ + body: { + deletedEntity: ENTITY_NAME, + }, + }); + + expect(logger.error).not.toBeCalled(); + }); + + it('return error response when client fails', async () => { + const error = new Error(ERROR_MESSAGE); + + const client = { + deleteEntityConfig: jest.fn().mockImplementation(() => { + throw error; + }), + }; + + const request = { + params: { + entity: ENTITY_NAME, + }, + }; + + const response = { + customError: jest.fn().mockReturnValue(ERROR_RESPONSE), + }; + + const logger = loggerMock.create(); + + const returnedResponse = await handleDeleteEntityConfig(client, request, response, logger); + + expect(returnedResponse).toBe(ERROR_RESPONSE); + + expect(client.deleteEntityConfig).toBeCalledTimes(1); + + expect(response.customError).toBeCalledWith({ + body: error, + statusCode: 500, + }); + + expect(logger.error).toBeCalledWith(error); + }); + }); }); From 4dd1f7288152fb6ada10f6f705324d95421bef46 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Sat, 24 Feb 2024 03:09:05 +0000 Subject: [PATCH 27/47] add enabled to this plugin Signed-off-by: Tianle Huang --- src/plugins/application_config/config.ts | 12 ++++++++++++ src/plugins/application_config/server/index.ts | 7 ++++++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/plugins/application_config/config.ts diff --git a/src/plugins/application_config/config.ts b/src/plugins/application_config/config.ts new file mode 100644 index 000000000000..4968c8a9a7c7 --- /dev/null +++ b/src/plugins/application_config/config.ts @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema, TypeOf } from '@osd/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); + +export type ApplicationConfigSchema = TypeOf; diff --git a/src/plugins/application_config/server/index.ts b/src/plugins/application_config/server/index.ts index 2f46a43d9add..cae955d2a82c 100644 --- a/src/plugins/application_config/server/index.ts +++ b/src/plugins/application_config/server/index.ts @@ -3,12 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { PluginInitializerContext } from '../../../core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../core/server'; +import { ApplicationConfigSchema, configSchema } from '../config'; import { ApplicationConfigPlugin } from './plugin'; // This exports static code and TypeScript types, // as well as, OpenSearch Dashboards Platform `plugin()` initializer. +export const config: PluginConfigDescriptor = { + schema: configSchema, +}; + export function plugin(initializerContext: PluginInitializerContext) { return new ApplicationConfigPlugin(initializerContext); } From 17c84b967c5502882700c5d4e17e24fcc75bd330 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Sat, 24 Feb 2024 03:17:06 +0000 Subject: [PATCH 28/47] update readme to mention experimental Signed-off-by: Tianle Huang --- src/plugins/application_config/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/plugins/application_config/README.md b/src/plugins/application_config/README.md index 8cffcc780ea3..f02e422ce53a 100755 --- a/src/plugins/application_config/README.md +++ b/src/plugins/application_config/README.md @@ -1,6 +1,6 @@ # ApplicationConfig Plugin -An OpenSearch Dashboards plugin for application configuration service and OpenSearch implementation. +An experimental OpenSearch Dashboards plugin for application configuration service and OpenSearch implementation. --- @@ -10,9 +10,17 @@ This plugin introduces the support of dynamic application configurations as oppo It also provides an interface `ConfigurationClient` for future extensions of external configuration clients. A default implementation based on OpenSearch as database is used. +This plugin is disabled by default. + ## Configuration -OSD users who want to set up application configurations can perform such through CURL the OSD APIs. +OSD users who want to set up application configurations will first need to enable this plugin by the following line in OSD YML. +``` +application_config.enabled: true + +``` + +Then they can perform configuration operations through CURL the OSD APIs. (Note that the commands following could be first obtained from a copy as curl option from the network tab of a browser development tool and then replaced with the API names) From d1dd2b5baebe7f106ec740be0866186711055d20 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Sat, 24 Feb 2024 03:26:26 +0000 Subject: [PATCH 29/47] update change log Signed-off-by: Tianle Huang --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7e9cff5c6e6..548e394072b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple Datasource] Refactor client and legacy client to use authentication registry ([#5881](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5881)) - [Multiple Datasource] Improved error handling for the search API when a null value is passed for the dataSourceId ([#5882](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5882)) - [Multiple Datasource] Hide/Show authentication method in multi data source plugin based on configuration ([#5916](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5916)) +- Add support for dynamic application configurations ([#5855](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5855)) ### 🐛 Bug Fixes From 43e3e7f6c1aae32ac65e9e79c953cde8bae65fd1 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Sat, 24 Feb 2024 05:47:08 +0000 Subject: [PATCH 30/47] dummy commit to trigger workflow rerun Signed-off-by: Tianle Huang --- src/plugins/application_config/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/application_config/README.md b/src/plugins/application_config/README.md index f02e422ce53a..be8d3890f72f 100755 --- a/src/plugins/application_config/README.md +++ b/src/plugins/application_config/README.md @@ -15,6 +15,7 @@ This plugin is disabled by default. ## Configuration OSD users who want to set up application configurations will first need to enable this plugin by the following line in OSD YML. + ``` application_config.enabled: true From cd54144cc2e16ff6f9b8e3b3a9381ed77ea8f187 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Mon, 26 Feb 2024 21:12:35 +0000 Subject: [PATCH 31/47] remove experimental Signed-off-by: Tianle Huang --- src/plugins/application_config/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/application_config/README.md b/src/plugins/application_config/README.md index be8d3890f72f..46f6078d52dd 100755 --- a/src/plugins/application_config/README.md +++ b/src/plugins/application_config/README.md @@ -1,6 +1,6 @@ # ApplicationConfig Plugin -An experimental OpenSearch Dashboards plugin for application configuration service and OpenSearch implementation. +An OpenSearch Dashboards plugin for application configuration service and OpenSearch implementation. --- From 3117dbb33253876b820b0bbb5a58e18b7e889ffe Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Tue, 27 Feb 2024 21:55:52 +0000 Subject: [PATCH 32/47] add key to yml file Signed-off-by: Tianle Huang --- config/opensearch_dashboards.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index a6a7e399e4ed..ea0a18c8b635 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -289,4 +289,7 @@ # opensearchDashboards.survey.url: "https://survey.opensearch.org" # Set the value of this setting to true to enable plugin augmentation on Dashboard -# vis_augmenter.pluginAugmentationEnabled: true \ No newline at end of file +# vis_augmenter.pluginAugmentationEnabled: true + +# Set the value of this setting to true to enable plugin application config. By default it is disabled. +# application_config.enabled: false \ No newline at end of file From ffe1a925979aedab5aad6f51e18d4c29b0a1b70a Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Tue, 27 Feb 2024 23:37:07 +0000 Subject: [PATCH 33/47] remove i18n Signed-off-by: Tianle Huang --- src/plugins/application_config/.i18nrc.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/plugins/application_config/.i18nrc.json diff --git a/src/plugins/application_config/.i18nrc.json b/src/plugins/application_config/.i18nrc.json deleted file mode 100644 index 06267a67f042..000000000000 --- a/src/plugins/application_config/.i18nrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "prefix": "applicationConfig", - "paths": { - "applicationConfig": "." - }, - "translations": ["translations/ja-JP.json"] -} From f6c46476c16ff9302794230b669d8cd85585dfc9 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Wed, 28 Feb 2024 01:25:37 +0000 Subject: [PATCH 34/47] remove lint rc Signed-off-by: Tianle Huang --- src/plugins/application_config/.eslintrc.js | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/plugins/application_config/.eslintrc.js diff --git a/src/plugins/application_config/.eslintrc.js b/src/plugins/application_config/.eslintrc.js deleted file mode 100644 index 05459e807dd4..000000000000 --- a/src/plugins/application_config/.eslintrc.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - root: true, - extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], - rules: { - }, -}; From 813d897e0ecedcb0524a3064363e50ee703192d3 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Wed, 28 Feb 2024 02:07:59 +0000 Subject: [PATCH 35/47] update comment style Signed-off-by: Tianle Huang --- src/plugins/application_config/server/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/application_config/server/index.ts b/src/plugins/application_config/server/index.ts index cae955d2a82c..1ef2bbc3baf9 100644 --- a/src/plugins/application_config/server/index.ts +++ b/src/plugins/application_config/server/index.ts @@ -7,8 +7,10 @@ import { PluginConfigDescriptor, PluginInitializerContext } from '../../../core/ import { ApplicationConfigSchema, configSchema } from '../config'; import { ApplicationConfigPlugin } from './plugin'; -// This exports static code and TypeScript types, -// as well as, OpenSearch Dashboards Platform `plugin()` initializer. +/* +This exports static code and TypeScript types, +as well as, OpenSearch Dashboards Platform `plugin()` initializer. +*/ export const config: PluginConfigDescriptor = { schema: configSchema, From a33886331df30f7ae17670ed56903e3ca91c0018 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Wed, 28 Feb 2024 19:54:14 +0000 Subject: [PATCH 36/47] add input validation Signed-off-by: Tianle Huang --- .../server/opensearch_config_client.ts | 16 ++++++++++++++++ .../application_config/server/string_utils.ts | 14 ++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/plugins/application_config/server/string_utils.ts diff --git a/src/plugins/application_config/server/opensearch_config_client.ts b/src/plugins/application_config/server/opensearch_config_client.ts index 41c4f4555610..b1e82d33f7a2 100644 --- a/src/plugins/application_config/server/opensearch_config_client.ts +++ b/src/plugins/application_config/server/opensearch_config_client.ts @@ -6,6 +6,7 @@ import { IScopedClusterClient, Logger } from '../../../../src/core/server'; import { ConfigurationClient } from './types'; +import { ERROR_FOR_EMPTY_INPUT, ERROR_MESSSAGE_FOR_EMPTY_INPUT, isEmpty } from './string_utils'; export class OpenSearchConfigurationClient implements ConfigurationClient { private client: IScopedClusterClient; @@ -23,6 +24,11 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { } async getEntityConfig(entity: string) { + if (isEmpty(entity)) { + this.logger.error(ERROR_MESSSAGE_FOR_EMPTY_INPUT); + throw ERROR_FOR_EMPTY_INPUT; + } + try { const data = await this.client.asInternalUser.get({ index: this.configurationIndexName, @@ -40,6 +46,11 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { } async updateEntityConfig(entity: string, newValue: string) { + if (isEmpty(entity) || isEmpty(newValue)) { + this.logger.error(ERROR_MESSSAGE_FOR_EMPTY_INPUT); + throw ERROR_FOR_EMPTY_INPUT; + } + try { await this.client.asCurrentUser.index({ index: this.configurationIndexName, @@ -60,6 +71,11 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { } async deleteEntityConfig(entity: string) { + if (isEmpty(entity)) { + this.logger.error(ERROR_MESSSAGE_FOR_EMPTY_INPUT); + throw ERROR_FOR_EMPTY_INPUT; + } + try { await this.client.asCurrentUser.delete({ index: this.configurationIndexName, diff --git a/src/plugins/application_config/server/string_utils.ts b/src/plugins/application_config/server/string_utils.ts new file mode 100644 index 000000000000..9216fdf4f4b9 --- /dev/null +++ b/src/plugins/application_config/server/string_utils.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +export const ERROR_MESSSAGE_FOR_EMPTY_INPUT = 'Input cannot be empty!'; +export const ERROR_FOR_EMPTY_INPUT = new Error(ERROR_MESSSAGE_FOR_EMPTY_INPUT); + +export function isEmpty(input: string): boolean { + if (!input) { + return true; + } + + return !input.trim(); +} From 98b2bfd9e7617811b70389f024069ed3a7191dc7 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Thu, 29 Feb 2024 00:39:11 +0000 Subject: [PATCH 37/47] update unit tests Signed-off-by: Tianle Huang --- .../server/opensearch_config_client.test.ts | 275 ++++++++++++++---- 1 file changed, 216 insertions(+), 59 deletions(-) diff --git a/src/plugins/application_config/server/opensearch_config_client.test.ts b/src/plugins/application_config/server/opensearch_config_client.test.ts index 5234e4d89c77..827d309303cb 100644 --- a/src/plugins/application_config/server/opensearch_config_client.test.ts +++ b/src/plugins/application_config/server/opensearch_config_client.test.ts @@ -4,47 +4,49 @@ */ import { ResponseError } from '@opensearch-project/opensearch/lib/errors'; -import { - ScopedClusterClientMock, - opensearchClientMock, -} from '../../../core/server/opensearch/client/mocks'; import { OpenSearchConfigurationClient } from './opensearch_config_client'; import { MockedLogger, loggerMock } from '@osd/logging/target/mocks'; const INDEX_NAME = 'test_index'; const ERROR_MESSAGE = 'Service unavailable'; +const ERROR_MESSSAGE_FOR_EMPTY_INPUT = 'Input cannot be empty!'; +const EMPTY_INPUT = ' '; describe('OpenSearch Configuration Client', () => { - let opensearchClient: ScopedClusterClientMock; let logger: MockedLogger; beforeEach(() => { - opensearchClient = opensearchClientMock.createScopedClusterClient(); logger = loggerMock.create(); }); describe('getConfig', () => { it('returns configurations from the index', async () => { - opensearchClient.asInternalUser.search.mockImplementation(() => { - return opensearchClientMock.createSuccessTransportRequestPromise({ - hits: { - hits: [ - { - _id: 'config1', - _source: { - value: 'value1', + const opensearchClient = { + asInternalUser: { + search: jest.fn().mockImplementation(() => { + return { + body: { + hits: { + hits: [ + { + _id: 'config1', + _source: { + value: 'value1', + }, + }, + { + _id: 'config2', + _source: { + value: 'value2', + }, + }, + ], }, }, - { - _id: 'config2', - _source: { - value: 'value2', - }, - }, - ], - }, - }); - }); + }; + }), + }, + }; const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); @@ -54,12 +56,27 @@ describe('OpenSearch Configuration Client', () => { }); it('throws error when opensearch errors happen', async () => { - const error = new Error(ERROR_MESSAGE); - - opensearchClient.asInternalUser.search.mockImplementation(() => { - return opensearchClientMock.createErrorTransportRequestPromise(error); + const error = new ResponseError({ + statusCode: 401, + body: { + error: { + type: ERROR_MESSAGE, + }, + }, + warnings: [], + headers: { + 'WWW-Authenticate': 'content', + }, + meta: {} as any, }); + const opensearchClient = { + asInternalUser: { + search: jest.fn().mockImplementation(() => { + throw error; + }), + }, + }; const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); await expect(client.getConfig()).rejects.toThrowError(ERROR_MESSAGE); @@ -68,13 +85,19 @@ describe('OpenSearch Configuration Client', () => { describe('getEntityConfig', () => { it('return configuration value from the document in the index', async () => { - opensearchClient.asInternalUser.get.mockImplementation(() => { - return opensearchClientMock.createSuccessTransportRequestPromise({ - _source: { - value: 'value1', - }, - }); - }); + const opensearchClient = { + asInternalUser: { + get: jest.fn().mockImplementation(() => { + return { + body: { + _source: { + value: 'value1', + }, + }, + }; + }), + }, + }; const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); @@ -83,13 +106,51 @@ describe('OpenSearch Configuration Client', () => { expect(value).toBe('value1'); }); - it('throws error when opensearch errors happen', async () => { - const error = new Error(ERROR_MESSAGE); + it('throws error when input is empty', async () => { + const opensearchClient = { + asInternalUser: { + get: jest.fn().mockImplementation(() => { + return { + body: { + _source: { + value: 'value1', + }, + }, + }; + }), + }, + }; + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + await expect(client.getEntityConfig(EMPTY_INPUT)).rejects.toThrowError( + ERROR_MESSSAGE_FOR_EMPTY_INPUT + ); + }); - opensearchClient.asInternalUser.get.mockImplementation(() => { - return opensearchClientMock.createErrorTransportRequestPromise(error); + it('throws error when opensearch errors happen', async () => { + const error = new ResponseError({ + statusCode: 401, + body: { + error: { + type: ERROR_MESSAGE, + }, + }, + warnings: [], + headers: { + 'WWW-Authenticate': 'content', + }, + meta: {} as any, }); + const opensearchClient = { + asInternalUser: { + get: jest.fn().mockImplementation(() => { + throw error; + }), + }, + }; + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); await expect(client.getEntityConfig('config1')).rejects.toThrowError(ERROR_MESSAGE); @@ -98,9 +159,13 @@ describe('OpenSearch Configuration Client', () => { describe('deleteEntityConfig', () => { it('return deleted entity when opensearch deletes successfully', async () => { - opensearchClient.asCurrentUser.delete.mockImplementation(() => { - return opensearchClientMock.createSuccessTransportRequestPromise({}); - }); + const opensearchClient = { + asCurrentUser: { + delete: jest.fn().mockImplementation(() => { + return {}; + }), + }, + }; const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); @@ -109,6 +174,22 @@ describe('OpenSearch Configuration Client', () => { expect(value).toBe('config1'); }); + it('throws error when input entity is empty', async () => { + const opensearchClient = { + asCurrentUser: { + delete: jest.fn().mockImplementation(() => { + return {}; + }), + }, + }; + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + await expect(client.deleteEntityConfig(EMPTY_INPUT)).rejects.toThrowError( + ERROR_MESSSAGE_FOR_EMPTY_INPUT + ); + }); + it('return deleted document entity when deletion fails due to index not found', async () => { const error = new ResponseError({ statusCode: 401, @@ -124,9 +205,13 @@ describe('OpenSearch Configuration Client', () => { meta: {} as any, }); - opensearchClient.asCurrentUser.delete.mockImplementation(() => { - return opensearchClientMock.createErrorTransportRequestPromise(error); - }); + const opensearchClient = { + asCurrentUser: { + delete: jest.fn().mockImplementation(() => { + throw error; + }), + }, + }; const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); @@ -148,9 +233,13 @@ describe('OpenSearch Configuration Client', () => { meta: {} as any, }); - opensearchClient.asCurrentUser.delete.mockImplementation(() => { - return opensearchClientMock.createErrorTransportRequestPromise(error); - }); + const opensearchClient = { + asCurrentUser: { + delete: jest.fn().mockImplementation(() => { + throw error; + }), + }, + }; const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); @@ -160,12 +249,28 @@ describe('OpenSearch Configuration Client', () => { }); it('throws error when opensearch throws error', async () => { - const error = new Error(ERROR_MESSAGE); - - opensearchClient.asCurrentUser.delete.mockImplementation(() => { - return opensearchClientMock.createErrorTransportRequestPromise(error); + const error = new ResponseError({ + statusCode: 401, + body: { + error: { + type: ERROR_MESSAGE, + }, + }, + warnings: [], + headers: { + 'WWW-Authenticate': 'content', + }, + meta: {} as any, }); + const opensearchClient = { + asCurrentUser: { + delete: jest.fn().mockImplementation(() => { + throw error; + }), + }, + }; + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); await expect(client.deleteEntityConfig('config1')).rejects.toThrowError(ERROR_MESSAGE); @@ -174,9 +279,13 @@ describe('OpenSearch Configuration Client', () => { describe('updateEntityConfig', () => { it('returns updated value when opensearch updates successfully', async () => { - opensearchClient.asCurrentUser.index.mockImplementation(() => { - return opensearchClientMock.createSuccessTransportRequestPromise({}); - }); + const opensearchClient = { + asCurrentUser: { + index: jest.fn().mockImplementation(() => { + return {}; + }), + }, + }; const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); @@ -185,13 +294,61 @@ describe('OpenSearch Configuration Client', () => { expect(value).toBe('newValue1'); }); - it('throws error when opensearch throws error', async () => { - const error = new Error(ERROR_MESSAGE); + it('throws error when entity is empty ', async () => { + const opensearchClient = { + asCurrentUser: { + index: jest.fn().mockImplementation(() => { + return {}; + }), + }, + }; + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); + + await expect(client.updateEntityConfig(EMPTY_INPUT, 'newValue1')).rejects.toThrowError( + ERROR_MESSSAGE_FOR_EMPTY_INPUT + ); + }); + + it('throws error when new value is empty ', async () => { + const opensearchClient = { + asCurrentUser: { + index: jest.fn().mockImplementation(() => { + return {}; + }), + }, + }; + + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); - opensearchClient.asCurrentUser.index.mockImplementation(() => { - return opensearchClientMock.createErrorTransportRequestPromise(error); + await expect(client.updateEntityConfig('config1', EMPTY_INPUT)).rejects.toThrowError( + ERROR_MESSSAGE_FOR_EMPTY_INPUT + ); + }); + + it('throws error when opensearch throws error', async () => { + const error = new ResponseError({ + statusCode: 401, + body: { + error: { + type: ERROR_MESSAGE, + }, + }, + warnings: [], + headers: { + 'WWW-Authenticate': 'content', + }, + meta: {} as any, }); + const opensearchClient = { + asCurrentUser: { + index: jest.fn().mockImplementation(() => { + throw error; + }), + }, + }; + const client = new OpenSearchConfigurationClient(opensearchClient, INDEX_NAME, logger); await expect(client.updateEntityConfig('config1', 'newValue1')).rejects.toThrowError( From a59fe4fe0c618b8fa181060f3e474a6bf2e61985 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Thu, 29 Feb 2024 04:15:14 +0000 Subject: [PATCH 38/47] prevent multiple registration Signed-off-by: Tianle Huang --- src/plugins/application_config/server/plugin.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/plugins/application_config/server/plugin.ts b/src/plugins/application_config/server/plugin.ts index c595e4c0739e..260d8ce0584b 100644 --- a/src/plugins/application_config/server/plugin.ts +++ b/src/plugins/application_config/server/plugin.ts @@ -40,6 +40,13 @@ export class ApplicationConfigPlugin private registerConfigurationClient(configurationClient: ConfigurationClient) { this.logger.info('Register a configuration client.'); + + if (this.configurationClient) { + const errorMessage = 'Configuration client is already registered! Cannot register again!'; + this.logger.error(errorMessage); + throw new Error(errorMessage); + } + this.configurationClient = configurationClient; } From 11980476419216b772ec5daaf7b2aa8375d5bcd7 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Thu, 29 Feb 2024 07:40:04 +0000 Subject: [PATCH 39/47] add return types Signed-off-by: Tianle Huang --- .../application_config/server/opensearch_config_client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/application_config/server/opensearch_config_client.ts b/src/plugins/application_config/server/opensearch_config_client.ts index b1e82d33f7a2..0fb39f29b9dd 100644 --- a/src/plugins/application_config/server/opensearch_config_client.ts +++ b/src/plugins/application_config/server/opensearch_config_client.ts @@ -102,7 +102,7 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { } } - async getConfig(): Promise { + async getConfig(): Promise> { try { const data = await this.client.asInternalUser.search({ index: this.configurationIndexName, @@ -118,7 +118,7 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { } } - transformIndexSearchResponse(hits) { + transformIndexSearchResponse(hits): Map { const configurations = {}; for (let i = 0; i < hits.length; i++) { From d2668d70d4e0f920b5a4ec283ed7533859636395 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Thu, 29 Feb 2024 07:51:34 +0000 Subject: [PATCH 40/47] update readme wording Signed-off-by: Tianle Huang --- src/plugins/application_config/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/application_config/README.md b/src/plugins/application_config/README.md index 46f6078d52dd..cad28722d63e 100755 --- a/src/plugins/application_config/README.md +++ b/src/plugins/application_config/README.md @@ -1,6 +1,6 @@ # ApplicationConfig Plugin -An OpenSearch Dashboards plugin for application configuration service and OpenSearch implementation. +An OpenSearch Dashboards plugin for application configuration and a default implementation based on OpenSearch as storage. --- From 90e5a0828b12b1aa752c3e6e7d9eecea6bca6b32 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Thu, 29 Feb 2024 09:26:35 +0000 Subject: [PATCH 41/47] add unit test to the plugin class about double register Signed-off-by: Tianle Huang --- .../application_config/server/plugin.test.ts | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/plugins/application_config/server/plugin.test.ts diff --git a/src/plugins/application_config/server/plugin.test.ts b/src/plugins/application_config/server/plugin.test.ts new file mode 100644 index 000000000000..e1ac45444c14 --- /dev/null +++ b/src/plugins/application_config/server/plugin.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { of } from 'rxjs'; +import { ApplicationConfigPlugin } from './plugin'; +import { ConfigurationClient } from './types'; + +describe('application config plugin', () => { + it('throws error when trying to register twice', async () => { + const initializerContext = { + logger: { + get: jest.fn().mockImplementation(() => { + return { + info: jest.fn(), + error: jest.fn(), + }; + }), + }, + config: { + legacy: { + globalConfig$: of({ + opensearchDashboards: { + configIndex: '.osd_test', + }, + }), + }, + }, + }; + + const plugin = new ApplicationConfigPlugin(initializerContext); + + const coreSetup = { + http: { + createRouter: jest.fn().mockImplementation(() => { + return { + get: jest.fn(), + post: jest.fn(), + delete: jest.fn(), + }; + }), + }, + }; + + const setup = await plugin.setup(coreSetup); + + const client1: ConfigurationClient = { + getConfig: jest.fn(), + getEntityConfig: jest.fn(), + updateEntityConfig: jest.fn(), + deleteEntityConfig: jest.fn(), + }; + + setup.registerConfigurationClient(client1); + + const scopedClient = {}; + expect(setup.getConfigurationClient(scopedClient)).toBe(client1); + + const client2: ConfigurationClient = { + getConfig: jest.fn(), + getEntityConfig: jest.fn(), + updateEntityConfig: jest.fn(), + deleteEntityConfig: jest.fn(), + }; + + // call the register function again + const secondCall = () => setup.registerConfigurationClient(client2); + + expect(secondCall).toThrowError( + 'Configuration client is already registered! Cannot register again!' + ); + + expect(setup.getConfigurationClient(scopedClient)).toBe(client1); + }); +}); From f4e1a31586019e732a99b60adce6531cf3d04610 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Thu, 29 Feb 2024 22:48:58 +0000 Subject: [PATCH 42/47] move related ymls Signed-off-by: Tianle Huang --- config/opensearch_dashboards.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index ea0a18c8b635..4dd647981bb5 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -33,6 +33,9 @@ # This shall be a different index from opensearchDashboards.index. # opensearchDashboards.configIndex: ".opensearch_dashboards_config" +# Set the value of this setting to true to enable plugin application config. By default it is disabled. +# application_config.enabled: false + # The default application to load. #opensearchDashboards.defaultAppId: "home" @@ -290,6 +293,3 @@ # Set the value of this setting to true to enable plugin augmentation on Dashboard # vis_augmenter.pluginAugmentationEnabled: true - -# Set the value of this setting to true to enable plugin application config. By default it is disabled. -# application_config.enabled: false \ No newline at end of file From 6db36dfa52e58dd70f360d9a36903cd816cf20f1 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Thu, 29 Feb 2024 23:06:32 +0000 Subject: [PATCH 43/47] move validation to a function Signed-off-by: Tianle Huang --- .../server/opensearch_config_client.ts | 18 ++++-------- .../server/string_utils.test.ts | 28 +++++++++++++++++++ .../application_config/server/string_utils.ts | 16 +++++++++-- 3 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 src/plugins/application_config/server/string_utils.test.ts diff --git a/src/plugins/application_config/server/opensearch_config_client.ts b/src/plugins/application_config/server/opensearch_config_client.ts index 0fb39f29b9dd..bc3993945f87 100644 --- a/src/plugins/application_config/server/opensearch_config_client.ts +++ b/src/plugins/application_config/server/opensearch_config_client.ts @@ -6,7 +6,7 @@ import { IScopedClusterClient, Logger } from '../../../../src/core/server'; import { ConfigurationClient } from './types'; -import { ERROR_FOR_EMPTY_INPUT, ERROR_MESSSAGE_FOR_EMPTY_INPUT, isEmpty } from './string_utils'; +import { validate } from './string_utils'; export class OpenSearchConfigurationClient implements ConfigurationClient { private client: IScopedClusterClient; @@ -24,10 +24,7 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { } async getEntityConfig(entity: string) { - if (isEmpty(entity)) { - this.logger.error(ERROR_MESSSAGE_FOR_EMPTY_INPUT); - throw ERROR_FOR_EMPTY_INPUT; - } + validate(entity, this.logger); try { const data = await this.client.asInternalUser.get({ @@ -46,10 +43,8 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { } async updateEntityConfig(entity: string, newValue: string) { - if (isEmpty(entity) || isEmpty(newValue)) { - this.logger.error(ERROR_MESSSAGE_FOR_EMPTY_INPUT); - throw ERROR_FOR_EMPTY_INPUT; - } + validate(entity, this.logger); + validate(newValue, this.logger); try { await this.client.asCurrentUser.index({ @@ -71,10 +66,7 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { } async deleteEntityConfig(entity: string) { - if (isEmpty(entity)) { - this.logger.error(ERROR_MESSSAGE_FOR_EMPTY_INPUT); - throw ERROR_FOR_EMPTY_INPUT; - } + validate(entity, this.logger); try { await this.client.asCurrentUser.delete({ diff --git a/src/plugins/application_config/server/string_utils.test.ts b/src/plugins/application_config/server/string_utils.test.ts new file mode 100644 index 000000000000..30dda06a4f6e --- /dev/null +++ b/src/plugins/application_config/server/string_utils.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { validate } from './string_utils'; + +describe('application config string utils', () => { + it('returns smoothly when input is not empty', () => { + const logger = { + error: jest.fn(), + }; + + validate('abc', logger); + + expect(logger.error).not.toBeCalled(); + }); + + it('throws error when input is empty', () => { + const logger = { + error: jest.fn(), + }; + + expect(() => { + validate(' ', logger); + }).toThrowError('Input cannot be empty!'); + }); +}); diff --git a/src/plugins/application_config/server/string_utils.ts b/src/plugins/application_config/server/string_utils.ts index 9216fdf4f4b9..b8a67d7d0bc8 100644 --- a/src/plugins/application_config/server/string_utils.ts +++ b/src/plugins/application_config/server/string_utils.ts @@ -2,13 +2,23 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -export const ERROR_MESSSAGE_FOR_EMPTY_INPUT = 'Input cannot be empty!'; -export const ERROR_FOR_EMPTY_INPUT = new Error(ERROR_MESSSAGE_FOR_EMPTY_INPUT); -export function isEmpty(input: string): boolean { +import { Logger } from 'src/core/server'; + +const ERROR_MESSSAGE_FOR_EMPTY_INPUT = 'Input cannot be empty!'; +const ERROR_FOR_EMPTY_INPUT = new Error(ERROR_MESSSAGE_FOR_EMPTY_INPUT); + +function isEmpty(input: string): boolean { if (!input) { return true; } return !input.trim(); } + +export function validate(input: string, logger: Logger) { + if (isEmpty(input)) { + logger.error(ERROR_MESSSAGE_FOR_EMPTY_INPUT); + throw ERROR_FOR_EMPTY_INPUT; + } +} From 6b07cd8d54e6fb3afb83f8aeeb576ffd08eb4dae Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Fri, 1 Mar 2024 23:26:56 +0000 Subject: [PATCH 44/47] use trimmed versions Signed-off-by: Tianle Huang --- config/opensearch_dashboards.yml | 9 +----- .../server/opensearch_config_client.ts | 30 +++++++++---------- .../server/string_utils.test.ts | 20 +++++++++++-- .../application_config/server/string_utils.ts | 4 ++- 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 4dd647981bb5..4f86a8729a3e 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -29,13 +29,6 @@ # dashboards. OpenSearch Dashboards creates a new index if the index doesn't already exist. #opensearchDashboards.index: ".opensearch_dashboards" -# OpenSearch Dashboards uses an index in OpenSearch to store dynamic configurations. -# This shall be a different index from opensearchDashboards.index. -# opensearchDashboards.configIndex: ".opensearch_dashboards_config" - -# Set the value of this setting to true to enable plugin application config. By default it is disabled. -# application_config.enabled: false - # The default application to load. #opensearchDashboards.defaultAppId: "home" @@ -292,4 +285,4 @@ # opensearchDashboards.survey.url: "https://survey.opensearch.org" # Set the value of this setting to true to enable plugin augmentation on Dashboard -# vis_augmenter.pluginAugmentationEnabled: true +# vis_augmenter.pluginAugmentationEnabled: true \ No newline at end of file diff --git a/src/plugins/application_config/server/opensearch_config_client.ts b/src/plugins/application_config/server/opensearch_config_client.ts index bc3993945f87..b0dea85be2ac 100644 --- a/src/plugins/application_config/server/opensearch_config_client.ts +++ b/src/plugins/application_config/server/opensearch_config_client.ts @@ -24,17 +24,17 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { } async getEntityConfig(entity: string) { - validate(entity, this.logger); + const entityValidated = validate(entity, this.logger); try { const data = await this.client.asInternalUser.get({ index: this.configurationIndexName, - id: entity, + id: entityValidated, }); return data?.body?._source?.value || ''; } catch (e) { - const errorMessage = `Failed to get entity ${entity} due to error ${e}`; + const errorMessage = `Failed to get entity ${entityValidated} due to error ${e}`; this.logger.error(errorMessage); @@ -43,21 +43,21 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { } async updateEntityConfig(entity: string, newValue: string) { - validate(entity, this.logger); - validate(newValue, this.logger); + const entityValidated = validate(entity, this.logger); + const newValueValidated = validate(newValue, this.logger); try { await this.client.asCurrentUser.index({ index: this.configurationIndexName, - id: entity, + id: entityValidated, body: { - value: newValue, + value: newValueValidated, }, }); - return newValue; + return newValueValidated; } catch (e) { - const errorMessage = `Failed to update entity ${entity} with newValue ${newValue} due to error ${e}`; + const errorMessage = `Failed to update entity ${entityValidated} with newValue ${newValueValidated} due to error ${e}`; this.logger.error(errorMessage); @@ -66,27 +66,27 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { } async deleteEntityConfig(entity: string) { - validate(entity, this.logger); + const entityValidated = validate(entity, this.logger); try { await this.client.asCurrentUser.delete({ index: this.configurationIndexName, - id: entity, + id: entityValidated, }); - return entity; + return entityValidated; } catch (e) { if (e?.body?.error?.type === 'index_not_found_exception') { this.logger.info('Attemp to delete a not found index.'); - return entity; + return entityValidated; } if (e?.body?.result === 'not_found') { this.logger.info('Attemp to delete a not found document.'); - return entity; + return entityValidated; } - const errorMessage = `Failed to delete entity ${entity} due to error ${e}`; + const errorMessage = `Failed to delete entity ${entityValidated} due to error ${e}`; this.logger.error(errorMessage); diff --git a/src/plugins/application_config/server/string_utils.test.ts b/src/plugins/application_config/server/string_utils.test.ts index 30dda06a4f6e..2baf765a5bc0 100644 --- a/src/plugins/application_config/server/string_utils.test.ts +++ b/src/plugins/application_config/server/string_utils.test.ts @@ -6,13 +6,29 @@ import { validate } from './string_utils'; describe('application config string utils', () => { - it('returns smoothly when input is not empty', () => { + it('returns input when input is not empty and no prefix or suffix whitespaces', () => { const logger = { error: jest.fn(), }; - validate('abc', logger); + const input = 'abc'; + const validatedInput = validate(input, logger); + + expect(validatedInput).toBe(input); + expect(logger.error).not.toBeCalled(); + }); + + it('returns trimmed input when input is not empty and prefix or suffix whitespaces', () => { + const logger = { + error: jest.fn(), + }; + + const input = ' abc '; + + const validatedInput = validate(input, logger); + + expect(validatedInput).toBe('abc'); expect(logger.error).not.toBeCalled(); }); diff --git a/src/plugins/application_config/server/string_utils.ts b/src/plugins/application_config/server/string_utils.ts index b8a67d7d0bc8..34e9842b7b6d 100644 --- a/src/plugins/application_config/server/string_utils.ts +++ b/src/plugins/application_config/server/string_utils.ts @@ -16,9 +16,11 @@ function isEmpty(input: string): boolean { return !input.trim(); } -export function validate(input: string, logger: Logger) { +export function validate(input: string, logger: Logger): string { if (isEmpty(input)) { logger.error(ERROR_MESSSAGE_FOR_EMPTY_INPUT); throw ERROR_FOR_EMPTY_INPUT; } + + return input.trim(); } From 1ff31f62844c8db05a28dfb4efc6e185e9ccab20 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Fri, 1 Mar 2024 23:32:19 +0000 Subject: [PATCH 45/47] reword changelog entry Signed-off-by: Tianle Huang --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 548e394072b3..9e67c86790f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,7 +74,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple Datasource] Refactor client and legacy client to use authentication registry ([#5881](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5881)) - [Multiple Datasource] Improved error handling for the search API when a null value is passed for the dataSourceId ([#5882](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5882)) - [Multiple Datasource] Hide/Show authentication method in multi data source plugin based on configuration ([#5916](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5916)) -- Add support for dynamic application configurations ([#5855](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5855)) +- [[Dynamic Configurations] Add support for dynamic application configurations ([#5855](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5855)) ### 🐛 Bug Fixes From 73fd1c48ea87d861dc8f1d889451a3fd08e4d6eb Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Fri, 1 Mar 2024 23:43:01 +0000 Subject: [PATCH 46/47] readability Signed-off-by: Tianle Huang --- .../application_config/server/opensearch_config_client.ts | 4 ++-- src/plugins/application_config/server/plugin.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/application_config/server/opensearch_config_client.ts b/src/plugins/application_config/server/opensearch_config_client.ts index b0dea85be2ac..9103919c396f 100644 --- a/src/plugins/application_config/server/opensearch_config_client.ts +++ b/src/plugins/application_config/server/opensearch_config_client.ts @@ -14,11 +14,11 @@ export class OpenSearchConfigurationClient implements ConfigurationClient { private readonly logger: Logger; constructor( - openSearchClient: IScopedClusterClient, + scopedClusterClient: IScopedClusterClient, configurationIndexName: string, logger: Logger ) { - this.client = openSearchClient; + this.client = scopedClusterClient; this.configurationIndexName = configurationIndexName; this.logger = logger; } diff --git a/src/plugins/application_config/server/plugin.ts b/src/plugins/application_config/server/plugin.ts index 260d8ce0584b..d0bd2ab42270 100644 --- a/src/plugins/application_config/server/plugin.ts +++ b/src/plugins/application_config/server/plugin.ts @@ -50,13 +50,13 @@ export class ApplicationConfigPlugin this.configurationClient = configurationClient; } - private getConfigurationClient(configurationClient: IScopedClusterClient): ConfigurationClient { + private getConfigurationClient(scopedClusterClient: IScopedClusterClient): ConfigurationClient { if (this.configurationClient) { return this.configurationClient; } const openSearchConfigurationClient = new OpenSearchConfigurationClient( - configurationClient, + scopedClusterClient, this.configurationIndexName, this.logger ); From 4d80ad62c0e5b163d7ad99b9bbb0b8053b9f05e2 Mon Sep 17 00:00:00 2001 From: Tianle Huang Date: Fri, 1 Mar 2024 23:57:16 +0000 Subject: [PATCH 47/47] add back yml change Signed-off-by: Tianle Huang --- config/opensearch_dashboards.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 4f86a8729a3e..f1858ac35dde 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -29,6 +29,13 @@ # dashboards. OpenSearch Dashboards creates a new index if the index doesn't already exist. #opensearchDashboards.index: ".opensearch_dashboards" +# OpenSearch Dashboards uses an index in OpenSearch to store dynamic configurations. +# This shall be a different index from opensearchDashboards.index. +# opensearchDashboards.configIndex: ".opensearch_dashboards_config" + +# Set the value of this setting to true to enable plugin application config. By default it is disabled. +# application_config.enabled: false + # The default application to load. #opensearchDashboards.defaultAppId: "home"