ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Backstage 如何编写自定义 Entity Provider 从外部系统摄取实体?

Backstage 如何编写自定义 Entity Provider 从外部系统摄取实体? Backstage 如何编写自定义 Entity Provider 从外部系统摄取实体【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage当你的数据存放在 Backstage 没有现成 provider 模块覆盖的外部系统里比如 HR 系统、内部 API、任意能列出实体的服务而你又希望这些数据进入软件目录时就需要编写一个自定义 Entity Provider。Entity Provider 位于 catalog 的最外层是实体处理树的根来源——内置的 dynamic location store API 和你在app-config.yaml里声明的静态 locations 就属于两种内置 provider。本文基于 Backstage 官方文档给出从 CLI 脚手架生成 provider 模块、实现读取逻辑、注册与配置调度到验证摄取结果的完整路径。前提是你的 backend 使用新版 backend system并已注册 catalog 后端插件backend.add(import(backstage/plugin-catalog-backend))。Entity Provider 的几条决定实现方式的特点写代码前先理解 provider 的运行模型这些特性直接决定了你要怎么实现你在 backend 代码里实例化 provider并通过 catalog builder 注册通常每个远程系统对应一个 provider 实例。你可能需要主动驱动它运行有的 provider 按周期触发有的响应 webhook 或 pub/sub 事件。它的时机与处理循环processing loops相互独立——一个 provider 可以每 30 秒跑一次另一个则随每次 webhook 调用运行。它对自己的实体集做更新时既可以整体替换也可以逐条增删。它的输出是一组 unprocessed entities之后还要经过处理循环才能成为最终的、stitched 的实体。当它删除一个实体时该根节点下由 processor 生成的整个子树也会被删除。用 CLI 脚手架生成 provider 模块官方文档给出的最快起点是 Backstage CLIyarn new --select catalog-provider-module它会脚手架出一个完整的 backend module包含 provider 类、配置解析、调度和测试。CLI 会提示输入 module ID例如frobs生成后在plugins文件夹下得到如下结构plugins/catalog-backend-module-frobs-provider/ ├── config.d.ts ├── package.json ├── src/ │ ├── index.ts │ ├── module.ts │ └── provider/ │ ├── FrobsProvider.ts │ ├── FrobsProvider.test.ts │ └── readProviderConfigs.ts在 read 方法里实现实体读取逻辑生成的 provider 类实现了EntityProvider接口处理调度、连接管理和 mutation。以下是以 module IDfrobs为例的关键结构来自官方文档import { Config } from backstage/config; import { DeferredEntity, EntityProvider, EntityProviderConnection, } from backstage/plugin-catalog-node; import { randomUUID } from node:crypto; import { readProviderConfigs } from ./readProviderConfigs; import { LoggerService, SchedulerService, SchedulerServiceTaskRunner, } from backstage/backend-plugin-api; export class FrobsProvider implements EntityProvider { static fromConfig( configRoot: Config, options: { logger: LoggerService; scheduler: SchedulerService }, ): FrobsProvider[] { return readProviderConfigs(configRoot).map(providerConfig { return new FrobsProvider({ id: providerConfig.id, target: providerConfig.target, logger: options.logger, taskRunner: options.scheduler.createScheduledTaskRunner( providerConfig.schedule, ), }); }); } readonly #id: string; readonly #target: string; readonly #logger: LoggerService; readonly #taskRunner: SchedulerServiceTaskRunner; constructor(options: { id: string; target: string; logger: LoggerService; taskRunner: SchedulerServiceTaskRunner; }) { this.#id options.id; this.#target options.target; this.#logger options.logger; this.#taskRunner options.taskRunner; } getProviderName() { return FrobsProvider:${this.#id}; } async connect(connection: EntityProviderConnection) { const id ${this.getProviderName()}:refresh; await this.#taskRunner.run({ id, fn: async () { const logger this.#logger.child({ taskId: id, taskInstanceId: randomUUID(), }); try { const entities await this.read({ logger }); logger.info(Read ${entities.length} entities); await connection.applyMutation({ type: full, entities, }); } catch (error) { logger.error(Refresh failed, error); } }, }); } async read(options: { logger: LoggerService }): PromiseDeferredEntity[] { const { logger } options; logger.info(Reading entities from ${this.#target}); // Replace this with your actual>metadata: { annotations: { [ANNOTATION_LOCATION]: hr-user:${this.getStaffUrl}, [ANNOTATION_ORIGIN_LOCATION]: hr-user:${this.getStaffUrl}, }, links, name: kebabCase(user.displayName), title: user.displayName, },两个注解的取值规则何时相同、何时因 location 委托而不同见 Well-known Annotations 文档。把 provider 模块注册进 catalog生成的module.ts通过 backend module 系统把 provider 接入 catalogimport { coreServices, createBackendModule, } from backstage/backend-plugin-api; import { catalogProcessingExtensionPoint } from backstage/plugin-catalog-node; import { FrobsProvider } from ./provider/FrobsProvider; export const catalogModuleFrobs createBackendModule({ moduleId: frobs-provider, pluginId: catalog, register({ registerInit }) { registerInit({ deps: { logger: coreServices.logger, config: coreServices.rootConfig, scheduler: coreServices.scheduler, processing: catalogProcessingExtensionPoint, }, async init({ logger, scheduler, config, processing }) { processing.addEntityProvider( FrobsProvider.fromConfig(config, { logger, scheduler, }), ); }, }); }, });CLI 模板同时生成了在 backend 中注册该 module 的代码即packages/backend/src/index.ts中的const backend createBackend(); backend.add(import(backstage/plugin-catalog-backend)); /* highlight-add-next-line */ backend.add(import(./plugins/catalog-backend-module-frobs-provider)); backend.start();注册路径是processing.addEntityProvider(...)它通过catalogProcessingExtensionPoint扩展点把fromConfig创建出的 provider 数组交给 catalog 处理系统。配置 target 与调度生成的readProviderConfigs.ts从app-config.yaml解析配置同时支持单实例和多个命名实例两种写法。单实例catalog: providers: frobsProvider: target: https://frobs.example.com/api/v2 schedule: frequency: { minutes: 30 } timeout: { minutes: 3 }多实例例如指向不同环境catalog: providers: frobsProvider: production: target: https://frobs.example.com/api/v2 schedule: frequency: { minutes: 30 } timeout: { minutes: 3 } staging: target: https://frobs-staging.example.com/api/v2 schedule: frequency: { hours: 1 } timeout: { minutes: 3 }不指定schedule时provider 默认每 30 分钟运行一次、超时 3 分钟——这与模板readProviderConfigs.ts中的DEFAULT_SCHEDULE一致。此外你可以用脚手架生成的config.d.ts文件为配置添加 schema其中类型引用了SchedulerServiceTaskScheduleDefinitionConfig配置 schema 的写法参见配置文件定义文档。选择 full 还是 delta mutation每个 provider 实例拥有自己的实体 bucket由getProviderName返回的稳定名字标识。每次 provider 发出 mutation改变的都是这个 bucket 的内容bucket 之外不可访问。两种 mutationFull mutation——替换整个 bucket 的内容。catalog 内部把它实现为高效的 delta因为相邻两次运行之间的差异通常很小。这是生成模板的默认策略适合能从远程源批量拉取全部实体的场景await connection.applyMutation({ type: full, entities: entities.map(entity ({ entity, locationKey: frobs-provider:${this.#id}, })), });Delta mutation——对 bucket 内的特定实体做 upsert 或删除更适合事件驱动的 provider收到的是单条变更通知而非全量快照await connection.applyMutation({ type: delta, added: newEntities.map(entity ({ entity, locationKey: frobs-provider:${this.#id}, })), removed: removedEntities.map(entity ({ entity, locationKey: frobs-provider:${this.#id}, })), });无论哪种方式catalog 都会把这些实体当作 unprocessed 处理落库后已注册的 processors 再把它们转成最终的、stitched 的实体。Location key 的冲突规则provider 发出的每个实体都可以带locationKey它是一个冲突解决键一个不透明字符串对每个实体可能出现的来源位置应唯一。建议设置为能明确标识 provider 及其实例属性的字符串。当两个实体定义共享同一个 entity referencekind、namespace、name时发生冲突location key 按以下规则裁决已有实体没有 location key 时新实体胜出。已有实体有 location key 时只有 location key 匹配新实体才胜出。实体尚不存在时catalog 用提供的 location key 插入它。这套规则防止 rogue provider 抢占属于其他 provider 的实体。验证摄取结果验证依据是生成代码自带的日志行为和官方文档给出的失败现象每次调度刷新成功后日志会打印Read ${entities.length} entities示例值Read 3 entities具体数量取决于你read返回的实体数可以据此确认每次运行是否正常读到了数据。读取或 mutation 抛错时日志打印Refresh failed并附带错误对象——出现该日志说明本次刷新失败需要检查外部系统连接和read实现。如果你发现实体没有出现在 catalog 中先检查read返回的DeferredEntity是否包含backstage.io/managed-by-location和backstage.io/managed-by-origin-location两个注解。文档明确说明缺少注解的实体不会出现在 catalog 中并会生成 warning 日志——出现这类 warning 即是注解缺失的信号。实体经 provider 写入后还要经过处理循环才会成为最终的 stitched 实体这也是 已写入 provider bucket 与 在目录中可见 之间的区别。参考资料Custom entity providers本文对应的官方文档包含完整的UserEntityProvider示例从 HR 系统同步用户实体并附加 Slack 链接Well-known Annotationsbackstage.io/managed-by-location等注解的语义说明CLI 模板源码本文展示代码的原始生成模板Defining config schemas用生成的config.d.ts为app-config.yaml增加 schema【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表