Open huyaboo opened 6 days ago
@xinruiba @BionIT @zhyuanqi @zengyan-amazon @SuZhou-Joe @ruanyl @kavilla would you help to review
Thanks @huyaboo for this RFC!
Based on this RFC, we are planning to create a dynamicConfigService in core-service to handle all core-services and plugin configurations. This approach makes sense. Looking ahead, to make core-service and plugin configurations more flexible, I see an opportunity for improvement.
What if we create a dynamicClientConfig service like you have proposed, and at the same time, we also create a ConfigurationPlugin?
The dynamicClientConfig would be responsible solely for core service configurations, while the ConfigurationPlugin would manage the configurations for all other plugins.
This approach offers the following benefits:
What do you think?
Thanks @huyaboo ! Like the idea, a few questions and things to consider and happy to hear your thoughts:
Integrate with dashboard admin
It seems plugins can only get the client through context, I do not know if that can work with dashboard admin implementation because we try to detect if user is a dashboard admin in postAuth
lifeCycle. @yubonluo could you please help to confirm that?
Performance concern
As the dynamicConfigService will be a global change and plugin will call the client method every time there is a incoming request. Have we consider a cache solution to mitigate the issue?
Integrate with dashboard admin
It seems plugins can only get the client through context, I do not know if that can work with dashboard admin implementation because we try to detect if user is a dashboard admin in
postAuth
lifeCycle. @yubonluo could you please help to confirm that?Performance concern
As the dynamicConfigService will be a global change and plugin will call the client method every time there is a incoming request. Have we consider a cache solution to mitigate the issue?
Thanks @huyaboo ~
As mentioned above, we need to determine whether the users/groups is dashboard admin through request (code). If possible, cloud you provide a service to enable plugins to register, and a method to get the client
through request
? Here is an example:
private setupPermission(core: CoreSetup) {
core.http.registerOnPostAuth(async (request, response, toolkit) => {
const { groups = [], users = [] } = this.permissionControl!.getPrincipalsFromRequest(request));
const client = dynamicService.getClient(request);
const [configGroups, configUsers] = await client.getConfig(this.globalConfig$);
updateDashboardAdminStateForRequest(request, groups, users, configGroups, configUsers);
return toolkit.next();
});
}
Thanks @huyaboo for this RFC!
Based on this RFC, we are planning to create a dynamicConfigService in core-service to handle all core-services and plugin configurations. This approach makes sense. Looking ahead, to make core-service and plugin configurations more flexible, I see an opportunity for improvement.
What if we create a dynamicClientConfig service like you have proposed, and at the same time, we also create a ConfigurationPlugin?
The dynamicClientConfig would be responsible solely for core service configurations, while the ConfigurationPlugin would manage the configurations for all other plugins.
This approach offers the following benefits:
1. Decouples configurations for core services and plugins. 2. Provides the potential for more customization in plugin configurations.
What do you think?
@xinruiba
The issue with the ConfigurationPlugin
is that it does not have access to all the configs without a core service providing the whole config. There could be some concerns with dynamicConfigService
exposing the entire config.
- Provides the potential for more customization in plugin configurations.
I am curious what do you mean by this?
Thanks @huyaboo for this RFC! Based on this RFC, we are planning to create a dynamicConfigService in core-service to handle all core-services and plugin configurations. This approach makes sense. Looking ahead, to make core-service and plugin configurations more flexible, I see an opportunity for improvement. What if we create a dynamicClientConfig service like you have proposed, and at the same time, we also create a ConfigurationPlugin? The dynamicClientConfig would be responsible solely for core service configurations, while the ConfigurationPlugin would manage the configurations for all other plugins. This approach offers the following benefits:
1. Decouples configurations for core services and plugins. 2. Provides the potential for more customization in plugin configurations.
What do you think?
@xinruiba The issue with the
ConfigurationPlugin
is that it does not have access to all the configs without a core service providing the whole config. There could be some concerns withdynamicConfigService
exposing the entire config.
- Provides the potential for more customization in plugin configurations.
I am curious what do you mean by this?
Thanks for the response~ It refers to
ConfigurationPlugin
ConfigurationPlugin
to dynamic handle plugin's configurations Thanks @huyaboo ! Like the idea, a few questions and things to consider and happy to hear your thoughts:
1. for the config store, what the index would look like and how we plan to address deprecated configs or any changes on the config path that may lead to incorrect mapping of the new config schema vs the ones stored in the config store 2. I think the static config and dynamic config would have some overlap and the dynamic config would take precedence, is it correct? 3. since the client for the dynamic config is read only, do we plan to expose setters somewhere to set the values into config store? 4. from a user perspective, we might need to expose a few interfaces/APIs for user to CRUDL the dynamic configs, otherwise
Thanks @BionIT
dynamicConfigBlob
will override the old config blobInternalClient
, a client that adds create/delete operations. These operations will only be invoked via the CRUD routes. registerRoutesAndHandlers()
method is within the dynamicConfigService
. This function will register CRUD routes (/config/create
, /config/delete
, etc.) and in the future, we can have a management page that calls these routes to change configs directly from UIHi Huy. Thanks for the purpose. So the purpose of dynamic configuration service is to enable config change without a restart. I have some questions here.
@yubonluo
Not sure if we can include the client using the request
, but we may be able to expose getStartServices()
in the DynamicConfigServiceSetup
, a method that returns a promise of the start services (currently, it's only for the InternalDynamicConfigServiceSetup
). Since your use case depends on a postAuth
handler and the asyncLocalStore
will be ready on preAuth
, it may be possible to do something like the following:
private setupPermission(core: CoreSetup) {
core.http.registerOnPostAuth(async (request, response, toolkit) => {
const { groups = [], users = [] } = this.permissionControl!.getPrincipalsFromRequest(request));
// Get the config store configs
const startServices: DynamicConfigServiceStart = await core.dynamicConfigService.getStartServices();
const client = startServices.getClient();
const store = startServices.getAsyncLocalStore();
const updatedWorkspaceConfigObject = await client.getConfig({ name: 'workspace' }, { asyncLocalStoreContext: store! });
/**
Assume the following config schema
schema.object({
enabled: schema.boolean({ defaultValue: false }),
configGroups: schema.arrayOf( ... ),
configUsers: schema.arrayOf( ... ),
});
*/
const { configGroups, configUsers } = updatedWorkspaceConfigObject;
updateDashboardAdminStateForRequest(request, groups, users, configGroups, configUsers);
return toolkit.next();
});
}
As the dynamicConfigService will be a global change and plugin will call the client method every time there is a incoming request. Have we consider a cache solution to mitigate the issue?
@SuZhou-Joe
Cache will have to be baked into the config store client implementation. For the default OpenSearch
config store client, the configs will be cached by their configPath
with a refeshInterval
to clear the cache. The reason the cache implementation has to be implemented within the client is that different clients can be cached by different values.
Hi Huy. Thanks for the purpose. So the purpose of dynamic configuration service is to enable config change without a restart. I have some questions here.
1. When customer provides a incorrect config, how do we handle it. Will it crash the request call? 2. How will access to the dynamic configuration store be secured? 3. When the external config store is unavailable or a network issue, what will happen? 4. How frequently will configurations be fetched from the external store, and is there a caching mechanism in place? 5. I am a bit confused on registerRoutesAndHandlers() method. So for each request, we use dynamicConfigService to fetch values from the external config service?
@zhyuanqi
create
operation will perform a validation check so it will reject invalid entries. We can leverage the schema
s registered when plugins are discovered by calling schema.validate()
on the updated config entryyml
file if necessary.registerRoutesAndHandlers()
is to register CRUD routes (/config/create
, /config/delete
) and create the async local store. This store will help group together relevant headers into one place and prevent props drilling.
Proposal
Currently, all configs are statically determined via the config schema
defaultValues
property and theopensearch_dashboards.yml
. This provides a centralized repository of config values but a major limitation is the inability to change configs without a server restart. For instance, if the admin needs to set theserver.maxPayloadBytes
, they would need to go into the config file, edit it, and restart Dashboards. Additionally, the existing config service does not allow for per-request config values. Say an admin needs to set different CSP rules based on the request. This is not feasible with the current model. The proposal instead is for a new core servicedynamicConfigService
, an extension of the existingconfigService
, to expose a read-only dynamic config client in theRequestHandlerContext
for plugins to consume and obtain configs from an external config store. For core services, the client will be exposed in thedynamicConfigService.start()
method.Background
Currently, the configs are handled by the
ConfigService
in @osd-config, which takes thedefaultValues
defined in a plugin/core service config schema and overrides them with the config values found inopensearch_dashboards.yml
. Takecsp
configs for example. Its config schema is the following:This means that by default, the
csp
config will be the following:If we define the following config in the
opensearch_dashboards.yml
file:The final config exposed to all core services and plugins will be
For plugins, the config schema can be found in
config.ts
.Terminology
As to not introduce confusion later in the doc, the below terms will be used
configService
This is the existing OSD config service, which can be found at @osd-config.
dynamicConfigService
This is an extension of the
configService
, which enables plugins/core services to define config values in a config store and fetch dynamic values for a subset of configs.config store
The external store which contains all configurable OSD configs. The default store will be an OpenSearch index
.dynamic_opensearch_dashboards
config schema
This is the
schema
type object used to define the structure, value types, and default values of a core service/plugin configconfig.ts
. Takehome
plugin for example; its config schema is the followingconfig object
This is a generic object which contains all the values of a given
configPath
. This is the return value for thedynamicConfigService
clientgetConfig
configPath
This is the key value that uniquely identifies configs for all core services and plugins
path
field of the config schema.configPath
isserver
, which is found in the config schemaconfigPath
found in the plugin manifest file (opensearch_dashboards.json
). If this is not specified in the manifest, theplugin id
should be usedasync local storage
Async local storage are Node provided classes that enable necessary request context to be exposed throughout a request lifecycle. This is needed to call the
dynamicConfigService
client since different requests can fetch different config values.Out of Scope
Creating a management page
To access/update config values from the Dashboards UI, a page should be created. However, the specifics of such a page are outside the scope of this RFC.
Determining which configs can be dynamic
This RFC seeks to establish a framework for setting configs as dynamic. A general guideline for what constitutes a dynamic config is as follows:
setup()
and cannot be modified are not dynamicOpenSearchService
configs cannot be made dynamic as the earliest time theDynamicConfigService
can start is afterOpenSearchService
finishes starting (so that the OpenSearch client can be created)Plugin owners and core service owners should determine which of their configs can be made dynamic.
High-Level Approach
As stated in the Proposal, there should be a new core service
DynamicConfigService
, which is an extension of the existingConfigService
. The configs taken from theDynamicConfigService
will override theConfigService
configs. In essence, what this means is there is a priority on the config values:opensearch_dashboards.yml
will be usedopensearch_dashboards.yml
, the config schemadefaultValue
will be usedOn initialization/before
setup()
The
DynamicConfigService
instance will need to followConfigService
initialization in order to consume its public methods. It should also be exposed toCoreContext
so other core services can consume the service easily.In particular, the
DynamicConfigService
also requires plugins and core services to have their schemas registered by callingsetSchema()
to leverage existing schema validation logic. WhileConfigService
already does this, its schema map is private.setup()
The
DynamicConfigService
setup should happen right after theConfigService
sets up. There are several reasons why:ConfigService
onsetup()
will discover all plugin configs and construct a plugin dependency graph. To avoid duplicate efforts, theDynamicConfigService
should not concern itself with thisConfigService
performs a validation step in itssetup()
that ensures config schemas are well formed, have unique namespaces, andopensearch_dashboards.yml
values are valid and are validconfigPaths
When the
setup()
finishes, theDynamicConfigService
will expose the following methods:Between
setup()
andstart()
To make configs configurable to the admin, CRUDL routes have to be registered. However, if the HTTP route registration is handled in
setup()
stage, there will be a circular dependency sincehttpServer
now depends on thedynamicConfigService
and thedynamicConfigService
depends onhttpService
. As a workaround, thedynamicConfigService
will have an intermediate stepregisterRoutesAndHandlers()
(tentatively named) to register http routes and other handlers after http finishes setting up. This serves two purposes: to register http routes and the async local store in thepreAuth
server extension. This is what enables a per request context to be feasible:start()
In
start()
, the service will create the readonly client for other core services and plugins to consume. If a custom client factory was provided, the custom client will be created. Otherwise, the default OpenSearch client will be created. Whenstart()
, finishes the service will expose the following methods:DynamicConfigService
should be the first service to start so that other core services can use its client.Example usage
For plugins
For core services
Challenges
Scoping the client for plugins
Currently, all configs are isolated per plugin. This means that plugin A cannot access plugin B without B explicitly exposing its configs. How
ConfigService
handled config object scoping was leveraging thePluginInitializerContext
to expose a create function that would create the scoped down client. Since plugins can register their own custom config store client in thesetup()
stage and the client can only be used in the request context, the initializer context cannot be used to create the scoped down client.Core service limitations
While core services can have configs, they cannot be disabled meaning
DynamicConfigService
will exist as a permanent service. This can cause excess network calls to OpenSearch if a Dashboards instance does not use this feature. A workaround could be to define some "dummy" client that only returns the configs from theConfigService
and set that as the client if the service is "disabled".Alternatives
A plugin was initially considered to make the service easy to enable/disable and prevent an extensive core change. However, plugins cannot access all configs because configs are isolated per plugin. Additionally, core services cannot be changed as core services cannot depend on a plugin (circular dependency).