Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Make config-schema extensible for handling of unknown fields #156214

Merged
merged 12 commits into from
May 5, 2023

Conversation

mikecote
Copy link
Contributor

@mikecote mikecote commented Apr 28, 2023

Related issue #155764.

In this PR, I'm adding an extendsDeep function to the schema object. This feature allows you to create a copy of an existing schema definition and recursively modify options without mutating them. With extendsDeep, you can specify whether unknown attributes on objects should be allowed, forbidden or ignored.

This new function is particularly useful for alerting scenarios where we need to drop unknown fields when reading from Elasticsearch without modifying the schema object. Since we don't control the schema definition in some areas, extendsDeep provides a convenient way to set the unknowns option to all objects recursively. By doing so, we can validate and drop unknown properties using the same defined schema, just with unknowns: forbid extension.

Usage:

// Single, shared type definition
const type = schema.object({ foo: schema.string() });

// Drop unknown fields (bar in this case)
const savedObject = { foo: 'test', bar: 'test' };
const ignoreSchema = type.extendsDeep({ unknowns: 'ignore' });
ignoreSchema.validate(savedObject);

// Prevent unknown fields (bar in this case)
const soToUpdate = { foo: 'test', bar: 'test' };
const forbidSchema = type.extendsDeep({ unknowns: 'forbid' });
forbidSchema.validate(soToUpdate);

@mikecote mikecote self-assigned this Apr 28, 2023
@mikecote
Copy link
Contributor Author

mikecote commented May 1, 2023

@elasticmachine merge upstream

@mikecote
Copy link
Contributor Author

mikecote commented May 2, 2023

@elastic/kibana-core I would be curious to hear your thoughts about this approach. Would you benefit from this elsewhere?

Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

I think the approach looks fine to me.

My questions and concerns in comments.

