Harbor 端点(Endpoint)删除功能验证:Admin 删除注册表端点的完整测试与实现原理
2026/9/10 8:39:09 网站建设 项目流程

Harbor 端点(Endpoint)删除功能验证:Admin 删除注册表端点的完整测试与实现原理

【免费下载链接】harborAn open source trusted cloud native registry project that stores, signs, and scans content.项目地址: https://gitcode.com/GitHub_Trending/ha/harbor

导读

本文围绕 Harbor 复制(Replication)模块中"端点(Endpoint)删除"这一核心管理操作,以官方测试用例 7-14-Endpoints-endpoints-delete.md 为骨架展开:先梳理该用例的测试目的、环境要求与验证步骤,再深入 Harbor 源码,揭示"被复制规则引用的端点无法删除、未被引用的端点可以删除"这一预期结果背后的引用检查机制、REST API 调用链与数据库设计。读完本文,你将掌握 Harbor 中注册表端点(Registries)删除功能的完整验证方法、底层实现原理,以及如何通过 UI 与 API 两种方式安全地清理不再使用的端点。

一、测试用例定位:端点删除在 Harbor 复制体系中的角色

在 Harbor 的复制(Replication)功能中,"端点(Endpoint)"即注册表(Registry),是复制的源(Source)或目标(Destination)对象。管理员在Administration -> Registries页面维护这些端点的连接信息(类型、URL、访问凭据、是否跳过证书校验等),复制规则(Replication Policy)则在这些端点之间搬运制品(镜像、Chart 等)。

测试用例 7-14 归属于Group7-Replication测试组,其核心目的是:

To verify admin user can delete an endpoint.验证管理员用户可以删除端点。

这条用例表面上只验证"删除"这一个动作,但它真正的技术含量在于验证 Harbor 对端点删除的保护性约束:端点可能正被复制规则或代理缓存项目引用,此时删除必须被拒绝,以防止复制链路出现悬空引用。

测试环境要求

用例明确列出了两条环境前提:

  1. 必须有一个正在运行且可访问的 Harbor 实例(one Harbor instance is running and available);
  2. 至少存在一个端点(At least one endpoint should exist)。

这意味着该用例属于"状态依赖型"验证:要完整跑通"可删除/不可删除"两个分支,环境中至少需要一个被规则引用的端点和一个未被引用的端点。

二、验证步骤详解:如何正确执行端点删除测试

按照用例原文,测试步骤如下:

  1. 以管理员(admin)用户登录 Harbor UI;
  2. Administration -> Registries页面,删除一个**正被复制规则使用(in use by a rule)**的端点;
  3. Administration -> Registries页面,删除一个**未被任何规则使用(not in use by a rule)**的端点。

预期结果(Expected Outcome)

步骤操作对象预期结果
步骤 2被复制规则引用的端点删除失败(endpoint can not be deleted)
步骤 3未被复制规则引用的端点删除成功(endpoint can be deleted)

实操细节补充

在真实环境中执行该验证时,可按如下方式构造前置条件:

  1. 登录 Harbor UI,进入Administration -> Registries,先创建一个远端端点(例如类型选择Docker HubHarbor,填写 URL 与访问凭据);
  2. Administration -> Replications创建一条复制规则,将刚才创建的端点作为源或目标端点(此时该端点即成为"in use by a rule"的状态);
  3. 回到Administration -> Registries页面,分别对"被引用端点"和"未被引用端点"点击删除按钮:
    • 对被引用端点,页面应提示删除失败并给出相应错误信息(如"被复制策略引用,无法删除");
    • 对未被引用端点,删除成功,列表刷新后该端点消失。

三、预期结果背后的源码原理:引用检查三重关卡

为什么"被规则引用的端点删不掉"?这不是 UI 层的简单提示,而是由后端控制器在删除前执行的引用完整性检查强制保证的。下面逐层拆解。

3.1 请求入口:REST API 的权限与路由

端点删除对应的 HTTP 接口为:

DELETE /api/v2.0/registries/{id}

该接口在 Swagger 定义中(见 api/v2.0/swagger.yaml)声明了操作摘要Delete the specific registry,路径参数id为 int64 类型的 Registry ID,并定义了如下响应码:

  • 200:删除成功
  • 401:未认证
  • 403:无权限
  • 404:端点不存在
  • 412前置条件不满足(Precondition Failed)
  • 500:服务器内部错误

其中412正是"端点被引用、无法删除"时的语义化响应码。

服务端实现位于 src/server/v2.0/handler/registry.go:

func (r *registryAPI) DeleteRegistry(ctx context.Context, params operation.DeleteRegistryParams) middleware.Responder { if err := r.RequireSystemAccess(ctx, rbac.ActionDelete, rbac.ResourceRegistry); err != nil { return r.SendError(ctx, err) } if err := r.ctl.Delete(ctx, params.ID); err != nil { return r.SendError(ctx, err) } return operation.NewDeleteRegistryOK() }

可以看到,DeleteRegistry处理函数首先通过RequireSystemAccess(ctx, rbac.ActionDelete, rbac.ResourceRegistry)做 RBAC 校验,只有具备registry资源delete动作权限的系统级用户(如 admin)才能执行删除——这正是测试用例要求"以 admin 登录"的原因。权限通过后,实际删除逻辑委托给控制器registry.Ctl.Delete

3.2 核心逻辑:控制器层的前置条件检查

删除的真正业务逻辑在 src/controller/registry/controller.go 的Delete方法中,它依次做了三次引用计数检查

