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

[ZDT] DocumentMigrator: support higher version documents #157895

Merged
merged 21 commits into from
May 23, 2023

Conversation

pgayvallet
Copy link
Contributor

@pgayvallet pgayvallet commented May 16, 2023

Summary

Part of #150312
(next steps depend on #153117)

This PR does two things:

  • introduce the concept of version persistence schema
  • adapt the document migrator to support downward migrations for documents of an higher version.

In the follow-up, we will then update the calls from the SOR to the document migrator to allow downward conversions when we're using the ZDT migration algorithm (which requires #153117 to be merged)

Model version persistence schema.

(This is what has also been named 'eviction schema' or 'known fields schema'.)

A new SavedObjectsModelVersion.schemas.backwardConversion property was added to the model version definition.

This 'schema' can either be an arbitrary function, or a schema.object from @kbn/config-schema

type SavedObjectModelVersionBackwardConversionSchema<
  InAttrs = unknown,
  OutAttrs = unknown
> = ObjectType | SavedObjectModelVersionBackwardConversionFn<InAttrs, OutAttrs>;

When specified for a version, the document's attributes will go thought this schema during down conversions by the document migrator.

Adapt the document migrator to support downward migrations for documents of an higher version.

Add an allowDowngrade option to DocumentMigrator.migrate and KibanaMigrator.migrateDocument. When this option is set to true, the document migration will accept to 'downgrade' the document if necessary, instead of throwing an error as done when the option is false or unspecified (which was the only behavior prior to this PR's changes)

@pgayvallet pgayvallet 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 16, 2023
Copy link
Contributor Author

@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.

Self-review

Note: now that #153117 is merged, I could do the next steps (leveraging the change from the SOR) in the same PR. However I think it makes sense to validate the document migrator changes / API design first before going further, which is why I'm planning on doing the rest in a distinct PR.

*
* See {@link SavedObjectModelVersionBackwardConversionSchema} for more info.
*/
backwardConversion?: SavedObjectModelVersionBackwardConversionSchema;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know naming is hard, but it was even harder here. I'm still really not sure how we should name that thing.

I started with persistenceSchema, but it wasn't explicit enough, so I switched to backwardPersistence, higherVersionConversion and then backwardConversion... But nothing seems outstandingly correct here.

If anyone has a better idea, please voice your opinion...

Copy link
Member

Choose a reason for hiding this comment

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

Throwing ideas: versionedModelSchema?

Copy link
Contributor

Choose a reason for hiding this comment

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

reverseConversion?

Comment on lines 73 to 76
export type SavedObjectModelVersionBackwardConversionSchema<
InAttrs = unknown,
OutAttrs = unknown
> = ObjectType | SavedObjectModelVersionBackwardConversionFn<InAttrs, OutAttrs>;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can probably improve this later (e.g with helper methods / factory to build the validation function from another shape of schema or something), but I just KISSed for now. The schema can either be a schema.object of the properties of the document, or a plain JS function taking the attributes as input and returning the filtered / processed attributes as output.

*/
export interface SavedObjectsModelVersionSchemaDefinitions {
create?: SavedObjectModelVersionCreateSchema;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

NIT: I removed the created schema from SavedObjectsModelVersionSchemaDefinitions given it's unused for now (and now that we have an other, used, schema on this type)

Comment on lines 176 to 188
const typeMigrations = this.migrations[doc.type];
if (downgradeRequired(doc, typeMigrations?.latestVersion ?? {})) {
const currentVersion = doc.typeMigrationVersion ?? doc.migrationVersion?.[doc.type];
const latestVersion = this.migrations[doc.type].latestVersion[TransformType.Migrate];
if (!allowDowngrade) {
throw Boom.badData(
`Document "${doc.id}" belongs to a more recent version of Kibana [${currentVersion}] when the last known version is [${latestVersion}].`
);
}
return this.transformDown(doc, { targetTypeVersion: latestVersion! });
} else {
return this.transformUp(doc, { convertNamespaceTypes, allowDowngrade });
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

One of the root change of the PR: DocumentMigrator.migrate (and the underlying .transform) now accepts a allowDowngrade option.

If true, the document will go through the downgrade pipeline instead of throwing an error if the provided document is at an higher version than the last knows by the system.

Comment on lines +1381 to +1385
describe('down migration', () => {
it('accepts to downgrade the document if `allowDowngrade` is true', () => {
const registry = createRegistry({});

const fooType = createType({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

NIT: only smoke testing in this file, as the actual coverage is done in the down pipeline's tests

Comment on lines 54 to 57
if (!transform.transformDown) {
if (this.ignoreMissingTransforms) {
continue;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I decided to reuse the existing downgrade pipeline, even if, realistically given our latest decisions regarding down migrations, we could just rewrite it to only apply the version schema if present.

The reasoning was that

  1. the system is already in place and tested
  2. we may eventually need such downward transformations
  3. the perf impact is negligible given we will just not apply down transforms that are not here.

The only thing is that I had to add this ignoreMissingTransforms option to change the behavior of the down pipeline to now throw when encountering versions that did not register down transformations.

Copy link
Contributor

Choose a reason for hiding this comment

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

we could just rewrite it to only apply the version schema if present

So this is the version where the down pipeline will throw, but we added a flag. What's the reasoning for not just changing how the implementation works since we are not using ignoreMissingTransforms: false outside of tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, you are right. I will remove the option and just keep the only used behavior

Comment on lines +137 to +144
private applyVersionSchema(doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc {
const targetVersion = this.targetTypeVersion;
const versionSchema = this.typeTransforms.versionSchemas[targetVersion];
if (versionSchema) {
return versionSchema(doc);
}
return doc;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where the actual version schema is applied.

Note that, compare to the existing create schema (not yet ported to model versions), here we will only use the schema matching the exact version we're downgrading to. trying to find and use an older version doesn't make sense for such 'persistence shape' schema I think.

Comment on lines +270 to +274
public migrateDocument(
doc: SavedObjectUnsanitizedDoc,
{ allowDowngrade = false }: MigrateDocumentOptions = {}
): SavedObjectUnsanitizedDoc {
return this.documentMigrator.migrate(doc, { allowDowngrade });
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also added the allowDowngrade option to IKibanaMigrator.migrateDocument, as this is the interface the SOR is using to access the actual document migrator.

@pgayvallet pgayvallet marked this pull request as ready for review May 22, 2023 12:23
@pgayvallet pgayvallet requested a review from a team as a code owner May 22, 2023 12:23
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-core (Team:Core)

Copy link
Contributor

@jloleysens jloleysens left a comment

Choose a reason for hiding this comment

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

Still reviewing just left a comment on what I think is a mistake

@@ -48,7 +48,7 @@ export function buildActiveMigrations({
referenceTransforms,
});

if (typeTransforms.transforms.length) {
if (!typeTransforms.transforms.length && !Object.keys(typeTransforms.versionSchemas).length) {
Copy link
Contributor

@jloleysens jloleysens May 22, 2023

Choose a reason for hiding this comment

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

Not quite sure I follow the logic here:

(1) no transforms and (2) no version schemas then we add it to active migration map.

I would have thought we want (1) and (or?) (2) but I may be missing something.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, you're right, I just messed up when I rebased from main. This is what is causing the whole PR to explode btw, thanks :)

Copy link
Contributor

@jloleysens jloleysens left a comment

Choose a reason for hiding this comment

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

Great work @pgayvallet , I left one non-blocker question about the thinking behind schemas, but I expect this is just me missing some context.

}

private transform(
private transformUp(
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like allowDowngrade is being passed in here but not used. Shall we create an inline type for the options object in this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice catch, will fix

*/
export type SavedObjectModelVersionCreateSchema = ObjectType;
export interface SavedObjectsModelVersionSchemaDefinitions {
Copy link
Contributor

Choose a reason for hiding this comment

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

What other (eviction) schemas would we want to define for a model version?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

eviction? None.

We will need to implement the create schema at least, but it's a more "traditional" validation schema

Comment on lines 49 to 50
* Note that These conversion mechanism shouldn't assert the data itself, only strip unknown fields to convert
* the document to the *shape* of the document at the given version.
Copy link
Contributor

Choose a reason for hiding this comment

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

By shape we also mean format of the fields themselves, the idea is not to transform values -- I am a little concerned that allowing an arbitrary function opens the door for something like that. But I think this doc comment is good enough guidance.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I agree, we will need to be fairly careful when reviewing PRs adding such functions.

But given we still don't know what the best way to define these schema is, I needed something low level, which is therefor very permissive...


export type ConvertedSchema = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc;

export const convertModelVersionBackwardConversionSchema = (
Copy link
Contributor

Choose a reason for hiding this comment

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

One other idea:

getSchemaConversionFunction

Comment on lines 54 to 57
if (!transform.transformDown) {
if (this.ignoreMissingTransforms) {
continue;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

we could just rewrite it to only apply the version schema if present

So this is the version where the down pipeline will throw, but we added a flag. What's the reasoning for not just changing how the implementation works since we are not using ignoreMissingTransforms: false outside of tests?

Copy link
Member

@afharo afharo left a comment

Choose a reason for hiding this comment

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

Great job! I added 2 comments I'd like to clarify before approving 😇

if (!docTypeVersion || !latestMigrationVersion) {
return false;
}
return Semver.gt(docTypeVersion, latestMigrationVersion);
Copy link
Member

Choose a reason for hiding this comment

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

Q: is this only for V2 versions? I thought model versions didn't look like Semver, do they?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're using a compatibility format so that both stack versions (used by old migrations) and model versions are compatible and can share the same logic

/**
* Represents the virtual version of a given SO type.
* The virtual version is a compatibility format between the old
* migration system's versioning, based on the stack version, and the new model versioning.
*
* A virtual version is a plain semver version. Depending on its major version value, the
* underlying version can be the following:
* - Major < 10: Old migrations system (stack versions), using the equivalent value (e.g `8.7.0` => migration version `8.7.0`)
* - Major == 10: Model versions, using the `10.{modelVersion}.0` format (e.g `10.3.0` => model version 3)
*/
export type VirtualVersion = string;

*
* See {@link SavedObjectModelVersionBackwardConversionSchema} for more info.
*/
backwardConversion?: SavedObjectModelVersionBackwardConversionSchema;
Copy link
Member

Choose a reason for hiding this comment

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

Throwing ideas: versionedModelSchema?

@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/core-saved-objects-base-server-internal 57 59 +2
@kbn/core-saved-objects-migration-server-internal 88 86 -2
@kbn/core-saved-objects-server 117 116 -1
total -1

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/core-saved-objects-base-server-internal 9 11 +2
@kbn/core-saved-objects-migration-server-internal 44 45 +1
total +3
Unknown metric groups

API count

id before after diff
@kbn/core-saved-objects-base-server-internal 83 85 +2
@kbn/core-saved-objects-migration-server-internal 122 120 -2
@kbn/core-saved-objects-server 531 533 +2
total +2

ESLint disabled line counts

id before after diff
enterpriseSearch 19 21 +2
securitySolution 399 403 +4
total +6

Total ESLint disabled count

id before after diff
enterpriseSearch 20 22 +2
securitySolution 479 483 +4
total +6

History

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

@pgayvallet pgayvallet merged commit c24dc35 into elastic:main May 23, 2023
@kibanamachine kibanamachine added the backport:skip This commit does not require backporting label May 23, 2023
delanni pushed a commit to delanni/kibana that referenced this pull request May 25, 2023
)

## Summary

Part of elastic#150312
(next steps depend on elastic#153117)

**This PR does two things:**
- introduce the concept of version persistence schema
- adapt the document migrator to support downward migrations for
documents of an higher version.

In the follow-up, we will then update the calls from the SOR to the
document migrator to allow downward conversions when we're using the ZDT
migration algorithm (which requires
elastic#153117 to be merged)

### Model version persistence schema.

*(This is what has also been named 'eviction schema' or 'known fields
schema'.)*

A new `SavedObjectsModelVersion.schemas.backwardConversion` property was
added to the model version definition.

This 'schema' can either be an arbitrary function, or a `schema.object`
from `@kbn/config-schema`

```ts
type SavedObjectModelVersionBackwardConversionSchema<
  InAttrs = unknown,
  OutAttrs = unknown
> = ObjectType | SavedObjectModelVersionBackwardConversionFn<InAttrs, OutAttrs>;
```

When specified for a version, the document's attributes will go thought
this schema during down conversions by the document migrator.

### Adapt the document migrator to support downward migrations for
documents of an higher version.

Add an `allowDowngrade` option to `DocumentMigrator.migrate` and
`KibanaMigrator.migrateDocument`. When this option is set to `true`, the
document migration will accept to 'downgrade' the document if necessary,
instead of throwing an error as done when the option is `false` or
unspecified (which was the only behavior prior to this PR's changes)

---------

Co-authored-by: kibanamachine <[email protected]>
pgayvallet added a commit that referenced this pull request May 30, 2023
#158251)

## Summary

Follow-up of #157895
Part of #150312

Adapt the SOR to accept retrieving documents on higher versions from the
persistence, and convert them back to the latest knows version (using
the version schema feature added in
#157895).
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 Feature:Saved Objects 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.

7 participants