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 对端点删除的保护性约束:端点可能正被复制规则或代理缓存项目引用,此时删除必须被拒绝,以防止复制链路出现悬空引用。
测试环境要求
用例明确列出了两条环境前提:
- 必须有一个正在运行且可访问的 Harbor 实例(one Harbor instance is running and available);
- 至少存在一个端点(At least one endpoint should exist)。
这意味着该用例属于"状态依赖型"验证:要完整跑通"可删除/不可删除"两个分支,环境中至少需要一个被规则引用的端点和一个未被引用的端点。
二、验证步骤详解:如何正确执行端点删除测试
按照用例原文,测试步骤如下:
- 以管理员(admin)用户登录 Harbor UI;
- 在
Administration -> Registries页面,删除一个**正被复制规则使用(in use by a rule)**的端点; - 在
Administration -> Registries页面,删除一个**未被任何规则使用(not in use by a rule)**的端点。
预期结果(Expected Outcome)
| 步骤 | 操作对象 | 预期结果 |
|---|---|---|
| 步骤 2 | 被复制规则引用的端点 | 删除失败(endpoint can not be deleted) |
| 步骤 3 | 未被复制规则引用的端点 | 删除成功(endpoint can be deleted) |
实操细节补充
在真实环境中执行该验证时,可按如下方式构造前置条件:
- 登录 Harbor UI,进入
Administration -> Registries,先创建一个远端端点(例如类型选择Docker Hub或Harbor,填写 URL 与访问凭据); - 到
Administration -> Replications创建一条复制规则,将刚才创建的端点作为源或目标端点(此时该端点即成为"in use by a rule"的状态); - 回到
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) }这三道检查分别对应三种引用关系:
- 作为复制规则的源端点:统计
replication_policy表中src_registry_id等于该端点 ID 的策略数; - 作为复制规则的目标端点:统计
replication_policy表中dest_registry_id等于该端点 ID 的策略数; - 作为代理缓存项目的远端源:统计
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(端点类型)、description、health(健康状态)。
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.go的TestDelete已覆盖三种分支,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),仅供参考