packages/kbn-config-schema/src/types/array_type.test.ts Outdated Show resolved Hide resolved
Comment on lines +184 to +187
public extendsDeep(options: ExtendsDeepOptions) {
const extendedProps = Object.entries(this.props).reduce((memo, [key, value]) => {
if (value !== null && value !== undefined) {
return {
Copy link
Contributor

Choose a reason for hiding this comment

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

(thinking out loud here) I'm overall fine with the recursive approach, I can understand that it would be complicated / tedious to manually redefine everything.

Now, if this extendsDeep approach looks fine to me as an internal API to perform the mutation, I wonder if that's what we want as the public API.

I was more thinking of something like:

const type = schema.object({ foo: schema.string() });
const savedObject = { foo: 'test', bar: 'test' };
// ... later
type.validate(savedObject, { unknowns: 'ignore' });

Now, technically, internally we don't want to create a new schema everytime validate is called with this option. But if we know that we will only be exposing this single 'schema mutation' option to validate, we could store the mutated schema internally.

However, the validate signature is already crowded with 'useless' stuff as second and third parameters:

public validate(value: any, context: Record<string, any> = {}, namespace?: string): V {

So I don't think it would be that easy to adapt it to have a good DX, as it would look like

type.validate(savedObject, {}, undefined, { unknowns: 'ignore' });

So in summary, your approach is probably the most pragmatic one.

Comment on lines +17 to +19
private readonly keyType: Type<K>;
private readonly valueType: Type<V>;
private readonly options: RecordOfOptions<K, V>;
Copy link
Contributor

Choose a reason for hiding this comment

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

We're now storing all the constructor options on all of our type classes to be able to clone them during extendsDeep.

I'm thinking about GC / memory consumption here. I think it's fine, given the various props were referenced by lower schemas anyway, so we're probably not introducing any significant impact to the memory consumption of schemas here, but ideally someone else would also confirm that.

packages/kbn-config-schema/src/types/type.ts Outdated Show resolved Hide resolved
packages/kbn-config-schema/src/types/type.ts Show resolved Hide resolved
Comment on lines +414 to +415
const result = type
.extendsDeep({ unknowns: 'allow' })
Copy link
Contributor

Choose a reason for hiding this comment

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

(unrelated to this line) I wonder if that extendsDeep will be sufficient covering our 'eviction schema' needs. Like, isn't there going to be scenarios were teams would need a more finely grained configuration, e.g ignore for some props, allow for some others and so on?

I guess in that case, they can still just manually redefine the schema (if such scenario even make sense)

Copy link
Contributor Author

@mikecote mikecote May 3, 2023

Choose a reason for hiding this comment

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

I can only explain from how I plan to use this but feel free to propose changes. In the example of Task Manager, we plan to evict unknown properties when reading from the task.state object while preventing tasks from running if task.params contains unknown properties (ex: properties created by a newer Kibana version).

We would build two different task schema objects to satisfy the needs:

  • schema for read would ignore unknowns for task.state while forbid unknowns for task.params
  • schema for write would forbid unknowns for task.state and task.params

Since we have control of the params schema by task type and the state schema by task type, we can intercept those and only apply .extendsDeep(...) as needed before building the task read/write schema for a given task type.

@mikecote
Copy link
Contributor Author

mikecote commented May 4, 2023

@elasticmachine merge upstream

@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/config-schema 127 133 +6

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
aiops 792.4KB 794.1KB +1.6KB

Public APIs missing exports

Total count of every type that is part of your API that should be exported but is not. This will cause broken links in the API documentation system. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats exports for more detailed information.

id before after diff
@kbn/config-schema 17 18 +1
Unknown metric groups

API count

id before after diff
@kbn/config-schema 129 135 +6

ESLint disabled line counts

id before after diff
enterpriseSearch 19 21 +2
securitySolution 398 401 +3
total +5

Total ESLint disabled count

id before after diff
enterpriseSearch 20 22 +2
securitySolution 478 481 +3
total +5

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

cc @mikecote

@mikecote mikecote changed the title [POC] Making config-schema extensible for handling of unknown fields Make config-schema extensible for handling of unknown fields May 4, 2023
@mikecote mikecote marked this pull request as ready for review May 4, 2023 17:42
@mikecote mikecote requested a review from a team as a code owner May 4, 2023 17:42
@mikecote mikecote added Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc release_note:skip Skip the PR/issue when compiling release notes v8.9.0 labels May 4, 2023
Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

LGTM

@mikecote mikecote merged commit 1cab306 into elastic:main May 5, 2023
@kibanamachine kibanamachine added the backport:skip This commit does not require backporting label May 5, 2023
smith pushed a commit to smith/kibana that referenced this pull request May 5, 2023
…#156214)

Related issue elastic#155764.

In this POC, I'm adding an `extendsDeep` function to the schema object.
This feature allows you to create a copy of an existing schema
definition and recursively modify options without mutating them. With
`extendsDeep`, you can specify whether unknown attributes on objects
should be allowed, forbidden or ignored.

This new function is particularly useful for alerting scenarios where we
need to drop unknown fields when reading from Elasticsearch without
modifying the schema object. Since we don't control the schema
definition in some areas, `extendsDeep` provides a convenient way to set
the `unknowns` option to all objects recursively. By doing so, we can
validate and drop unknown properties using the same defined schema, just
with `unknowns: forbid` extension.

Usage:
```
// Single, shared type definition
const type = schema.object({ foo: schema.string() });

// Drop unknown fields (bar in this case)
const savedObject = { foo: 'test', bar: 'test' };
const ignoreSchema = type.extendsDeep({ unknowns: 'ignore' });
ignoreSchema.validate(savedObject);

// Prevent unknown fields (bar in this case)
const soToUpdate = { foo: 'test', bar: 'test' };
const forbidSchema = type.extendsDeep({ unknowns: 'forbid' });
forbidSchema.validate(soToUpdate);
```

---------

Co-authored-by: Kibana Machine <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v8.9.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants