Velero 删除插件(Deletion Plugins / DeleteItemAction)设计详解与源码实现
2026/9/15 22:04:57 网站建设 项目流程

Velero 删除插件(Deletion Plugins / DeleteItemAction)设计详解与源码实现

【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero

导读

本篇文章围绕 Velero 的删除插件机制展开,核心解决一个现实痛点:当备份(Backup)被删除时,由备份插件在备份/恢复过程中创建的外部关联资源(如云厂商快照、自定义资源等)可能被遗留为"孤儿资源"。文章先完整还原官方设计文档design/Implemented/deletion-plugins.md的提案思路,再结合当前仓库中已经落地的DeleteItemAction实现,从插件接口、gRPC 框架、备份删除控制器调用链到两个内置实现(DataUpload 删除、CSI VolumeSnapshotContent 删除)进行源码级剖析。读完本文,你将掌握 Velero 删除插件的设计动机、接口契约、注册方式与执行流程,并具备自行编写第三方删除插件的基础能力。

背景:为什么需要一种"删除插件"

Velero 的插件体系此前已经拥有两类核心插件:

  • BackupItemAction(BIA):在备份单个资源时执行,可创建额外的资源来完成备份;
  • RestoreItemAction(RIA):在恢复单个资源时执行,负责还原备份项。

以 CSI 插件为例,Velero 开发者正是借助 BIA/RIA 插件(与 PersistentVolumeClaim 绑定)来创建 VolumeSnapshot、VolumeSnapshotContent 等辅助资源,从而完成卷备份。对于这些辅助资源,当前是在 Velero 核心服务器内部直接做清理(即删除这些 Kubernetes 自定义资源)。

但这种"核心内清理"模式对外部插件并不实用,原因有二(见 design/Implemented/deletion-plugins.md):

  1. Velero 核心无法为所有可能出现的自定义资源都编写清理逻辑;
  2. 插件创建的外部资源并不一定都是 Kubernetes 自定义资源,它们可能存在于任意外部系统中(如对象存储中的快照、第三方服务的资源实例)。

因此,设计文档得出结论:Velero 需要一种机制,让在 BIA/RIA 插件中创建过资源的插件作者,能够保证这些资源在备份被删除时得到清理——无论这些资源位于哪个系统

设计目标与非目标

Goals(目标)

  • 在 Velero 中提供一种新的插件类型,当备份被删除时被调用。

Non Goals(非目标)

  • 不实现具体的删除插件(即核心仓库不负责为每种外部资源编写清理逻辑);
  • 不支持删除插件执行的回滚(rollback)。

这两个"非目标"非常关键:它界定了 Velero 核心只负责提供机制(mechanism),而把具体资源的清理逻辑交给插件作者;同时明确删除操作是"不可撤销"的,这与后面"插件不能阻止删除"的设计约束一脉相承。

高层设计:DeleteAction 插件的核心约定

设计文档为这类新插件命名为DeleteAction,并给出如下高层约定:

  1. 调用时机:当备份被删除时执行;
  2. 输入:插件接收正在被删除的Backup自定义资源;
  3. 不能阻止删除:由于多个DeleteAction插件可以同时注册,且方案不包含回滚/撤销,如果某些插件已经执行、而另一个插件要求停止删除,那么保留下来的备份会处于不一致状态。因此插件只负责"清理",无权中止删除流程;
  4. 按标签匹配(AppliesTo)DeleteAction基于Backup自身的标签决定是否生效。插件可以注册一个AppliesTo函数,它定义了一个作用于 Velero 备份的标签选择器,从而避免执行与自身无关的删除插件;
  5. 执行顺序DeleteAction按插件名称的字母顺序执行。该顺序虽然有些任意,但能为插件作者和用户提供相对可预测的事件顺序。

详细设计:Go 接口提案

设计文档给出了DeleteAction插件的 Go 接口草案(定义于pkg/plugin/velero/deletion_action.go):

