Skip to main content

扩展模型

Backstage目录实体数据模型是基于Kubernetes 对象格式本页将从更高层次介绍这些语义,以及如何扩展它们以适应您的组织。

Backstage 开箱即带许多目录概念:

  • There are a number of builtin versioned kinds, such as Component, User etc. These encapsulate the high level concept of an entity, and define the schema for its entity definition data. * An entity has both a metadata object and a spec object at the root. * Each kind may or may not have a type. For example, there are several well known types of component, such as service and website. These clarify the more detailed nature of the entity, and may affect what features are exposed in the interface. * Entities may have a number of annotations on them. These can be added either by humans into the descriptor files, or added by automated processes when the entity is ingested into the catalog. * Entities may have a number of labels on them. * Entities may have a number of relations, expressing how they relate to each other in different ways.

我们将在下文中列出各种扩展的可能性。

为现有种类添加新的 apiVersion

意图示例:

我想发展这个核心种类,对语义进行一些调整,因此我将 > apiVersion 提升一个级别" > "我想发展这个核心种类,对语义进行一些调整,因此我将 > apiVersion 提升一个级别"。

"这个核心种类很合适,但我们想随意发展它,所以我们将把它 > 移到我们自己公司的 apiVersion 空间,并用它来代替 > backstage.io"。

backstage.ioapiVersion 空间仅供Backstage维护者使用,请勿在该空间内更改或添加版本。

如果您添加一个api 版本实体种类由 apiVersion + 种类对标识,因此即使生成的实体可能与核心实体相似,也不能保证插件能够解析或理解其数据。 请参阅下面关于添加新种类的内容。

添加新种类

意图示例:

我想给另一个东西建模,但它不适合内置的任何一种"。

"这个核心种类很合适,但我们想随意发展它,所以我们将把它 > 移到我们自己公司的 apiVersion 空间,并用它来代替 > backstage.io"。

A一种是一个总体系列,或者可以说是一个概念,由同样共享模式的实体组成。 Backstage 提供了许多内置的实体,我们认为这些实体对于人们可能希望在 Backstage 中建模的各种需求都很有用。 我们的主要目标是将事物映射到这些类型中,但有时您可能希望或需要扩展到这些类型之外。

引入新的 apiVersion 与添加新的种类基本相同。 请注意,大多数插件都是根据内置的@backstage/catalog-model包,并有与之相一致的期望值。

从存储和应用程序接口的角度来看,目录后端本身并不关心其存储的实体种类。 扩展新的实体种类主要是在使用CatalogBuilder然后让插件能够理解新的类型。

