Saved Objects 服务
Saved Objects 服务在服务器端和客户端都可用。
Saved Objects service
允许 Kibana 插件像主数据库一样使用 Elasticsearch。可以将其视为 Elasticsearch 的对象文档映射器。一旦插件注册了一个或多个 Saved Object 类型,就可以使用 Saved Objects 客户端来查询或对每种类型执行创建、读取、更新和删除操作。
通过使用 Saved Objects,您的插件可以利用以下功能:
- 迁移可以通过转换文档并确保索引上的字段映射始终是最新的来发展您的文档模式。
- 一个 HTTP API 会自动为每种类型公开(除非指定了
hidden=true
)。 - 一个可以从服务器和浏览器使用的 Saved Objects 客户端。
- 用户可以使用 Saved Objects 管理 UI 或 Saved Objects 导入/导出 API 导入或导出 Saved Objects。
- 通过声明
references
,将导出对象的整个引用图。这使得用户可以轻松地导出例如dashboard
对象,并将显示仪表板所需的所有visualization
对象都包含在导出中。 - 当启用 X-Pack 安全和空间插件时,它们会透明地提供 RBAC 访问控制以及将 Saved Objects 组织到空间中的能力。
本文档包含希望使用 Saved Objects 的插件的开发者指南和最佳实践。
Saved object 类型定义应在其自己的 my_plugin/server/saved_objects
目录中定义。
该文件夹应包含每个类型的文件,以类型的 snake_case 名称命名,以及一个导出所有类型的 index.ts
文件。
import { SavedObjectsType } from 'src/core/server';
export const dashboardVisualization: SavedObjectsType = {
name: 'dashboard_visualization',
hidden: true,
namespaceType: 'multiple-isolated',
modelVersions: {
1: modelVersion1,
2: modelVersion2,
},
mappings: {
dynamic: false,
properties: {
description: {
type: 'text',
},
hits: {
type: 'integer',
},
},
},
// ...other mandatory properties
};
- 由于 Saved Object 类型的名称可能构成公共 Saved Objects HTTP API 的 URL 路径的一部分,因此这些名称应遵循我们的 API URL 路径约定,并且始终以 snake case 编写。
- 此字段确定“空间行为” — 这些对象可以存在于一个空间、多个空间还是所有空间中。此值表示此类型的对象只能存在于单个空间中。有关更多信息,请参阅 共享 Saved Objects。
export { dashboardVisualization } from './dashboard_visualization';
export { dashboard } from './dashboard';
import { dashboard, dashboardVisualization } from './saved_objects';
export class MyPlugin implements Plugin {
setup({ savedObjects }) {
savedObjects.registerType(dashboard);
savedObjects.registerType(dashboardVisualization);
}
}
每个 Saved Object 类型都可以定义自己的 Elasticsearch 字段映射。由于多个 Saved Object 类型可以共享相同的索引,因此类型定义的映射将嵌套在与类型名称匹配的顶级字段下。
例如,search
Saved Object 类型定义的映射
import { SavedObjectsType } from 'src/core/server';
// ... other imports
export function getSavedSearchObjectType: SavedObjectsType = {
name: 'search',
hidden: false,
namespaceType: 'multiple-isolated',
mappings: {
dynamic: false,
properties: {
title: { type: 'text' },
description: { type: 'text' },
},
},
modelVersions: { ... },
// ...other optional properties
};
- 简化
将导致以下映射应用于 .kibana_analytics
索引
{
"mappings": {
"dynamic": "strict",
"properties": {
...
"search": {
"dynamic": false,
"properties": {
"title": {
"type": "text",
},
"description": {
"type": "text",
},
},
}
}
}
}
不要像使用 SQL 数据库的列的数据类型那样使用字段映射。相反,字段映射类似于 SQL 索引。仅为你希望搜索或查询的字段指定字段映射。通过在映射的任何级别指定 dynamic: false
,Elasticsearch 将接受并存储任何其他字段,即使它们未在你的映射中指定。
由于 Elasticsearch 默认限制每个索引 1000 个字段,因此插件应仔细考虑添加到映射的字段。同样,Saved Object 类型永远不应使用 dynamic: true
,因为这可能会导致任意数量的字段添加到 .kibana
索引中。
Saved Objects 使用 modelVersions
支持更改。 modelVersion API 是一种为 savedObject 类型定义转换(“迁移”)的新方法,它将在 Kibana 版本 8.10.0
之后取代传统的迁移 API。 传统的迁移 API 已被弃用,这意味着不再可以使用传统系统注册迁移。
模型版本与堆栈版本分离,并满足零停机时间和向后兼容性的要求。
每个 Saved Object 类型都可以为其模式定义模型版本,并绑定到给定的 savedObject type。 通过定义新模型来指定对已保存对象类型的更改。
对于旧迁移,模型版本绑定到给定的 savedObject type
注册 SO 类型时,可以使用新的 modelVersions 属性。 此属性是 SavedObjectsModelVersion 的映射,它是定义模型版本的顶级类型/容器。
此映射遵循与旧迁移映射类似的 { [版本号] => 版本定义 }
格式,但是给定 SO 类型的模型版本现在由单个整数标识。
第一个版本必须编号为版本 1,每个新版本递增 1。
这样: - SO 类型版本与堆栈版本控制分离 - SO 类型版本在类型之间是独立的
一个**有效**的版本编号
const myType: SavedObjectsType = {
name: 'test',
modelVersions: {
1: modelVersion1,
2: modelVersion2,
},
// ...other mandatory properties
};
- 有效:从版本 1 开始
- 有效:版本之间没有间隙
一个**无效**的版本编号
const myType: SavedObjectsType = {
name: 'test',
modelVersions: {
2: modelVersion2,
4: modelVersion3,
},
// ...other mandatory properties
};
- 无效:第一个版本必须是 1
- 无效:跳过了版本 3
模型版本 不仅仅是以前的迁移那样的函数,而是描述版本行为方式以及自上次版本以来所做更改的结构化对象。
模型版本外观的基本示例
const myType: SavedObjectsType = {
name: 'test',
modelVersions: {
1: {
changes: [
{
type: 'mappings_addition',
addedMappings: {
someNewField: { type: 'text' },
},
},
{
type: 'data_backfill',
transform: someBackfillFunction,
},
],
schemas: {
forwardCompatibility: fcSchema,
create: createSchema,
},
},
},
// ...other mandatory properties
};
注意: 设计上支持给定版本的同一类型的多个更改,以允许合并不同的来源(为最终更高级别的 API 做准备)
此定义将完全有效
const version1: SavedObjectsModelVersion = {
changes: [
{
type: 'mappings_addition',
addedMappings: {
someNewField: { type: 'text' },
},
},
{
type: 'mappings_addition',
addedMappings: {
anotherNewField: { type: 'text' },
},
},
],
};
它当前由两个主要属性组成
描述在此版本期间应用的一系列更改。
重要提示: 这是替换旧迁移系统的部分,并允许定义版本何时添加新映射、改变文档或其他类型相关的更改。
当前的更改类型是
用于定义给定版本中引入的新映射。
使用示例
const change: SavedObjectsModelMappingsAdditionChange = {
type: 'mappings_addition',
addedMappings: {
newField: { type: 'text' },
existingNestedField: {
properties: {
newNestedProp: { type: 'keyword' },
},
},
},
};
注意: *添加映射时,还必须相应地更新根 type.mappings
(如先前所做的那样)。*
用于将映射标记为不再使用并准备好删除。
使用示例
let change: SavedObjectsModelMappingsDeprecationChange = {
type: 'mappings_deprecation',
deprecatedMappings: ['someDeprecatedField', 'someNested.deprecatedField'],
};
注意: *当前无法从现有索引的映射中删除字段(而无需重新索引到另一个索引),因此暂时不会删除使用此更改类型标记的映射,但这仍然应该用于允许我们的系统在上游 (ES) 解除阻止时清除映射。*
用于填充同一版本中添加的字段(索引或未索引)。
使用示例
let change: SavedObjectsModelDataBackfillChange = {
type: 'data_backfill',
transform: (document) => {
return { attributes: { someAddedField: 'defaultValue' } };
},
};
注意: *即使没有执行任何检查来确保它,也应仅使用此类型的模型更改来回填新引入的字段。*
用于从该类型的所有文档中删除数据(取消设置字段)。
使用示例
let change: SavedObjectsModelDataRemovalChange = {
type: 'data_removal',
attributePaths: ['someRootAttributes', 'some.nested.attribute'],
};
注意: *由于向后兼容性,在实际数据删除之前(如果需要回滚),必须在先前版本中停止字段利用率。 请参阅本文档中下面的字段删除迁移示例*
用于执行任意转换函数。
使用示例
let change: SavedObjectsModelUnsafeTransformChange = {
type: 'unsafe_transform',
transformFn: (document) => {
document.attributes.someAddedField = 'defaultValue';
return { document };
},
};
注意: *使用此类转换可能是不安全的,因为迁移系统将不知道哪些类型的操作将有效地针对文档执行。 只有在没有其他方法来满足一个人的迁移需求时,才应使用这些方法。* 如果您认为您需要使用此方法,请联系开发团队,因为从理论上讲,您不应该这样做。
与此版本关联的模式。 模式用于在 SO 文档生命周期的各个阶段验证或转换 SO 文档。
当前可用的模式是
这是模型版本引入的新概念。 此模式用于版本间兼容性。
从索引检索已保存对象文档时,如果文档的版本高于 Kibana 实例的最新已知版本,则该文档将通过关联模型版本的 forwardCompatibility
模式。
重要提示: 这些转换机制不应断言数据本身,而应仅剥离未知字段以将文档转换为给定版本的文档的形状。
基本上,此模式应保留给定版本的所有已知字段,并删除所有未知字段,而不会引发异常。
可以通过两种不同的方式实现向前兼容性模式。
- 使用
config-schema
具有两个字段的版本的模式示例:someField 和 anotherField
const versionSchema = schema.object(
{
someField: schema.maybe(schema.string()),
anotherField: schema.maybe(schema.string()),
},
{ unknowns: 'ignore' }
);
重要提示: 请注意模式选项中的 { unknowns: 'ignore' }
。 使用基于 config-schema
的模式时,这是必需的,因为这就是在不引发错误的情况下清除其他字段的方式。
- 使用普通的 javascript 函数
具有两个字段的版本的模式示例:someField 和 anotherField
const versionSchema: SavedObjectModelVersionEvictionFn = (attributes) => {
const knownFields = ['someField', 'anotherField'];
return pick(attributes, knownFields);
}
注意: *即使强烈推荐,也并非必须严格实现此模式。 类型所有者可以改为在他们的服务层中管理未知字段和版本间兼容性。*
这是 旧的 SavedObjectType.schemas 定义的直接替代品,现在直接包含在模型版本定义中。
作为复习,create
模式是 @kbn/config-schema
对象类型模式,用于在 create
和 bulkCreate
操作期间验证文档的属性。
注意: *实现此模式是可选的,但仍然建议这样做,因为否则在导入对象时将不会进行验证*
有关实现示例,请参阅用例示例。
这些是系统当前支持(开箱即用)的迁移方案的示例。
注意:更复杂的场景(例如通过复制/同步进行字段突变)可能已经实现,但由于 Core 没有提供合适的工具,与同步和兼容性相关的大部分工作需要在类型所有者的领域层中实现,因此我们尚未对此进行文档记录。
我们目前处于模型版本 1,并且我们的类型定义了 2 个索引字段:foo
和 bar
。
版本 1 中类型的定义如下所示:
const myType: SavedObjectsType = {
name: 'test',
namespaceType: 'single',
modelVersions: {
// initial (and current) model version
1: {
changes: [],
schemas: {
// FC schema defining the known fields (indexed or not) for this version
forwardCompatibility: schema.object(
{ foo: schema.string(), bar: schema.string() },
{ unknowns: 'ignore' }
),
// schema that will be used to validate input during `create` and `bulkCreate`
create: schema.object(
{ foo: schema.string(), bar: schema.string() },
)
},
},
},
mappings: {
properties: {
foo: { type: 'text' },
bar: { type: 'text' },
},
},
};
- 注意 `unknown: ignore`,这是我们如何清除未知字段的方式。
从这里开始,假设我们要引入一个新的非索引字段 dolly
,并且我们不需要使用默认值来填充它。
为了实现这一点,我们需要引入一个新的模型版本,唯一需要做的是定义关联的模式以包含这个新字段。
添加的模型版本如下所示:
// the new model version adding the `dolly` field
let modelVersion2: SavedObjectsModelVersion = {
// not an indexed field, no data backfill, so changes are actually empty
changes: [],
schemas: {
// the only addition in this model version: taking the new field into account for the schemas
forwardCompatibility: schema.object(
{ foo: schema.string(), bar: schema.string(), dolly: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ foo: schema.string(), bar: schema.string(), dolly: schema.string() },
)
},
};
- 注意 `unknown: ignore`,这是我们如何清除未知字段的方式。
添加新模型版本后,完整的类型定义如下所示:
const myType: SavedObjectsType = {
name: 'test',
namespaceType: 'single',
modelVersions: {
1: {
changes: [],
schemas: {
forwardCompatibility: schema.object(
{ foo: schema.string(), bar: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ foo: schema.string(), bar: schema.string() },
)
},
},
2: {
changes: [],
schemas: {
forwardCompatibility: schema.object(
{ foo: schema.string(), bar: schema.string(), dolly: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ foo: schema.string(), bar: schema.string(), dolly: schema.string() },
)
},
},
},
mappings: {
properties: {
foo: { type: 'text' },
bar: { type: 'text' },
},
},
};
此场景与前一个场景非常接近。不同之处在于,使用索引字段意味着添加一个 mappings_addition
变更,并且还需要相应地更新根映射。
为了重用前面的例子,假设我们要添加的 dolly
字段需要被索引。
在这种情况下,新版本需要执行以下操作:- 添加一个 mappings_addition
类型变更来定义新的映射 - 相应地更新根 mappings
- 像前面的例子一样添加更新后的模式
新版本定义如下所示:
let modelVersion2: SavedObjectsModelVersion = {
// add a change defining the mapping for the new field
changes: [
{
type: 'mappings_addition',
addedMappings: {
dolly: { type: 'text' },
},
},
],
schemas: {
// adding the new field to the forwardCompatibility schema
forwardCompatibility: schema.object(
{ foo: schema.string(), bar: schema.string(), dolly: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ foo: schema.string(), bar: schema.string(), dolly: schema.string() },
)
},
};
如前所述,我们还需要更新根映射定义:
mappings: {
properties: {
foo: { type: 'text' },
bar: { type: 'text' },
dolly: { type: 'text' },
},
},
添加模型版本 2 后,完整的类型定义如下所示:
const myType: SavedObjectsType = {
name: 'test',
namespaceType: 'single',
modelVersions: {
1: {
changes: [
{
type: 'mappings_addition',
addedMappings: {
foo: { type: 'text' },
bar: { type: 'text' },
},
},
],
schemas: {
forwardCompatibility: schema.object(
{ foo: schema.string(), bar: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ foo: schema.string(), bar: schema.string() },
)
},
},
2: {
changes: [
{
type: 'mappings_addition',
addedMappings: {
dolly: { type: 'text' },
},
},
],
schemas: {
forwardCompatibility: schema.object(
{ foo: schema.string(), bar: schema.string(), dolly: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ foo: schema.string(), bar: schema.string(), dolly: schema.string() },
)
},
},
},
mappings: {
properties: {
foo: { type: 'text' },
bar: { type: 'text' },
dolly: { type: 'text' },
},
},
};
现在是一个稍微不同的场景,我们希望使用默认值来填充新引入的字段。
在这种情况下,我们需要添加一个额外的 data_backfill
变更来填充新字段的值(除了 mappings_addition
变更之外)。
let modelVersion2: SavedObjectsModelVersion = {
changes: [
// setting the `dolly` field's default value.
{
type: 'data_backfill',
transform: (document) => {
return { attributes: { dolly: 'default_value' } };
},
},
// define the mappings for the new field
{
type: 'mappings_addition',
addedMappings: {
dolly: { type: 'text' },
},
},
],
schemas: {
// define `dolly` as an know field in the schema
forwardCompatibility: schema.object(
{ foo: schema.string(), bar: schema.string(), dolly: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ foo: schema.string(), bar: schema.string(), dolly: schema.string() },
)
},
};
完整的类型定义如下所示:
const myType: SavedObjectsType = {
name: 'test',
namespaceType: 'single',
modelVersions: {
1: {
changes: [
{
type: 'mappings_addition',
addedMappings: {
foo: { type: 'text' },
bar: { type: 'text' },
},
},
],
schemas: {
forwardCompatibility: schema.object(
{ foo: schema.string(), bar: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ foo: schema.string(), bar: schema.string() },
)
},
},
2: {
changes: [
{
type: 'data_backfill',
transform: (document) => {
return { attributes: { dolly: 'default_value' } };
},
},
{
type: 'mappings_addition',
addedMappings: {
dolly: { type: 'text' },
},
},
],
schemas: {
forwardCompatibility: schema.object(
{ foo: schema.string(), bar: schema.string(), dolly: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ foo: schema.string(), bar: schema.string(), dolly: schema.string() },
)
},
},
},
mappings: {
properties: {
foo: { type: 'text' },
bar: { type: 'text' },
dolly: { type: 'text' },
},
},
};
注意:如果该字段是非索引的,我们将不会使用 mappings_addition
变更或更新映射(如示例 1 中所做的那样)。
我们目前处于模型版本 1,并且我们的类型定义了 2 个索引字段:kept
和 removed
。
版本 1 中类型的定义如下所示:
const myType: SavedObjectsType = {
name: 'test',
namespaceType: 'single',
modelVersions: {
// initial (and current) model version
1: {
changes: [],
schemas: {
// FC schema defining the known fields (indexed or not) for this version
forwardCompatibility: schema.object(
{ kept: schema.string(), removed: schema.string() },
{ unknowns: 'ignore' }
),
// schema that will be used to validate input during `create` and `bulkCreate`
create: schema.object(
{ kept: schema.string(), removed: schema.string() },
)
},
},
},
mappings: {
properties: {
kept: { type: 'text' },
removed: { type: 'text' },
},
},
};
- 注意 `unknown: ignore`,这是我们如何清除未知字段的方式。
从这里开始,假设我们要删除 removed
字段,因为我们的应用程序由于最近的更改不再需要它。
首先要理解的是对向后兼容性的影响:假设 Kibana 版本 X
仍在使用此字段,并且我们在版本 X+1
中停止使用该字段。
我们不能在版本 X+1
中删除数据,因为我们需要能够在任何时候回滚到之前的版本。如果我们在升级到版本 X+1
期间删除此 removed
字段的数据,并且如果因为任何原因我们需要回滚到版本 X
,这将导致数据丢失,因为版本 X
仍在使用此字段,但在回滚后,它将不再出现在我们的文档中。
这就是为什么我们需要将任何字段删除作为 2 步操作来执行:- 发布 X
:Kibana 仍然使用该字段 - 发布 X+1
:Kibana 不再使用该字段,但数据仍然存在于文档中 - 发布 X+2
:数据已从文档中有效删除。
这样,任何先前版本的回滚(从 X+2
到 X+1
或 从 X+1
到 X
)在数据完整性方面都是安全的。
那么,主要的问题是,在版本 X+1
期间,如何让我们的应用程序层简单地忽略这个 removed
字段,因为我们不希望这个字段(现在未使用)从持久层返回,因为它可能会污染更高层,在该层中该字段实际上不再使用甚至未知。
通过引入新版本并使用 forwardCompatibility
模式来屏蔽该字段,可以很容易地做到这一点。
X+1
模型版本如下所示:
// the new model version ignoring the `removed` field
let modelVersion2: SavedObjectsModelVersion = {
changes: [],
schemas: {
forwardCompatibility: schema.object(
{ kept: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ kept: schema.string() },
)
},
};
- `removed` 不再在此处定义
- `removed` 不再在此处定义
添加新模型版本后,完整的类型定义如下所示:
const myType: SavedObjectsType = {
name: 'test',
namespaceType: 'single',
modelVersions: {
// initial (and current) model version
1: {
changes: [],
schemas: {
// FC schema defining the known fields (indexed or not) for this version
forwardCompatibility: schema.object(
{ kept: schema.string(), removed: schema.string() },
{ unknowns: 'ignore' }
),
// schema that will be used to validate input during `create` and `bulkCreate`
create: schema.object(
{ kept: schema.string(), removed: schema.string() },
)
},
},
2: {
changes: [],
schemas: {
forwardCompatibility: schema.object(
{ kept: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ kept: schema.string() },
)
},
}
},
mappings: {
properties: {
kept: { type: 'text' },
removed: { type: 'text' },
},
},
};
- 注意 `unknown: ignore`,这是我们如何清除未知字段的方式。
- `removed` 不再在此处定义
- `removed` 不再在此处定义
然后,在后续版本中,我们可以部署将有效删除文档中数据的更改。
// the new model version ignoring the `removed` field
let modelVersion3: SavedObjectsModelVersion = {
changes: [
{
type: 'data_removal',
removedAttributePaths: ['removed']
}
],
schemas: {
forwardCompatibility: schema.object(
{ kept: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ kept: schema.string() },
)
},
};
- 定义一个 data_removal 更改以删除该字段
数据删除后的完整类型定义如下所示:
const myType: SavedObjectsType = {
name: 'test',
namespaceType: 'single',
modelVersions: {
// initial (and current) model version
1: {
changes: [],
schemas: {
// FC schema defining the known fields (indexed or not) for this version
forwardCompatibility: schema.object(
{ kept: schema.string(), removed: schema.string() },
{ unknowns: 'ignore' }
),
// schema that will be used to validate input during `create` and `bulkCreate`
create: schema.object(
{ kept: schema.string(), removed: schema.string() },
)
},
},
2: {
changes: [],
schemas: {
forwardCompatibility: schema.object(
{ kept: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ kept: schema.string() },
)
},
},
3: {
changes: [
{
type: 'data_removal',
removedAttributePaths: ['removed']
}
],
schemas: {
forwardCompatibility: schema.object(
{ kept: schema.string() },
{ unknowns: 'ignore' }
),
create: schema.object(
{ kept: schema.string() },
)
},
}
},
mappings: {
properties: {
kept: { type: 'text' },
removed: { type: 'text' },
},
},
};
- 注意 `unknown: ignore`,这是我们如何清除未知字段的方式。
- `removed` 不再在此处定义
- `removed` 不再在此处定义
- 定义一个 data_removal 更改以删除该字段
模型版本定义比旧的迁移函数更具结构性,这使得它们在没有适当工具的情况下更难测试。这就是为什么从 @kbn/core-test-helpers-model-versions
包中公开了一组测试工具和实用程序,以帮助正确测试与模型版本及其关联转换相关的逻辑。
对于单元测试,该软件包公开了实用程序,可以轻松测试将文档从一个模型版本转换为另一个模型版本(向上或向下)的影响。
createModelVersionTestMigrator
助手允许创建一个测试迁移器,该迁移器可用于通过以与升级期间迁移算法相同的方式转换文档来测试版本之间的模型版本更改。
例子
import {
createModelVersionTestMigrator,
type ModelVersionTestMigrator
} from '@kbn/core-test-helpers-model-versions';
const mySoTypeDefinition = someSoType();
describe('mySoTypeDefinition model version transformations', () => {
let migrator: ModelVersionTestMigrator;
beforeEach(() => {
migrator = createModelVersionTestMigrator({ type: mySoTypeDefinition });
});
describe('Model version 2', () => {
it('properly backfill the expected fields when converting from v1 to v2', () => {
const obj = createSomeSavedObject();
const migrated = migrator.migrate({
document: obj,
fromVersion: 1,
toVersion: 2,
});
expect(migrated.properties).toEqual(expectedV2Properties);
});
it('properly removes the expected fields when converting from v2 to v1', () => {
const obj = createSomeSavedObject();
const migrated = migrator.migrate({
document: obj,
fromVersion: 2,
toVersion: 1,
});
expect(migrated.properties).toEqual(expectedV1Properties);
});
});
});
在集成测试期间,我们可以启动一个真实的 Elasticsearch 集群,使我们能够以几乎与生产运行时相同的方式操作 SO 文档。通过集成测试,我们甚至可以模拟两个具有不同模型版本的 Kibana 实例的共存,以断言它们交互的行为。
该软件包公开了一个 createModelVersionTestBed
函数,可用于完全设置用于模型版本集成测试的测试平台。它可用于启动和停止 ES 服务器,并启动我们正在测试的两个版本之间的迁移。
例子
import {
createModelVersionTestBed,
type ModelVersionTestKit
} from '@kbn/core-test-helpers-model-versions';
describe('myIntegrationTest', () => {
const testbed = createModelVersionTestBed();
let testkit: ModelVersionTestKit;
beforeAll(async () => {
await testbed.startES();
});
afterAll(async () => {
await testbed.stopES();
});
beforeEach(async () => {
// prepare the test, preparing the index and performing the SO migration
testkit = await testbed.prepareTestKit({
savedObjectDefinitions: [{
definition: mySoTypeDefinition,
// the model version that will be used for the "before" version
modelVersionBefore: 1,
// the model version that will be used for the "after" version
modelVersionAfter: 2,
}]
})
});
afterEach(async () => {
if(testkit) {
// delete the indices between each tests to perform a migration again
await testkit.tearDown();
}
});
it('can be used to test model version cohabitation', async () => {
// last registered version is `1` (modelVersionBefore)
const repositoryV1 = testkit.repositoryBefore;
// last registered version is `2` (modelVersionAfter)
const repositoryV2 = testkit.repositoryAfter;
// do something with the two repositories, e.g
await repositoryV1.create(someAttrs, { id });
const v2docReadFromV1 = await repositoryV2.get('my-type', id);
expect(v2docReadFromV1.attributes).toEqual(whatIExpect);
});
});
限制
由于测试平台仅创建实例化两个 SO 存储库所需的核心部分,并且由于我们无法正确加载所有插件(为了适当的隔离),因此集成测试平台目前有一些限制。
未启用任何扩展
- 没有安全性
- 没有加密
- 没有空间
所有 SO 类型都将使用相同的 SO 索引
无服务器环境,以及在此类环境中执行升级的方式(在某些时候,应用程序的新旧版本共存),导致 SO API 工作方式方面的一些特殊性,以及我们需要记录的一些限制/边缘情况。
默认情况下,find
API(与返回文档的任何其他 SO API 一样)将在返回所有文档之前对其进行迁移,以确保在共存期间新旧版本都可以使用这些文档(例如,旧节点搜索已迁移的文档,或新节点搜索尚未迁移的文档)。
但是,当使用 find
API 的 fields
选项时,无法迁移文档,因为某些模型版本更改无法应用于部分属性集。 因此,当提供 fields
选项时,从 find
返回的文档将不会被迁移。
这就是为什么在使用此选项时,API 使用者需要确保传递给 fields
选项的所有字段都已经存在于之前的模型版本中。否则,可能会导致升级期间出现不一致,在这种情况下,当使用该选项时,新引入或回填的字段可能不一定出现在从 search
API 返回的文档中。
(注意:Kibana 的先前版本和下一个版本都必须遵循此规则)
savedObjects bulkUpdate
API 将在客户端更新文档,然后重新索引更新后的文档。这些更新操作在内存中完成,并且在更新存储在某些字段中的具有大型 json
blobs 的许多对象时,会导致内存约束问题。因此,我们建议不要对以下 savedObjects 使用 bulkUpdate
:- 使用数组(因为这些往往是大型对象) - 在某些字段中存储大型 json
blobs