type DeleteAction struct { // AppliesTo will match the DeleteAction plugin against Velero Backups that it should operate against. AppliesTo() // Execute runs the custom plugin logic and may connect to external services. Execute(backup *api.backup) error }

同时,文档提议在clientmgmt.Manager接口(即 pkg/plugin/clientmgmt/manager.go)中新增方法:

type Manager interface { ... // GetDeleteActions returns the registered DeleteActions. //TODO: do we need to get these by name, or can we get them all? GetDeleteActions([]velero.DeleteAction, error) ...

需要指出的是,设计文档此处标注为Status: Alternative Proposal(备选提案),接口为草案形态。在仓库中落地时,接口演化为下文介绍的DeleteItemAction

从提案到落地:当前仓库中的 DeleteItemAction 接口

设计文档的DeleteAction最终在仓库中实现为DeleteItemAction,定义在 pkg/plugin/velero/delete_item_action.go:

// DeleteItemAction is an actor that performs an operation on an individual item being restored. type DeleteItemAction interface { // AppliesTo returns information about which resources this action should be invoked for. // A DeleteItemAction's Execute function will only be invoked on items that match the returned // selector. A zero-valued ResourceSelector matches all resources. AppliesTo() (ResourceSelector, error) // Execute allows the ItemAction to perform arbitrary logic with the item being deleted. // An error should be returned if there were problems with the deletion process, but the // overall deletion process cannot be stopped. // Returned errors are logged. Execute(input *DeleteItemActionExecuteInput) error }

与提案相比,落地版本有以下关键演变:

  1. 名称DeleteActionDeleteItemAction,语义上强调"针对备份中的每个资源项(item)执行";
  2. AppliesTo 返回值:从无返回值变为(ResourceSelector, error)ResourceSelector定义在 pkg/plugin/velero/shared.go,包含IncludedNamespacesExcludedNamespacesIncludedResourcesExcludedResourcesLabelSelector等字段,用于精细控制插件作用于哪些资源;零值(空选择器)表示匹配所有资源;
  3. Execute 入参:从只接收Backup变为接收结构体DeleteItemActionExecuteInput(见 pkg/plugin/velero/delete_item_action.go):
type DeleteItemActionExecuteInput struct { // Item is the item taken from the pristine backed up version of resource. Item runtime.Unstructured // Backup is the representation of the restore resource processed by Velero. Backup *velerov1api.Backup }

也就是说,实际执行时插件不仅能拿到正在被删除的Backup,还能拿到备份归档中的每一个资源对象(Unstructured),这正是设计文档中"确保这些资源被删除"诉求的具体落点:插件从备份包中逐个取出由自己创建的资源,执行外部清理。

Manager 接口中的落地实现

设计文档提议的GetDeleteActions在 pkg/plugin/clientmgmt/manager.go 中以GetDeleteItemActions实现:

// GetDeleteItemActions returns all delete item actions as restartableDeleteItemActions. func (m *manager) GetDeleteItemActions() ([]velero.DeleteItemAction, error) { list := m.registry.List(common.PluginKindDeleteItemAction) actions := make([]velero.DeleteItemAction, 0, len(list)) for i := range list { id := list[i] r, err := m.GetDeleteItemAction(id.Name) if err != nil { return nil, err } actions = append(actions, r) } return actions, nil }

该方法从插件注册表(registry)中按PluginKindDeleteItemAction类型列出全部已注册插件(插件类型常量定义在 pkg/plugin/framework/common/plugin_kinds.go),并为每个插件构造"可重启"(restartable)的客户端包装后批量返回——这也回答了设计文档中//TODO: do we need to get these by name, or can we get them all?的疑问:当前实现一次性返回全部已注册的删除插件

执行链路:备份删除控制器如何调用删除插件

DeleteItemAction的真正触发点位于备份删除控制器 pkg/controller/backup_deletion_controller.go。当用户删除一个 Backup 时(实际通过DeleteBackupRequest驱动),控制器的处理流程大致如下:

  1. 前置校验:检查备份存储位置(BackupStorageLocation)是否处于只读模式或不可用状态(见 pkg/controller/backup_deletion_controller.go);
  2. 状态流转:将DeleteBackupRequest置为InProgress,并给请求打上velero.io/backup-namevelero.io/backup-uid标签;同时把Backup的阶段置为Deleting(见 pkg/controller/backup_deletion_controller.go);
  3. 获取插件:通过pluginManager.GetDeleteItemActions()获取全部删除插件(见 pkg/controller/backup_deletion_controller.go);
  4. 下载备份包:如果存在已注册的删除插件,则从对象存储下载备份 tarball(downloadToTempFile)。若下载失败(如 tarball 不存在),则跳过删除插件,仅做 CSI VolumeSnapshot 的离线清理(见 pkg/controller/backup_deletion_controller.go);
  5. 构造上下文并调用:创建delete.Context(包含 Backup、BackupReader、Actions、DiscoveryHelper、Filesystem 等),然后调用delete.InvokeDeleteActions(deleteCtx)真正执行插件(见 pkg/controller/backup_deletion_controller.go);
  6. 后续清理:插件执行完成后,控制器继续清理 PV 快照、Pod 卷快照、DataMover 移动的数据等(见 pkg/controller/backup_deletion_controller.go)。

InvokeDeleteActions 的内部实现

InvokeDeleteActions定义在 internal/delete/delete_item_action_handler.go,其工作流程是理解删除插件执行语义的核心:

  1. 解析动作:通过framework.NewDeleteItemActionResolver(ctx.Actions).ResolveActions(...)将插件与其AppliesTo选择器解析为DeleteItemResolvedAction。若没有任何插件且无错误,直接返回,删除流程照旧——这就是兼容性的来源;
  2. 解包备份:将备份 tarball 解压到临时目录(archive.NewExtractor(...).UnzipAndExtractBackup),再通过archive.NewParser(...).Parse解析出备份中的资源清单;
  3. 按资源遍历:对每个 group/resource、每个命名空间、每个具体 item,先通过getApplicableActions按 groupResource 与 namespace 过滤出候选插件,再对每个插件校验action.Selector.Matches(labels.Set(obj.GetLabels()))——即用资源对象自身的标签去匹配插件的 LabelSelector(见 internal/delete/delete_item_action_handler.go);
  4. 执行与容错:逐个调用action.DeleteItemAction.Execute(&velero.DeleteItemActionExecuteInput{Item: obj, Backup: ctx.Backup})即使某个插件执行出错,循环也会继续,保证单个失败插件不会阻断其余资源的清理;但错误会被聚合收集(kubeerrs.NewAggregate(deleteErrs))并返回给控制器,导致本次删除失败以便后续重试——因为插件失败往往意味着其管理的外部产物(如 DataMover 仓库快照)可能未被删除,若此时直接删除备份元数据,会造成永久孤儿资源(相关注释见 internal/delete/delete_item_action_handler.go)。

这里值得对照设计文档的一个细节:"DeleteActions 将按插件名称的字母顺序执行"。在实现中,插件的遍历顺序取决于GetDeleteItemActions从注册表取回的列表顺序,而在单个资源 item 上,多个插件的执行顺序由 resolvedActions 的顺序决定;另一方面,控制器里对所有资源项是无序遍历的。因此"可预测的顺序"更多体现在"同一资源上多个插件的执行次序"这一层面。

gRPC 插件框架:删除插件的进程外通信

与 BIA/RIA 插件一致,DeleteItemAction也走 Velero 的 gRPC 插件框架(基于 hashicorp/go-plugin):

  • 插件类型封装:DeleteItemActionPlugin实现了 go-plugin 的Plugin接口,GRPCServer注册DeleteItemActionGRPCServer(见 pkg/plugin/framework/delete_item_action.go);
  • gRPC 服务端:DeleteItemActionGRPCServer.AppliesTo调用实现者的AppliesTo()并把ResourceSelector序列化为 proto 响应;Execute则把请求中的Item(Unstructured JSON)与Backup反序列化后封装为DeleteItemActionExecuteInput交给实现者(见 pkg/plugin/framework/delete_item_action_server.go);
  • proto 契约定义在 pkg/plugin/proto/DeleteItemAction.proto,生成代码位于 pkg/plugin/generated/DeleteItemAction.pb.go;
  • 客户端侧还有restartable_delete_item_action.go负责插件进程崩溃后的自动重启逻辑。

这意味着第三方插件作者可以完全独立于 Velero 核心进程开发删除插件,通过标准插件协议注册即可——这正是设计文档中"核心无法为所有自定义资源扩展"问题的最终解法。

内置实现:仓库中的两个 DeleteItemAction 实例

当前仓库在 pkg/cmd/server/plugin/plugin.go 内置注册了两个删除插件,可作为插件作者的参考范例:

RegisterDeleteItemAction( "velero.io/dataupload-delete", newDateUploadDeleteItemAction(f), ). RegisterDeleteItemAction( "velero.io/csi-volumesnapshotcontent-delete", newVolumeSnapshotContentDeleteItemAction(f), )

示例一:DataUploadDeleteAction

实现位于 pkg/datamover/dataupload_delete_action.go,职责是在 DataMover 数据移动场景下,为被删除备份对应的 DataUpload 生成"快照信息 ConfigMap",供备份删除控制器后续定位并清理 Kopia 快照。其AppliesTo声明只作用于datauploads.velero.io资源:

func (d *DataUploadDeleteAction) AppliesTo() (velero.ResourceSelector, error) { return velero.ResourceSelector{ IncludedResources: []string{"datauploads.velero.io"}, }, nil }

Execute中有一个非常值得学习的"防御性归属校验":它检查 DataUpload 上的velero.io/backup-name标签是否与正在删除的 Backup 一致,只有当归属匹配时才创建 ConfigMap。原因(见 pkg/datamover/dataupload_delete_action.go)在于:若标签缺失或指向其他备份(例如用户把 velero 命名空间也纳入了备份,导致 DataUpload CR 被卷入他人备份包),凭空创建带错误标签的 ConfigMap 会让真正的属主备份删除时查不到快照信息,从而在对象存储中泄漏 Kopia 快照。此外,该插件的单元测试位于 pkg/datamover/dataupload_delete_action_test.go。

示例二:VolumeSnapshotContentDeleteItemAction

实现位于 internal/delete/actions/csi/volumesnapshotcontent_action.go,职责是清理 CSI 快照背后的云存储快照。其AppliesTo声明作用于volumesnapshotcontents.snapshot.storage.k8s.io(见 internal/delete/actions/csi/volumesnapshotcontent_action.go):

func (p *volumeSnapshotContentDeleteItemAction) AppliesTo() (velero.ResourceSelector, error) { return velero.ResourceSelector{ IncludedResources: []string{"volumesnapshotcontents.snapshot.storage.k8s.io"}, }, nil }

它的Execute展示了删除插件的典型"三步走"模式:

  1. 把 Unstructured item 转换为强类型对象VolumeSnapshotContent
  2. 校验资源归属——不删除 Velero 之外创建的 VolumeSnapshotContent,通过检查其标签是否带有所删备份的名称(kubeutil.HasBackupLabel,见 internal/delete/actions/csi/volumesnapshotcontent_action.go);
  3. 执行真实清理——优先尝试删除集群中遗留的原始 VSC;若不存在,则创建一个临时的、DeletionPolicy=Delete、指向原SnapshotHandle的 VSC,以触发云厂商删除底层快照(见 internal/delete/actions/csi/volumesnapshotcontent_action.go)。

这两个实例共同印证了设计文档的核心主张:删除插件负责"跨系统的资源清理",而 Velero 核心只提供调度与执行框架

兼容性

设计文档在 Compatibility 一节明确指出:向后兼容应当是直截了当的——如果没有安装任何DeleteAction插件,备份删除流程将和今天完全一致

这一点在实现中得到双重印证:

  1. 控制器层:GetDeleteItemActions()返回空列表时,len(actions) > 0为假,整个下载备份包、调用插件的分支被跳过(见 pkg/controller/backup_deletion_controller.go);
  2. 处理器层:InvokeDeleteActionslen(ctx.resolvedActions) == 0 && err == nil时直接返回,"No delete item actions present, proceeding with rest of backup deletion process"(见 internal/delete/delete_item_action_handler.go)。

因此,升级到带有删除插件的 Velero 版本不会改变既有备份的删除行为。

已知问题与后续演进

设计文档的 Open Issues 一节记录了一个当时尚未解决的问题:为了给 Backup 打上自定义标签(供AppliesTo的标签选择器使用),Backup 对象必须在BackupItemActionRestoreItemAction插件内部可被修改——而当时它不可修改。文档给出的临时变通方案是:用户在创建备份时手动打标签,但这并不理想。

从落地实现看,这个问题实际上被"重定向"解决了:最终接口并未依赖"给 Backup 打标签",而是把AppliesTo的匹配粒度下沉到了备份包内的资源对象标签——插件在Execute中拿到具体 item 后,再通过资源自身的标签(如velero.io/backup-name)做归属校验。设计文档中"按 Backup 标签匹配"的思路演化为"按资源选择器 + 资源标签归属校验"的双重机制,既保证了插件只处理自己创建的资源,也避免了对核心 Backup 对象修改能力的依赖。

另外,设计文档中标记为 TODO 的Alternatives Considered(备选方案)与Security Considerations(安全考量)部分,在公开设计阶段未补充细节;从实现看,安全性主要体现在插件执行错误会被聚合上报(不吞错)、资源归属校验(不误删他人资源)、以及临时目录的清理(defer ctx.Filesystem.RemoveAll(dir))等方面。

小结

回顾整个设计到落地的过程,可以提炼出删除插件机制的三大设计支柱:

设计要点设计文档表述落地实现位置
新插件类型DeleteActionDeleteItemAction,见 pkg/plugin/velero/delete_item_action.go
匹配机制AppliesTo标签选择器AppliesTo() (ResourceSelector, error)+ 资源标签校验
调用时机备份删除时执行备份删除控制器 pkg/controller/backup_deletion_controller.go
插件注册注册表 + Manager 方法GetDeleteItemActions,见 pkg/plugin/clientmgmt/manager.go
执行引擎高层面言"删除时运行"internal/delete/delete_item_action_handler.go
兼容性无插件则流程不变空列表短路,行为不变

对于希望为 Velero 生态贡献删除插件的开发者,参考路径非常清晰:实现AppliesTo()声明目标资源、在Execute()中编写跨系统清理逻辑、用RegisterDeleteItemAction注册插件,然后由 Velero 在每次备份删除时自动为你清理外部遗留资源。相关的测试用例(如 internal/delete/delete_item_action_handler_test.go、pkg/plugin/clientmgmt/manager_test.go)完整覆盖了"选择器匹配""标签过滤""多插件协同""插件报错不中断"等关键场景,可作为理解行为边界的补充阅读材料。

【免费下载链接】veleroBackup and migrate Kubernetes applications and their persistent volumes项目地址: https://gitcode.com/GitHub_Trending/ve/velero

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

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

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

立即咨询