对于消费端来说,情况就不同了。 添加种类会产生非常大的影响。 Backstage 的基础就是将行为、视图和功能附加到我们赋予一定意义的实体上。 在许多地方,代码会检查if (kind === 'X')为一些硬编码的X并将其转换为从软件包(如@backstage/catalog-model.

如果您想建立的模型不适合任何一种内置类型,请随时联系Backstage维护者,讨论如何以最佳方式进行。

如果最终添加了新种类,则必须将其apiVersion因此,请使用一个合理的前缀,通常以您的组织名称为基础,例如:"......"。my-company.net/v1.也确实挑选了一个新的kind标识符,不会与内置类型相冲突。

为现有类型添加新类型

意图示例:

"这显然是一个组件,但它的类型与>我以前见过的组件不太一致"。

"我们的团队不叫 "团队",我们不能把 "羊群 "作为团队类型吗?

某些实体类型有一个type字段。 组织可以在此自由表达同类中的各种实体。 该字段应遵循某种对自己有意义的分类法。 所选值可能会影响Backstage中针对该实体启用的操作和视图。 在 Spotify 内部,我们的模型在过去几年中得到了显著发展,现在我们的组件类型包括 ML 模型、应用程序、数据管道等。

将不适合任何现有类型的软件归入其他通用类型可能很有诱惑力。 我们建议不要这样做有几个原因:首先,我们发现最好与工程师描述软件时的概念模型相匹配。 其次,Backstage 通过插件集成基础架构工具,帮助工程师管理软件。 不同的插件用于管理不同类型的组件。

例如light house插件软件建模越具体,就越容易提供与上下文相关的插件。

添加一个新类型所需的工作量相对较少,风险也很小。 目录后端可以接受任何类型的值,但如果你想在新类型上附加特定行为,则可能需要更新插件。

更改实体包络或元数据字段的验证规则

意图示例:

"我们想导入旧目录,但元数据名称的默认允许字符集 > 太严格了"。

"我想更改注释的规则,以便允许我在注释值中存储任何 > 数据,而不仅仅是字符串"。

从某个位置读取原始实体数据后,这些数据会经过一定数量的所谓Validators作为实体策略检查步骤的一部分,它们确保基本封套和元数据的类型和语法是合理的--简而言之,这些都不是实体种类特有的。 在使用后端目录构建时,可以替换部分或全部这些验证器。CatalogBuilder.

这类扩展的风险和影响因所要做的事情而异。 例如,扩展种类、命名空间和名称的有效字符集可能相当无害,但有几个明显的例外--例如,有些代码希望这些字符集永远不包含冒号或斜线,而引入不安全的 URL 字符则有可能破坏那些对编码参数不谨慎的插件。 在注解中支持非字符串可能是可行的,但尚未在现实世界中尝试过--可能会出现某种程度的插件破坏,而这是很难预测的。

在进行此类扩展之前,我们建议您联系 Backstage 维护者或支持合作伙伴,讨论您的使用情况。

更改核心实体字段的验证规则

示例意图:

"我不喜欢 Owner 是强制性的,我希望它是可选的"。

从一个位置读取并经过策略检查的实体数据后,会通过处理器链发送,寻找能执行validateEntityKind步骤,以确定数据属于已知类型并遵守其模式。 有一个内置处理器可为所有已知核心类型实现这一功能,并将数据与它们的固定验证模式相匹配。 在使用CatalogBuilder该替代处理器的名称必须与内置处理器一致、BuiltinKindsEntityProcessor.

这种类型的扩展风险很高,根据所做更改的类型,可能会对整个生态系统产生很大影响。 因此,在一般情况下不建议使用这种扩展。 将有大量插件和处理器,甚至是内核本身,会对数据的形状做出假设,并从@backstage/catalog-model包装

为元数据对象添加新字段

示例意图:

"我们的实体有这样一个辅助属性,我想为 > 几种实体表达这个属性,但它并不适合作为规范字段"。

元数据对象目前是开放的,可以进行扩展。 在元数据中发现的任何未知字段都将被逐字存储在目录中。 不过,我们要提醒您不要过度扩展元数据。 首先,您有可能会与模型的未来扩展相冲突。 其次,这种类型的扩展通常更适合在其他地方进行--主要是在元数据标签或注释中,但有时您甚至可能想要创建一个新的组件类型或类似类型。

在某些情况下,元数据可能是最合适的地方。 如果您觉得自己遇到了这样的情况,而且这种情况也适用于其他人,请随时联系Backstage维护者或支持合作伙伴,讨论您的使用情况。 也许我们可以扩展核心模型,让您和其他人都能从中受益。

为现有类型的规范对象添加新字段

示例意图:

"内置的组件类型很好,但我们希望在 > 规范中添加一个额外的字段,用于描述它是在生产阶段还是暂存阶段"。

一种模式的验证通常不会禁止实体中的 "未知 "字段spec因此,从目录的角度来看,这样做通常是可行的。

添加这样的字段与上面提到的元数据扩展有同样的风险。 首先,您有可能与模型未来的扩展相冲突。 其次,这种扩展通常在其他地方更容易实现--主要是在元数据标签或注释中,但有时您甚至可能想创建一个新的组件类型或类似的类型。

在某些情况下,规范可能是最合适的地方。 如果您认为自己遇到了这样的情况,而且这种情况也适用于其他人,请随时联系Backstage维护者或支持合作伙伴,讨论您的使用情况。 也许我们可以扩展核心模型,让您和其他人都能从中受益。

添加新注释

意图示例:

"我们定制的构建系统有命名管道集的概念,我们 > 希望将单个组件与其对应的管道集 > 关联起来,以便显示其构建状态。

我们希望 > 能在 Backstage 中显示服务的持续警报,因此 > 最好能以某种方式将集成密钥附加到实体上。

注释主要供插件使用,用于特征检测或链接到外部系统。 有时注释是由人工添加的,但通常是由处理器在摄取时自动生成的。 有一套知名注释只要遵守以下命名规则,就不会对其他系统造成风险或影响。

  • The backstage.io annotation prefix is reserved for use by the Backstage maintainers. Reach out to us if you feel that you would like to make an addition to that prefix. * Annotations that pertain to a well known third party system should ideally be prefixed with a domain, in a way that makes sense to a reader and connects it clearly to the system (or the maker of the system). For example, you might use a pagerduty.com prefix for pagerduty related annotations, but maybe not ldap.com for LDAP annotations since it's not directly affiliated with or owned by an LDAP foundation/company/similar. * Annotations that have no prefix at all, are considered local to your Backstage instance and can be used freely as such, but you should not make use of them outside of your organization. For example, if you were to open source a plugin that generates or consumes annotations, then those annotations must be properly prefixed with your company domain or a domain that pertains to the annotation at hand.

添加新标签

意图示例:

"我们的流程收割系统希望定期搜索 > 具有特定属性的组件"。

"如果我们的服务 Owners 能以某种方式标记他们的组件, > 让 CD 系统知道是否要为该 > 服务自动生成 SRV 记录,那就更好了"。

标签主要用于过滤实体,由外部系统查找具有特定属性的实体。 有时也用于特征检测/选择。 例如,可以添加一个标签deployments.my-company.net/register-srv: "true".

在撰写本文时,标签的使用还非常有限,我们仍在与社区共同研究如何更好地使用标签。 如果您认为您的使用情况最适合标签,我们希望您能告知Backstage维护人员。

您可以自由添加标签,只要遵守以下命名规则,就不会对其他系统造成风险或影响。

  • The backstage.io label prefix is reserved for use by the Backstage maintainers. Reach out to us if you feel that you would like to make an addition to that prefix. * Labels that pertain to a well known third party system should ideally be prefixed with a domain, in a way that makes sense to a reader and connects it clearly to the system (or the maker of the system). For example, you might use a pagerduty.com prefix for pagerduty related labels, but maybe not ldap.com for LDAP labels since it's not directly affiliated with or owned by an LDAP foundation/company/similar. * Labels that have no prefix at all, are considered local to your Backstage instance and can be used freely as such, but you should not make use of them outside of your organization. For example, if you were to open source a plugin that generates or consumes labels, then those labels must be properly prefixed with your company domain or a domain that pertains to the label at hand.

添加新的关系类型

意图示例:

"我们有这样一个概念,即服务维护权与所有权是分开的, > 我们希望与个人用户建立关系"。

"我们认为,我们需要将团队到全球部门 > 映射显式地建模为一种关系,因为它是我们组织设置的核心,而且我们经常 > 查询它。

任何处理器都可以在处理实体时发出实体的关系,在构建后端目录时可以使用CatalogBuilder它们可以根据实体数据本身或从其他地方收集的信息来发出关系。 关系是有指向性的,从源实体到目标实体。 它们也与发出关系的实体相关联,即发出关系时需要处理的实体。 关系可能是悬空的(引用目录中实际上不存在的名称),调用者需要注意这一点。

有一套知名关系你不能改变它们是定向的这一事实,它们的源和目标都必须是一个实体参考要接受新的关系类型,不需要对目录后端做任何修改。

在撰写本文时,我们还没有关系类型的命名/前缀方案。 类型也没有验证是否只包含特定的字符集。 在这方面的规则确定之前,您应该坚持只使用字母、破折号和数字,并且为了避免与未来的核心关系类型相冲突,您可能需要在类型前加上某种前缀。 例如myCompany-maintainerOf+myCompany-maintainedBy.

如果您建议将某种关系类型提升为核心产品,请联系Backstage维护者或支持合作伙伴。

使用知名关系类型实现新目的

意图示例:

"ownerOf/ownedBy 关系类型听起来很适合表达 > 用户是我们公司特定 ServiceAccount 类型的技术所有者,我们 > 希望为此重用这些关系类型。"

在撰写本文时,这还是一个未知领域。 例如,如果关系的文档使用说明关系的一端通常是用户或组,那么消费者很可能会使用以下形式的条件语句if (x.kind === 'User') {} else {}当出现意想不到的种类时,它们会感到困惑。

如果您想扩展已建立的关系类型的使用范围,使其影响到您所在组织之外,请联系Backstage维护者或支持合作伙伴,讨论风险/影响。 甚至可以考虑将关系的一端添加到核心中。

添加新的状态字段

示例意图:

"我们希望通过目录以通用方式传达实体状态, > 作为一个集成层。 我们的监控和警报系统有一个 > Backstage 插件,如果实体的状态字段包含 > 当前警报状态,并与实际实体数据接近,供任何人使用,这将非常有用。 我们 > 发现 status.items 语义不太合适,因此我们希望在 status 下为这些目的制作我们自己的 > 自定义字段"。

我们还没有大胆地为status我们建议使用status.items机制(见下文),否则第三方消费者将无法使用您的状态信息。 如果您对该主题感兴趣,请在 Discord 上联系维护者,或在 GitHub 上发布一个问题,描述您的使用案例。

添加新的状态项目类型

示例意图:

"实体 status.items 字段的语义可以满足我们的需要,但 > 我们想把我们自己的状态类型添加到该数组中,而不是 > 目录特定类型。

这是一种简单、低风险的方法,可为实体添加自己的状态信息。 消费者将能够轻松地与其他类型/来源一起跟踪和显示状态。

我们建议对组织内部非严格私有的任何状态类型进行命名,以避免碰撞。 例如,由Backstage核心进程发出的状态将前缀为backstage.io/您的组织可能在以下方面具有前缀my-org.net/pagerduty.com/active-alerts可以是该特定外部系统的合理的完整状态项目类型。

关于如何发送自定义状态的机制还未到位,因此如果您对此感兴趣,可以考虑在 discord 上联系维护者,或者在 GitHub 上提交一个问题,描述您的使用情况。本期还包含更多背景信息。

使用模型引用不同的环境

示例意图:

"我的应用程序接口有多个版本,部署在不同的环境中,因此我 > 希望将 mytool-devmytool-prod 作为不同的实体"。

虽然可以将同一事物的不同版本表示为不同的实体,但我们一般不建议这样做。 我们认为,开发人员应该能够找到一个Component代表一个服务,并能在其视图中看到在整个堆栈中部署的不同代码版本。 这种推理方法同样适用于其他类型,如API.

尽管如此,有时不同版本之间的差异非常大,以至于从消费者的角度来看,它们代表的是一个全新的实体。 例如,不同的重要在这种情况下,可以考虑使用一个或多个主要版本的应用程序接口。my-api-v2一个my-api-v3这符合最终用户在搜索 API 时的期望,也符合为这两种应用程序设计独立文档的愿望。 但要慎用这种方法--只有在额外的建模负担大于为用户提供更好的清晰度的情况下才使用。

在编写自定义插件时,我们鼓励设计这些插件,使其能够在目录中对软件的统一引用下,通过环境等显示所有不同的变化。 例如,对于持续部署插件,如果用户能够在一个视图中看到实体在所有不同环境中部署的版本,就会对其大有裨益。 此外,用户还可以在一个环境中推广到另一个环境,进行回滚,查看其相对性能指标等。 这种一致性和将工具集中在一个地方的做法,正是Backstage(Backstage)这样的工具能够提供最大价值和使用效率的地方。 将实体分割成一个个小岛会增加这种难度。

实现自定义模型扩展

本节将向你介绍使用新实体类型扩展目录模型的步骤。

创建自定义实体定义

引入自定义实体的第一步是定义它的形状和模式。 我们使用 TypeScript 类型和 JSONSchema 模式来实现这一点。

大多数情况下,您希望在前端和后端代码中都至少有扩展的 TypeScript 类型,这意味着您可能希望有一个同构包来容纳这些类型。 在 Backstage 主 repo 中,包的命名模式为<plugin>-common用于同构软件包,您也可以选择采用这种模式。

您可以通过运行yarn new --select plugin-common或者运行yarn new然后从选项列表中选择 "plugin-common

目前还没有使用@backstage/cli也许现在最简单的入门方法就是复制主软件源中一个现有软件包的内容,例如plugins/scaffolder-common,并将文件夹和文件内容重命名为所需名称。 本例使用_食物吧_作为插件名称,因此插件将被命名为_foobar-common_.

一旦有了通用软件包,就可以开始添加自己的实体定义了。 关于如何添加的具体细节,我们可以从现有的scaffolder-common但简而言之,您需要为新实体类型声明 TypeScript 类型和 JSONSchema。

为实体建立自定义处理器

下一步是为新的实体种类创建一个自定义处理器。 这将在目录中使用,以确保它能够摄取和验证新种类的实体。 就像定义包一样,你可以从现有的ScaffolderEntitiesProcessor我们还提供了一个自定义实体目录流程的高级示例:

import { CatalogProcessor, CatalogProcessorEmit, processingResult } from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common'
import { Entity, entityKindSchemaValidator } from '@backstage/catalog-model';

// For an example of the JSONSchema format and how to use $ref markers to the
// base definitions, see:
// https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema/kinds/Component.v1alpha1.schema.json
import { foobarEntityV1alpha1Schema } from '@internal/catalog-model';

export class FoobarEntitiesProcessor implements CatalogProcessor {
// You often end up wanting to support multiple versions of your kind as you
// iterate on the definition, so we keep each version inside this array as a
// convenient pattern.
private readonly validators = [
// This is where we use the JSONSchema that we export from our isomorphic
// package
entityKindSchemaValidator(foobarEntityV1alpha1Schema),
];

// Return processor name
getProcessorName(): string {
return 'FoobarEntitiesProcessor'
}

// validateEntityKind is responsible for signaling to the catalog processing
// engine that this entity is valid and should therefore be submitted for
// further processing.
async validateEntityKind(entity: Entity): Promise<boolean> {
for (const validator of this.validators) {
// If the validator throws an exception, the entity will be marked as
// invalid.
if (validator(entity)) {
return true;
}
}

// Returning false signals that we don't know what this is, passing the
// responsibility to other processors to try to validate it instead.
return false;
}

async postProcessEntity(
entity: Entity,
_location: LocationSpec,
emit: CatalogProcessorEmit,
): Promise<Entity> {
if (
entity.apiVersion === 'example.com/v1alpha1' &&
entity.kind === 'Foobar'
) {
const foobarEntity = entity as FoobarEntityV1alpha1;

// Typically you will want to emit any relations associated with the
// entity here.
emit(processingResult.relation({ ... }))
}

return entity;
}
}

处理器创建后,可以通过CatalogBuilderpackages/backend/src/plugins/catalog.ts:

packages/backend/src/plugins/catalog.ts
import { FoobarEntitiesProcessor } from '@internal/plugin-foobar-backend';

export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
builder.addProcessor(new FoobarEntitiesProcessor());
const { processingEngine, router } = await builder.build();
// ..
}