func (c *controller) Delete(ctx context.Context, id int64) error { // referenced by replication policy as source registry count, err := c.repMgr.Count(ctx, &q.Query{ Keywords: map[string]any{"src_registry_id": id}, }) if err != nil { return err } if count > 0 { return errors.New(nil).WithCode(errors.PreconditionCode). WithMessagef("the registry %d is referenced by replication policies, cannot delete it", id) } // referenced by replication policy as destination registry count, err = c.repMgr.Count(ctx, &q.Query{ Keywords: map[string]any{"dest_registry_id": id}, }) if err != nil { return err } if count > 0 { return errors.New(nil).WithCode(errors.PreconditionCode). WithMessagef("the registry %d is referenced by replication policies, cannot delete it", id) } // referenced by proxy cache project count, err = c.proMgr.Count(ctx, &q.Query{ Keywords: map[string]any{"registry_id": id}, }) if err != nil { return err } if count > 0 { return errors.New(nil).WithCode(errors.PreconditionCode). WithMessagef("the registry %d is referenced by proxy cache project, cannot delete it", id) } return c.regMgr.Delete(ctx, id) }

这三道检查分别对应三种引用关系:

  1. 作为复制规则的源端点:统计replication_policy表中src_registry_id等于该端点 ID 的策略数;
  2. 作为复制规则的目标端点:统计replication_policy表中dest_registry_id等于该端点 ID 的策略数;
  3. 作为代理缓存项目的远端源:统计project表中registry_id等于该端点 ID 的项目数(proxy cache 项目同样以端点为远端仓库)。

任一处计数大于 0,都会返回errors.PreconditionCode(对应 HTTP 412)并附带明确错误消息,最终regMgr.Delete不会被调用,端点保留。只有三次计数全部为 0,才会进入真正的删除动作。

从源码结构可以推断,这里采用的是"检查后删除"的非原子模式(Check-then-Delete):在极端并发场景下仍存在检查与删除之间的竞态窗口,但在常规单管理员操作场景下足够可靠。

3.3 数据层:物理删除与幂等语义

当引用检查全部通过后,调用链继续向下:

  • 管理器层 src/pkg/reg/manager.go 的Delete直接委托 DAO;
  • DAO 层 src/pkg/reg/dao/dao.go 执行真正的 ORM 删除:
func (d *dao) Delete(ctx context.Context, id int64) error { ormer, err := orm.FromContext(ctx) if err != nil { return err } n, err := ormer.Delete(&Registry{ID: id}) if err != nil { return err } if n == 0 { return errors.NotFoundError(nil).WithMessagef("registry %d not found", id) } return nil }

这里有一个值得注意的实现细节:DAO 删除时用n == 0判断删除影响行数,若目标端点本就不存在,会返回NotFoundError(404)。也就是说,重复删除同一端点会得到 404 而非静默成功,这与测试预期中的"可删除"分支在语义上是自洽的。

3.4 单元测试佐证:三种分支全部被覆盖

控制器层 src/controller/registry/controller_test.go 的TestDelete用例完整覆盖了上述三个分支:

func (r *registryTestSuite) TestDelete() { // referenced by replication policy mock.OnAnything(r.repMgr, "Count").Return(int64(1), nil) err := r.ctl.Delete(nil, 1) r.NotNil(err) r.SetupTest() // referenced by proxy cache project mock.OnAnything(r.repMgr, "Count").Return(int64(0), nil) mock.OnAnything(r.proMgr, "Count").Return(int64(1), nil) err = r.ctl.Delete(nil, 1) r.NotNil(err) r.SetupTest() // pass mock.OnAnything(r.repMgr, "Count").Return(int64(0), nil) mock.OnAnything(r.proMgr, "Count").Return(int64(0), nil) mock.OnAnything(r.regMgr, "Delete").Return(nil) err = r.ctl.Delete(nil, 1) r.Nil(err) }
  • 第一段:模拟repMgr.Count返回 1(被复制策略引用)→ 断言删除返回错误;
  • 第二段:模拟复制策略引用为 0、但代理缓存项目引用为 1 → 断言删除返回错误;
  • 第三段:两次引用计数均为 0 → 断言删除成功且regMgr.Delete被调用。

这三个分支与 7-14 用例的预期结果一一对应,属于典型的"UI 测试用例 + 单元测试"双轨验证设计。

四、数据模型:registry 表的演进与引用字段

理解删除保护机制,还需了解底层数据表结构。

4.1 端点表:由 replication_target 演进而来

Harbor 的端点表registry是从早期版本的replication_target表升级而来的。迁移脚本 make/migrations/postgresql/0004_1.8.0_schema.up.sql 记录了这一演进:

ALTER TABLE replication_target RENAME TO registry; ALTER TABLE registry ALTER COLUMN url TYPE varchar(256); ALTER TABLE registry ADD COLUMN credential_type varchar(16); ALTER TABLE registry RENAME COLUMN username TO access_key; ALTER TABLE registry RENAME COLUMN password TO access_secret; ALTER TABLE registry ALTER COLUMN access_secret TYPE varchar(4096); ALTER TABLE registry ADD COLUMN type varchar(32); ALTER TABLE registry DROP COLUMN target_type; ALTER TABLE registry ADD COLUMN description text; ALTER TABLE registry ADD COLUMN health varchar(16); UPDATE registry SET type='harbor'; UPDATE registry SET credential_type='basic';

从这段迁移可见端点表的核心列:url(端点地址,最长 256 字符)、credential_type(认证类型)、access_key/access_secret(访问凭据,secret 最长 4096 字符)、type(端点类型)、descriptionhealth(健康状态)。

4.2 引用字段:复制策略表与项目表

同一迁移脚本将replication_policy表的target_id重命名为dest_registry_id,并新增src_registry_id(见 0004_1.8.0_schema.up.sql):

ALTER TABLE replication_policy ADD COLUMN src_registry_id int; ALTER TABLE replication_policy RENAME COLUMN target_id TO dest_registry_id; ALTER TABLE replication_policy ALTER COLUMN dest_registry_id DROP NOT NULL;

对应的 ORM 模型定义在 src/pkg/replication/model/model.go:

SrcRegistryID int64 `orm:"column(src_registry_id)"` DestRegistryID int64 `orm:"column(dest_registry_id)"`

而代理缓存项目对端点的引用字段registry_id则由迁移脚本 make/migrations/postgresql/0040_2.1.0_schema.up.sql 为project表添加:

ALTER TABLE project ADD COLUMN IF NOT EXISTS registry_id int;

这三处外键式关联(src_registry_id、dest_registry_id、registry_id)正是控制器层三次引用计数检查的统计依据。虽然这些列在数据库中未显式声明外键约束,但 Harbor 通过应用层的引用检查补足了数据完整性保障。

4.3 端点模型与特殊端点 ID

端点模型定义见 src/pkg/reg/model/registry.go,包含 ID、Name、Description、Type、URL、Credential、Insecure、CACertificate、Status 等字段。

需要注意的是,Harbor 将本机 Harbor 实例视为一个特殊端点,其 ID 固定为 0。管理器 src/pkg/reg/manager.go 的Get方法在id == 0时直接返回本地注册表信息(getLocalRegistry,类型为harbor,名称为Local)。因此该端点不参与常规删除流程,删除 API 对 ID 0 也不会命中普通数据行。

五、通过 API 验证端点删除(curl 实践)

UI 操作背后对应的就是 REST API。在已登录拿到会话或认证令牌的前提下,可以等价地用命令行验证 7-14 用例的两个分支:

# 1. 列出所有端点,获取目标端点 ID curl -sk -u admin:password \ "https://<harbor-host>/api/v2.0/registries?page_size=100" # 2. 删除被复制规则引用的端点(预期返回 412 Precondition Failed) curl -sk -u admin:password -X DELETE \ "https://<harbor-host>/api/v2.0/registries/5" \ -w "\nHTTP status: %{http_code}\n" # 3. 删除未被任何规则引用的端点(预期返回 200 OK) curl -sk -u admin:password -X DELETE \ "https://<harbor-host>/api/v2.0/registries/6" \ -w "\nHTTP status: %{http_code}\n"

验证要点:

  • 步骤 2 返回412,响应体包含the registry N is referenced by replication policies, cannot delete it之类的错误消息(具体措辞见控制器实现 controller.go);
  • 步骤 3 返回200,随后再次调用 List 接口(GET /api/v2.0/registries)确认该端点已从列表中消失;
  • 若端点 ID 不存在,则返回404(由 DAO 层的NotFoundError产生)。

六、回归验证与常见问题

6.1 如何把该用例固化为回归测试

7-14 属于手工 UI 用例,其自动化回归可分层实施:

  • 单元层src/controller/registry/controller_test.goTestDelete已覆盖三种分支,go test ./src/controller/registry/...可直接运行;
  • API 层:可参考 src/server/v2.0/handler/registry_test.go 中基于htesting.Suite的 handler 测试模式,用 mock 控制器断言删除接口的权限校验、参数传递与响应码;
  • 端到端层tests/apitests/python目录下提供了基于 Python 的 Harbor API 测试框架,可将其中的 Registries 相关脚本扩展出"先建规则再删端点、先删规则再删端点"的完整流程。

6.2 常见问题排查

现象可能原因处理方式
删除端点时提示"被复制策略引用"该端点被某条复制规则用作源或目标先到Administration -> Replications删除或修改相关规则,再删除端点
删除端点时提示"被代理缓存项目引用"该端点被某个 proxy cache 项目用作远端源先删除/停用对应代理缓存项目,再删除端点
删除返回 404端点 ID 不存在或已被删除通过GET /api/v2.0/registries确认实际 ID
删除返回 403当前账号非系统管理员或缺少registry:delete权限使用具备系统管理权限的账号(如 admin)操作

结语

7-14 用例虽然只有三条测试步骤,但它精准地刻画了 Harbor 端点删除功能的两条核心行为:被引用的端点受到保护、未被引用的端点可被清理。这一行为的实现贯穿 UI(Administration -> Registries)、REST API(DELETE /api/v2.0/registries/{id})、控制器(三次引用计数检查)与数据层(registry表及复制策略、项目表中的引用列)四个层次。理解这条调用链,不仅能让运维人员安全地管理复制端点,也为扩展 Harbor 复制功能或排查删除失败问题提供了清晰的源码级线索。

【免费下载链接】harborAn open source trusted cloud native registry project that stores, signs, and scans content.项目地址: https://gitcode.com/GitHub_Trending/ha/harbor

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询