8.0 KiB
Gitea Runner 协议调度器路线
目标
长期形态不依赖 workflow_job webhook 发现工作。controller 本身作为 Gitea Runner
协议客户端注册,并声明 self-hosted、pod 和 vm labels;单个 registration 内按总
配置容量启动多个 FetchTask goroutine,再将 task 按 runs-on 交给 Pod 或 VM 的独立
容量池,由一次性 Pod 或 microVM 执行。
Gitea RunnerService
│ Register / Declare / FetchTask
▼
dynamic-runner scheduler
│ 已领取的 task + lease
├── Pod executor
└── microVM executor
│ logs / state / result
└──────────────────────► Gitea
controller 使用单一 Go 二进制;默认在同一进程启用 scheduler、pod-worker 和
vm-worker,也可通过 --components 只启用其中一部分。组件是独立应用服务边界,
共享进程不意味着共享后端状态或把 assignment 降级为内存 channel。
首轮集成的 facade pending/claimed registry 与三个组件同进程。虽然二进制保留组件选择 接口,但当前会拒绝“worker 不带 scheduler/facade”的拆分部署:普通 Kubernetes Service 无法保证 executor 回到持有其 assignment 的 replica。后续拆分必须增加按 assignment 路由或可重建的 claim 分发,不能新增一套生命周期数据库来掩盖该问题。
executor 直接运行固定版本的官方 Gitea Runner 二进制,不 fork workflow 执行引擎。
controller 暴露兼容 RunnerService 的 facade:FetchTask 只返回已分配 assignment,
UpdateTask 与 UpdateLog 转发真实 Gitea。facade 同时验证逻辑 SPIFFE ID、assignment
ID,以及由 controller 密钥确定性生成的 assignment HMAC capability;该 capability
只绑定执行实例,不参与 Zot/OpenBao 等业务授权。
executor 为官方 runner 生成与 v3.5.0 schema 一致的一次性 .runner 文件,并以
daemon --once 启动。runner 只访问 executor 内的 loopback HTTP proxy;proxy 使用
go-spiffe 从 Workload API 持续取得和轮换 X509-SVID,再以 mTLS 连接 controller
facade,并严格校验 facade 的 SPIFFE ID。这样无需修改 runner 或把静态客户端证书写入
镜像。assignment capability 会进入一次性 executor 环境,但不会进入 label、annotation
或 OpenSandbox metadata;它只对该 assignment 有效,并且不能绕过 SPIFFE 身份校验。
这与“收到 webhook 后临时注册另一个 act_runner”不同。FetchTask 已经完成任务分配,
不能再期待 Gitea 把同一个 task 分配给随后启动的 runner。协议调度器必须让 executor
执行已经领取的 task,并继续完成日志、状态、心跳、取消和最终结果上报。
设计约束
- 对 workflow 的接口保持
[self-hosted, pod]和[self-hosted, vm]不变。 - scheduler 使用单一 Gitea runner UUID/token 和一个
Declare,不为并发槽位重复注册;POD_CAPACITY + VM_CAPACITY决定并发FetchTaskgoroutine 数量。 - task 领取并持久化后按 backend 进入独立 durable consumer;对应容量池已满时延迟 NAK, assignment 保持 JetStream pending,且不得创建超出配置容量的 workload。
- scheduler Declare 后使用 RunnerService 长轮询;一旦 FetchTask 返回已分配 task,在 JetStream publish 成功前只重试该 assignment,不领取下一项。
- 每个 executor 只执行一个 task,完成后销毁。
- SPIFFE 身份从实际领取的 task 的 repository 和 workflow job key 派生,不需要 queued 与 in-progress webhook 的二阶段关联。
- 身份中的 task 段使用 workflow job key,而不是可带空格的展示名称;job key 必须满足
[A-Za-z_][A-Za-z0-9_-]*。slug + hash 只保留为旧名称的显式迁移后备方案。 - scheduler 只做确定性的身份派生与 executor 绑定,不维护业务授权 policy;Zot、 OpenBao 等资源服务继续是唯一授权决策点。
- scheduler 的 runner registration credential 不进入 executor;executor 只得到执行 当前 task 所需的短期 lease/capability。
- JetStream 只持久化和投递 assignment,不保存 executor 生命周期状态。Pod labels/annotations 与 OpenSandbox metadata 是后端运行状态的权威来源,Gitea 是 task 终态的权威来源。
- assignment 使用版本化 envelope 保存完整 Gitea protobuf task,并从 workflow
runs-on严格选择 pod 或 vm subject;消费者解码后重新派生 backend 与身份,拒绝被篡改的冗余字段。 - JetStream 的 message ID 等于稳定 assignment ID
gitea-task-<task-id>,仅用于发布去重, 不承担 executor 生命周期记录。 - worker 按稳定 assignment ID reconcile 后端资源,进程内只保留并发控制等可丢弃状态; 不新增数据库,也不依赖内存中的 runner-to-executor 映射。
- VM worker 使用 OpenSandbox 官方 Go SDK,并把 assignment ID、repository、job key 和
SPIFFE ID写入 sandbox metadata;通过
extensions.poolRef=ci-vm使用既有 Kata Pool。 - executor 成功 claim 后 ACK assignment。Gitea 接受 terminal update 后,facade 在后端 metadata 写入持久 terminal marker;backend reconciler 仅在执行环境也进入终态后清理, 从而关闭进程重启窗口且避免删除尚未完成结果上报的环境。
- pod 与 vm 使用独立 durable consumer、进程内 admission pool 和并发上限。consumer 只负责将 assignment
幂等落到后端;executor 与身份恢复 metadata 持久化后立即
DoubleAck。尚未取得 Pod UID 等短暂未就绪状态以及临时后端错误使用延迟 NAK。 - admission pool 只保存可重建的并发状态:启动时从 Pod labels/annotations 或 OpenSandbox metadata 恢复非终态 assignment,terminal update 持久化成功后释放槽位,不引入新存储。
- assignment ACK 后的运行、结果回报和清理由 backend reconciler 根据 Kubernetes、 OpenSandbox 与 Gitea 的事实状态驱动,不继续占用 JetStream delivery。
- consumer 在 executor 使用上述 facade 成功 claim task 后确认 assignment;无需把完整 task 写入 Pod annotation、OpenSandbox metadata 或环境变量。
- Pod 与 VM 共享 task/executor 协议,只有环境创建和销毁实现不同。
- scheduler 在 assignment 持久化到 JetStream 后即可继续领取;Pod 与 VM 分别由 durable consumer 的 capacity 限制并发,不共享全局执行槽位。未知后端故障由对应 consumer 的 NAK/redelivery 收敛,不能阻塞另一种 backend。
- 两种 backend 都注入同一份 runner bootstrap 环境;Pod 仍由 homelab Kubernetes 原生 创建,只有 VM 经 OpenSandbox 创建,bootstrap 机制不改变 backend 边界。
实现顺序
- 固定当前 Gitea 版本所使用的 RunnerService protobuf 与 act_runner 版本,记录兼容 范围并建立协议契约测试。
- 实现只注册、Declare labels 和容量感知 FetchTask 的 scheduler spike,暂不执行 task。首次集成必须验证 FetchTask 后、JetStream publish 前进程崩溃时 Gitea 对同一 runner 的 task 恢复语义;该窗口未验证前不能声称 scheduler 可无损恢复。
- 从 act_runner 提取或复用 task 执行与日志上报能力,定义 scheduler 到 executor 的 单任务协议。
- 首先接入 Pod executor,验证成功、失败、取消、超时和 scheduler 重启。
- 接入 microVM executor,并复用同一 task 协议和身份派生逻辑。
- 双轨运行并验证后,移除 webhook receiver、临时 runner 注册和 identity binding subject。
Bootstrap 实现的退出条件
只有同时满足以下条件才能删除 webhook 路径:
- scheduler 能通过 RunnerService 稳定领取并执行 Pod/VM task;
- Gitea UI 中的实时日志、取消、超时和结论与官方 runner 行为一致;
- scheduler 重启不会丢失已领取 task,也不会重复执行;
- SPIFFE 身份只来自实际领取 task;
- 同一套 workflow 无需修改
runs-on即可从 bootstrap 迁移。