mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 10:23:14 -04:00
## Summary
Added documentation explaining how SO migrations on serverless work
## Preview
<img width="741" alt="Screenshot 2024-03-22 at 16 15 13"
src="2217c01f
-8447-4f22-a782-a07ff221aa42">
304 lines
No EOL
11 KiB
Text
304 lines
No EOL
11 KiB
Text
---
|
|
id: kibDevTutorialSavedObject
|
|
slug: /kibana-dev-docs/tutorials/saved-objects
|
|
title: Register a new saved object type
|
|
description: Learn how to register a new saved object type.
|
|
date: 2021-02-05
|
|
tags: ['kibana','onboarding', 'dev', 'architecture', 'tutorials']
|
|
---
|
|
|
|
Saved Object type definitions should be defined in their own `my_plugin/server/saved_objects` directory.
|
|
|
|
The folder should contain a file per type, named after the snake_case name of the type, and an index.ts file exporting all the types.
|
|
|
|
**src/plugins/my_plugin/server/saved_objects/dashboard_visualization.ts**
|
|
|
|
```ts
|
|
import { SavedObjectsType } from 'src/core/server';
|
|
|
|
export const dashboardVisualization: SavedObjectsType = {
|
|
name: 'dashboard_visualization', [1]
|
|
hidden: true,
|
|
switchToModelVersionAt: '8.10.0', // this is the default, feel free to omit it unless you intend to switch to using model versions before 8.10.0
|
|
namespaceType: 'multiple-isolated', [2]
|
|
mappings: {
|
|
dynamic: false,
|
|
properties: {
|
|
description: {
|
|
type: 'text',
|
|
},
|
|
hits: {
|
|
type: 'integer',
|
|
},
|
|
},
|
|
},
|
|
modelVersions: {
|
|
1: dashboardVisualizationModelVersionV1,
|
|
2: dashboardVisualizationModelVersionV2,
|
|
},
|
|
};
|
|
```
|
|
|
|
[1] Since the name of a Saved Object type forms part of the URL path for the public Saved Objects HTTP API,
|
|
these should follow our API URL path convention and always be written in snake case.
|
|
|
|
[2] This field determines "space behavior" -- whether these objects can exist in one space, multiple spaces, or all spaces. This value means
|
|
that objects of this type can only exist in a single space. See
|
|
<DocLink id="kibDevDocsSavedObjectsIntro" section="sharing-saved-objects" text="Sharing Saved Objects"/> for more information.
|
|
|
|
**src/plugins/my_plugin/server/saved_objects/index.ts**
|
|
|
|
```ts
|
|
export { dashboardVisualization } from './dashboard_visualization';
|
|
export { dashboard } from './dashboard';
|
|
```
|
|
|
|
**src/plugins/my_plugin/server/plugin.ts**
|
|
|
|
```ts
|
|
import { dashboard, dashboardVisualization } from './saved_objects';
|
|
|
|
export class MyPlugin implements Plugin {
|
|
setup({ savedObjects }) {
|
|
savedObjects.registerType(dashboard);
|
|
savedObjects.registerType(dashboardVisualization);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Mappings
|
|
|
|
Each Saved Object type can define its own Elasticsearch field mappings. Because multiple Saved Object
|
|
types can share the same index, mappings defined by a type will be nested under a top-level field that matches the type name.
|
|
|
|
For example, the mappings defined by the dashboard_visualization Saved Object type:
|
|
|
|
**src/plugins/my_plugin/server/saved_objects/dashboard_visualization.ts**
|
|
|
|
```ts
|
|
import { SavedObjectsType } from 'src/core/server';
|
|
|
|
export const dashboardVisualization: SavedObjectsType = {
|
|
name: 'dashboard_visualization',
|
|
...
|
|
mappings: {
|
|
properties: {
|
|
dynamic: false,
|
|
description: {
|
|
type: 'text',
|
|
},
|
|
hits: {
|
|
type: 'integer',
|
|
},
|
|
},
|
|
},
|
|
modelVersions: { ... },
|
|
};
|
|
```
|
|
|
|
Will result in the following mappings being applied to the .kibana index:
|
|
|
|
```ts
|
|
{
|
|
"mappings": {
|
|
"dynamic": "strict",
|
|
"properties": {
|
|
...
|
|
"dashboard_vizualization": {
|
|
"dynamic": false,
|
|
"properties": {
|
|
"description": {
|
|
"type": "text",
|
|
},
|
|
"hits": {
|
|
"type": "integer",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
Do not use field mappings like you would use data types for the columns of a SQL database. Instead, field mappings are analogous to a
|
|
SQL index. Only specify field mappings for the fields you wish to search on or query. By specifying `dynamic: false`
|
|
in any level of your mappings, Elasticsearch will accept and store any other fields even if they are not specified in your mappings.
|
|
|
|
Since Elasticsearch has a default limit of 1000 fields per index, plugins should carefully consider the
|
|
fields they add to the mappings. Similarly, Saved Object types should never use `dynamic: true` as this can cause an arbitrary
|
|
amount of fields to be added to the .kibana index.
|
|
|
|
## References
|
|
|
|
Declare <DocLink id="kibDevDocsSavedObjectsIntro" section="References" text="Saved Object references"/> by adding an id, type and name to the
|
|
`references` array.
|
|
|
|
```ts
|
|
router.get(
|
|
{ path: '/some-path', validate: false },
|
|
async (context, req, res) => {
|
|
const object = await context.core.savedObjects.client.create(
|
|
'dashboard',
|
|
{
|
|
title: 'my dashboard',
|
|
panels: [
|
|
{ visualization: 'vis1' }, [1]
|
|
],
|
|
indexPattern: 'indexPattern1'
|
|
},
|
|
{ references: [
|
|
{ id: '...', type: 'visualization', name: 'vis1' },
|
|
{ id: '...', type: 'index_pattern', name: 'indexPattern1' },
|
|
]
|
|
}
|
|
)
|
|
...
|
|
}
|
|
);
|
|
```
|
|
[1] Note how `dashboard.panels[0].visualization` stores the name property of the reference (not the id directly) to be able to uniquely
|
|
identify this reference. This guarantees that the id the reference points to always remains up to date. If a
|
|
visualization id was directly stored in `dashboard.panels[0].visualization` there is a risk that this id gets updated without
|
|
updating the reference in the references array.
|
|
|
|
## Migrations
|
|
|
|
Saved Objects support schema changes between Kibana versions, which we call migrations, implemented with model versions.
|
|
Model version transitions are applied when a Kibana installation is upgraded from one version to a newer version, when exports are imported via
|
|
the Saved Objects Management UI, or when a new object is created via the HTTP API.
|
|
|
|
### Defining model versions
|
|
|
|
Model versions are bound to a given [savedObject type](https://github.com/elastic/kibana/blob/9b330e493216e8dde3166451e4714966f63f5ab7/packages/core/saved-objects/core-saved-objects-server/src/saved_objects_type.ts#L22-L27)
|
|
|
|
When registering a SO type, a [modelVersions](https://github.com/elastic/kibana/blob/9a6a2ccdff619f827b31c40dd9ed30cb27203da7/packages/core/saved-objects/core-saved-objects-server/src/saved_objects_type.ts#L138-L177)
|
|
property is available. This attribute is a map of version numbers to [SavedObjectsModelVersion](https://github.com/elastic/kibana/blob/9a6a2ccdff619f827b31c40dd9ed30cb27203da7/packages/core/saved-objects/core-saved-objects-server/src/model_version/model_version.ts#L12-L20)
|
|
which is the top-level type/container to define model versions.
|
|
|
|
The modelVersion map is of the form `{ [version: number] => versionDefinition }`, using single integer to identify a version definition.
|
|
|
|
The first version must be numbered as version 1, incrementing by one for each new version.
|
|
|
|
```ts
|
|
import { schema } from '@kbn/config-schema';
|
|
import { SavedObjectsType } from 'src/core/server';
|
|
|
|
|
|
const schemaV1 = schema.object({ title: schema.string({ maxLength: 50, minLength: 1 }) });
|
|
const schemaV2 = schemaV1.extends({
|
|
description: schema.maybe(schema.string({ maxLength: 200, minLength: 1 })),
|
|
});
|
|
|
|
export const dashboardVisualization: SavedObjectsType = {
|
|
name: 'dashboard_visualization',
|
|
...
|
|
mappings: {
|
|
dynamic: false,
|
|
properties: {
|
|
title: { type: 'text' }, // This mapping was added before model versions
|
|
description: { type: 'text' }, // mappings introduced in v2
|
|
},
|
|
},
|
|
modelVersions: {
|
|
1: {
|
|
// Sometimes no changes are needed in the initial version, but you may have
|
|
// pre-existing mappings or data that must be transformed in some way
|
|
// In this case, title already has mappings defined.
|
|
changes: [],
|
|
schemas: {
|
|
// The forward compatible schema should allow any future versions of
|
|
// this SO to be converted to this version, since we are using
|
|
// @kbn/config-schema we opt-in to unknowns to allow the schema to
|
|
// successfully "downgrade" future SOs to this version.
|
|
forwardCompatibility: schemaV1.extends({}, { unknowns: 'ignore' }),
|
|
create: schemaV1,
|
|
},
|
|
},
|
|
2: {
|
|
changes: [
|
|
// In this second version we added new mappings for the description field.
|
|
{
|
|
type: 'mappings_addition',
|
|
addedMappings: {
|
|
description: { type: 'keyword' },
|
|
},
|
|
},
|
|
{
|
|
type: 'data_backfill',
|
|
backfillFn: (doc) => {
|
|
return {
|
|
attributes: {
|
|
description: 'my default description',
|
|
},
|
|
};
|
|
},
|
|
},
|
|
],
|
|
schemas: {
|
|
forwardCompatibility: schemaV2.extends({}, { unknowns: 'ignore' }),
|
|
create: schemaV2,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
```
|
|
|
|
That way:
|
|
- SO type versions are decoupled from stack versioning
|
|
- SO type versions are independent between types
|
|
|
|
### Saved object migrations on serverless
|
|
|
|
On serverless, Kibana cannot have any downtime — even during data migrations. In order to accomplish this we rollout new versions while old versions are still running. As soon as new versions are ready to start serving, requests will be directed to them.
|
|
|
|
<DocCallOut title="Only once a new version is done being rolled out will data migrations be run.">
|
|
</DocCallOut>
|
|
|
|
|
|
|
|
This has a few implications:
|
|
|
|
1. A _new version_ of application code should **never** introduce a new Saved Object field and treat it as a **required** field
|
|
2. A _new version_ of application code must be **fully backward compatible** with the **previous version's (n-1) Saved Object fields**
|
|
|
|
In order to introduce a new, required Saved Object field the following algorithm _must_ be followed:
|
|
|
|
1. Introduce a new model version field, consider this field _optional_ in any application code that uses it
|
|
2. Provide a `data_backfill` function for the new field
|
|
3. Merge to `main`
|
|
4. Wait for the next serverless release containing your data migration changes to complete
|
|
5. Update your code marking the new field as required in interfaces
|
|
6. Merge to `main`
|
|
|
|
At step 6 your code that was just merged to `main` will be guaranteed to find a value for the new field in Saved Objects.
|
|
|
|
<DocCallOut color="warning" title="What happens when you skip step 4?">
|
|
Not waiting until your `data_backfill` has been released means that none of your SO documents will have the field populated with the value you provided in the `data_backfill` function. The new field value will be `undefined` until migrations have run which only happens _after_ application code has already started running!
|
|
</DocCallOut>
|
|
|
|
|
|
### Testing model versions
|
|
Bugs in model version transitions cause downtime for our users and therefore have a very high impact. Follow the <DocLink id="kibDevTutorialTestingPlugins" section="saved-objects-model-versions" text="Saved Objects model versions"/> section in the plugin testing guide.
|
|
|
|
### How to opt-out of the global savedObjects APIs?
|
|
|
|
There are 2 options, depending on the amount of flexibility you need:
|
|
For complete control over your HTTP APIs and custom handling, declare your type as `hidden`, as shown in the example.
|
|
The other option that allows you to build your own HTTP APIs and still use the client as-is is to declare your type as hidden from the global saved objects HTTP APIs as `hiddenFromHttpApis: true`
|
|
|
|
```ts
|
|
import { SavedObjectsType } from 'src/core/server';
|
|
|
|
export const foo: SavedObjectsType = {
|
|
name: 'foo',
|
|
hidden: false, [1]
|
|
hiddenFromHttpApis: true, [2]
|
|
namespaceType: 'multiple-isolated',
|
|
mappings: { ... },
|
|
modelVersions: { ... },
|
|
...
|
|
};
|
|
```
|
|
|
|
[1] Needs to be `false` to use the `hiddenFromHttpApis` option
|
|
|
|
[2] Set this to `true` to build your own HTTP API and have complete control over the route handler. |