22 KiB
Job API v1alpha1 草案
- 状态:Draft
- 日期:2026-09-17
- API group:
execution.ayatori.ddupan.top - Kind:
Job - Scope:Namespaced
目标
Job 表达一次有限时长、有明确退出结果的机器执行。调用者描述任务载荷和资源需求,平台
选择 execution backend 并持续观察,直到任务成功、失败或取消。
首个 adapter 使用 Kubernetes batch/v1 Job,第二个 adapter 使用 OpenSandbox lifecycle
与 execd API。API 不暴露 PodSpec、sandbox ID 创建参数或具体 adapter 配置,但允许表达两个
真实后端共有的 OCI image、进程、环境变量和资源语义。
Job 是短生命周期控制对象。完成后依据 ttlSecondsAfterFinished 回收,长期业务状态由调用
者保存,日志由 observability 平台保存。详细保留策略见 ADR-0005。
非目标
v1alpha1 不提供:
- DAG、workflow 或多步骤 task;
- 定时执行与可复用 Job template;
- 并行 completions、indexed job 或 gang scheduling;
- 自动业务重试;
- 暂停后恢复;
- 交互式 shell、endpoint、snapshot 或长生命周期 sandbox;
- workspace、cache、artifact 上传协议或结构化 task outputs;
- 永久 Job history。
上述能力应由后续独立资源或经过真实需求验证的兼容字段提供,不能通过透传 PodSpec 或 OpenSandbox extensions 提前进入 API。
示例
apiVersion: execution.ayatori.ddupan.top/v1alpha1
kind: Job
metadata:
generateName: hello-
namespace: ci
spec:
jobClassName: default
task:
image: docker.io/library/alpine:3.22
imagePullSecrets: []
command: ["/bin/sh", "-c"]
args:
- echo "hello ${TARGET}"
workingDir: /workspace
env:
- name: TARGET
value: world
- name: TOKEN
valueFrom:
secretKeyRef:
name: example-token
key: token
- name: CONFIG_VALUE
valueFrom:
configMapKeyRef:
name: example-config
key: value
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
activeDeadlineSeconds: 600
ttlSecondsAfterFinished: 3600
desiredState: Running
Spec
spec:
jobClassName: string
task: TaskSpec
resources: ResourceRequirements
activeDeadlineSeconds: int64
ttlSecondsAfterFinished: int32
desiredState: Running | Cancelled
jobClassName
可选。引用平台管理员维护的 cluster-scoped JobClass。调用者选择服务等级或执行
能力,而不是直接选择 adapter driver。省略时由 namespace policy 解析默认 class;不存在
默认值时 Job 保持未接受状态,不得静默选择任意后端。
一旦 Job 被接受,该字段不可变。解析出的实际 class 写入 status,以便默认策略后来变化时仍 能恢复原执行。
JobClass 及其强类型 parameters API 定义后端配置和调度策略。Job API 不暴露
Kubernetes namespace、OpenSandbox endpoint、API key 或 backend raw configuration。
task
必填,创建后不可变。
task:
image: string
imagePullSecrets: []LocalObjectReference
command: []string
args: []string
workingDir: string
env: []EnvVar
image:必填 OCI image reference。首版允许 tag;生产策略可以通过 admission 要求 digest。imagePullSecrets:可选,同 namespace 的私有 registry 凭据引用。command:可选,覆盖 image entrypoint;空值表示使用 image 默认值。args:可选,传给 entrypoint/command。workingDir:可选;为空时使用 image/backend 默认值。env:可选,名称必须唯一。
command 和 args 使用 argv 语义,不隐式经过 shell。需要 shell 展开时,调用者必须显式
指定 /bin/sh -c 等命令。
环境变量
- name: EXAMPLE
value: literal
- name: TOKEN
valueFrom:
secretKeyRef:
name: example
key: token
optional: false
value 与 valueFrom 必须且只能设置一个。v1alpha1 支持同 namespace 的 SecretKeyRef 和
ConfigMapKeyRef,两者具有相同的引用、optional 和等待语义。Adapter 负责以适合后端且不
写入 Job status 的方式传递值。引用对象或 key 缺失时 Job 保持未开始并通过 Condition 报告。
Secret 内容不得复制到 Event、日志或 backend reference;ConfigMap 值虽然不视为机密,也不
写入 status,避免状态膨胀和不同后端行为不一致。
Kubernetes adapter 保留原生 SecretKeyRef/ConfigMapKeyRef,由 kubelet 在执行节点解析,
controller 不读取内容。OpenSandbox create API 只接收已经解析的环境变量值,因此该 adapter
必须读取引用并把值放入 sandbox create request。JobClass 必须明确允许 Secret 的这种
传递路径,且 controller 的日志、Event 和 status 不得记录请求正文。未来需要避免把真实凭据
暴露给 sandbox 进程时,使用 OpenSandbox Credential Vault 或 Ayatori 独立 Credential 能力,
而不是改变 SecretKeyRef 的既有语义。
私有镜像凭据采用 Kubernetes kubernetes.io/dockerconfigjson Secret。Kubernetes adapter
直接传递引用;OpenSandbox adapter 选择与目标 image registry 匹配的条目,并映射到其
image.auth create 参数。无法解析、没有匹配 registry 或所选 OpenSandbox runtime 不支持
per-request image auth 时,Job 以明确 reason 失败,不得退回匿名拉取后隐藏真实原因。
resources
可选,使用 Kubernetes resource.Quantity 表示数值,但不复用完整 Pod
ResourceRequirements 行为。
v1alpha1 支持 cpu 和 memory 的 requests/limits。Requests 表达准入与调度需求,limits
表达执行上限。JobClass 可以提供默认值和允许范围;解析后的实际资源写入 status。
Adapter 必须显式验证能否满足请求,不能无提示地忽略 limit。后端无法区分 request 与 limit
时,其映射规则属于 JobClass,并在 Job 接受前确定。对 OpenSandbox,CPU 和内存
limits 直接映射为 sandbox VM/container 的 resourceLimits;requests 用于 Ayatori 的准入与
调度,并可由 JobClass 映射到后端 resource request 或 capacity profile。映射失败必须
显式拒绝或失败,不能静默降低资源保证。
activeDeadlineSeconds
可选,必须大于零。表示从实际执行开始到任务必须终止的最长时间,不包含排队、class 解析或
后端 provisioning 时间。到期后 controller 请求终止后端,最终以 Succeeded=False、
reason=DeadlineExceeded 结束。
后端自身的 timeout 可以作为执行机制,但 Ayatori controller 仍以 status.startTime 和观察
结果维护领域语义。调度等待超时是不同概念,v1alpha1 不提供。
ttlSecondsAfterFinished
可选,必须大于或等于零。语义与 Kubernetes Job 一致:从终态 transition time 起计算,零 表示立即具备删除资格。该字段在任务完成前后均可修改,但不能保证在既有 TTL 已过期后通过 延长 TTL 阻止并发删除。
平台应通过 schema、CEL 或 policy 设置最大值和推荐默认值。controller 本身不偷偷填充一个 无法从 spec 观察到的永久策略。
desiredState
可选,默认 Running。允许的状态迁移只有:
Running → Cancelled
设置 Cancelled 表示请求终止当前执行并保留 Job 至 TTL 到期。取消是尽力而为的异步操作;
只有 adapter 确认执行不会继续后,Job 才进入终态。字段不得从 Cancelled 改回 Running。
重新执行必须创建新的 Job。
v1alpha1 不提供 suspend/resume。对任意后端可靠实现 checkpoint/resume 并非共同能力,且暂停 不应被伪装为取消。
不可变性
创建后仅允许修改:
spec.desiredState,且只能单向变为Cancelled;spec.ttlSecondsAfterFinished。
task、resources、activeDeadlineSeconds 和 jobClassName 均不可变。首选 CRD CEL
validation 表达这些约束;只有 schema/CEL 无法正确表达时才引入 admission webhook。
Controller reconcile 的技术重试不表示任务重跑。v1alpha1 每个 Job 最多启动一个逻辑执行; adapter 必须使用 Job UID 作为幂等键。若请求结果未知,controller 必须先 Observe,不能因为 网络超时重新创建可能已经开始的执行。
首个 Kubernetes adapter 创建 backoffLimit: 0、restartPolicy: Never 的原生 Job,避免继承
Kubernetes 默认的多次业务执行语义。需要重新执行时创建新的 Ayatori Job。
Status
status:
observedGeneration: 1
conditions:
- type: Accepted
status: "True"
reason: Valid
observedGeneration: 1
lastTransitionTime: ...
- type: Scheduled
status: "True"
reason: BackendCreated
observedGeneration: 1
lastTransitionTime: ...
- type: Succeeded
status: "Unknown"
reason: Running
observedGeneration: 1
lastTransitionTime: ...
resolvedJobClass:
name: default
uid: 8aa4...
controllerName: execution.ayatori.ddupan.top/kubernetes
parametersRef:
group: execution.ayatori.ddupan.top
kind: KubernetesExecutionParameters
name: default
uid: c413...
effectiveResources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
execution:
adapter: kubernetes
references:
- type: Job
id: 5cb0...
startTime: ...
completionTime: ...
result:
exitCode: 0
reason: Completed
Conditions
使用标准 metav1.Condition。v1alpha1 定义三个核心 Condition:
Accepted:spec、引用、policy 和 JobClass 已解析,可进入调度;Scheduled:后端已确定并存在可观察的逻辑执行;Succeeded:任务结果。Unknown表示尚未结束,True表示成功,False表示已经失败或 取消。
失败不使用单独的 Failed Condition。Succeeded=True 与 Failed=True 会形成需要额外维护的
互斥状态,而标准三态 Condition 已能完整表达一次执行:运行中为 Unknown、成功为 True、
失败为 False。失败类型由稳定 reason 区分;这与 Tekton TaskRun 的状态约定一致。
不增加与 Conditions 重复的 phase 字段。面向 CLI 的阶段摘要由 printer columns 或客户端从
Conditions 推导,避免两个状态源发生漂移。
常用 Succeeded reason 初始包括:
Pending、Scheduling、Running;Completed;ProcessFailed;DeadlineExceeded;Cancelled;BackendLost;ResultUnknown。
Reason 是稳定、机器可读的 PascalCase 标识;message 面向人类且不得承载程序逻辑。
状态机
状态机名称用于设计、测试和 metrics,不增加持久化 status.phase。当前状态必须能够从 spec、
deletionTimestamp、Conditions、时间和 execution references 唯一推导。
状态定义
| 状态 | 判定摘要 | 含义 |
|---|---|---|
Resolving |
Accepted!=True,非终态 |
等待 JobClass、Secret、ConfigMap 或 policy 解析 |
Scheduling |
Accepted=True、Scheduled!=True |
选择 adapter 并幂等创建后端执行 |
Starting |
Scheduled=True、无 startTime |
后端已存在,任务主体尚未确认开始 |
Running |
有 startTime、Succeeded=Unknown |
任务主体正在执行 |
Cancelling |
desiredState=Cancelled、非终态 |
正在确认后端已经停止 |
ResultUnknown |
Succeeded=Unknown/ResultUnknown |
无法证明任务仍在运行或已经停止 |
Succeeded |
Succeeded=True |
成功终态 |
Failed |
Succeeded=False,reason 非 Cancelled |
失败终态 |
Cancelled |
Succeeded=False/Cancelled |
取消终态 |
Deleting |
存在 deletionTimestamp |
finalizer 正在停止并清理后端,覆盖其他状态 |
存在多个判定条件时按 Deleting → terminal → Cancelling → ResultUnknown → Running → Starting → Scheduling → Resolving 的优先级推导,保证状态唯一。
ResultUnknown 不是终态,不设置 completionTime,也不启动 TTL。只有确认任务已经停止,才能
转为成功、失败或取消。暂时无法联系后端不等于后端执行失败。
正常转移
Resolving
│ 引用与策略解析完成
▼
Scheduling
│ 后端逻辑执行已建立并持久化引用
▼
Starting
│ adapter 确认任务主体开始
▼
Running ───────────────→ Succeeded
└──────────────────→ Failed
后端在任务主体开始前就确定失败,例如 image pull、runtime 不兼容或 provisioning 失败,可以从
Scheduling 或 Starting 直接进入 Failed,此时 startTime 允许为空。
取消转移
Resolving ─┐
Scheduling ─┤
Starting ─┼→ Cancelling → Cancelled
Running ─┤
ResultUnknown ─┘
尚未创建后端执行时,取消可以立即确认。已经存在或可能存在后端执行时,必须反复执行
Cancel/Observe,确认不会继续运行后才能进入 Cancelled。取消请求与成功完成并发时,以先从
后端确认到的不可逆事实为准:已经成功完成的任务保持 Succeeded,不能改写成 Cancelled。
不确定结果与恢复
Ensure/Observe 返回歧义
↓
ResultUnknown
├── 找回执行 → Starting / Running
├── 找到终态 → Succeeded / Failed / Cancelled
└── 管理员确认无法继续 → Failed(BackendLost)
在 Ensure 请求超时且尚未成功写入 external reference 时,adapter 必须使用 Job UID 查询后端,
不能直接再次创建。Controller 重启后遵循相同规则。
删除与 TTL 转移
任意状态收到 deletionTimestamp 后进入 Deleting。若执行可能存在,先 Cancel/Observe,再
Delete 后端资源并移除 finalizer。TTL controller 只对具有 completionTime 的三个终态发起
删除;Resolving、Scheduling、Starting、Running、Cancelling 和 ResultUnknown 均不
具备 TTL 删除资格。
状态不变量
Succeeded=True/False是不可逆终态;终态 reason、completionTime和 result 不再改变。startTime和completionTime一旦设置不可改变;两者都存在时 completionTime 不早于 startTime。Succeeded=True必须具有completionTime,可以没有 exit code,但 adapter 应说明原因。Succeeded=False必须具有completionTime;进程失败且能取得退出码时必须保存 exit code。- 非终态的
Succeeded必须为Unknown,不得省略为具有歧义的空状态。 Scheduled=True前不得设置startTime;一旦为 True 不再回退。- execution references 只能由 controller 写入;已有引用不能静默替换成新的逻辑执行。
- Job UID 是执行幂等键;同名但不同 UID 的 Job 必须被视为不同执行。
desiredState=Cancelled后不得创建新的后端执行。- reconcile 错误和退避不得修改任务的业务结果。
状态机测试矩阵
实现必须至少覆盖以下 table-driven unit tests,并为关键恢复路径提供 envtest:
| 类别 | 场景 | 必要断言 |
|---|---|---|
| 正常 | 创建、开始、退出 0 | 单次 Ensure,时间与成功终态正确 |
| 正常 | 主进程非零退出 | Succeeded=False/ProcessFailed 与 exit code |
| 解析 | JobClass 后创建 | 不提前 Ensure,引用出现后继续 |
| 解析 | Secret/ConfigMap 或 key 后创建 | 不泄露值,解析后只启动一次 |
| 后端 | image pull 或 provisioning 失败 | 未设置 startTime 的失败终态合法 |
| 幂等 | Ensure 成功但 status 写入前崩溃 | 通过 UID 找回,不能创建第二次执行 |
| 幂等 | 重复 reconcile 与重复事件 | 不产生额外执行,不改变终态时间 |
| 恢复 | controller 在各非终态重启 | 从持久 status/reference 恢复正确状态 |
| 未知 | Ensure/Observe 超时且结果不明 | 保持非终态,不设 completionTime,不触发 TTL |
| 未知 | 后端恢复后找回运行任务 | 从 ResultUnknown 返回 Running |
| 未知 | 管理员确认执行丢失 | 只在确认后进入 Failed/BackendLost |
| 取消 | 在解析、调度、启动、运行阶段取消 | 不再创建或确认停止后才进入 Cancelled |
| 竞态 | 取消与成功完成并发 | 已确认成功不被取消覆盖 |
| 超时 | active deadline 到期 | 请求取消,确认停止后 DeadlineExceeded |
| 删除 | 每个非终态阶段删除 | finalizer 清理完成前对象不消失 |
| 删除 | 后端暂时不可达 | finalizer 保留并重试,不误报已清理 |
| TTL | 三种终态到期 | 到期前不删,到期后带 UID precondition 删除 |
| TTL | controller 在等待 TTL 时重启 | informer 恢复计时,最终删除一次 |
| TTL | 到期附近延长 TTL | 最终 GET 重新核对最新 TTL |
| 隔离 | 同名 Job 删除并以新 UID 重建 | 旧队列项和旧后端不得影响新 Job |
| 校验 | 修改不可变字段或取消后恢复 Running | schema/CEL 拒绝请求 |
| 引用 | OpenSandbox 保存 Sandbox 与 Command 引用 | 顺序重试后引用稳定且无凭据 |
时间
startTime:adapter 确认任务主体开始执行的时间,而不是 CR 创建或 backend provisioning 时间;设置后不可改变。completionTime:进入最终成功、失败或取消状态的时间;设置后不可改变。
TTL 以 completionTime 为基准。若后端已经完成但结果暂时无法确认,不得猜测 completionTime。
Execution reference
status.execution 是 execution 领域定义的正式 API 字段,保存 controller 重启后重新 Observe
所需的最小稳定引用:
adapter:实际 adapter 类型;references:一个或多个由 adapter 定义的不透明外部引用。
execution:
adapter: opensandbox
references:
- type: Sandbox
id: sandbox-123
- type: Command
id: command-456
单个 externalID 不足以表达 OpenSandbox 的 sandbox 与 command 两级资源。type 和 id 的
值由对应 adapter 定义,调用者只能用于诊断和关联,不能据此实现领域逻辑。execution 领域将
每个引用限制为 type 与 id 两个非空、有长度上限的字符串,不提供任意 metadata map 或
raw JSON。引用不包含 endpoint、凭据或 Secret 内容。Kubernetes adapter 可以另外通过 owner
reference 管理原生 Job,但仍需把恢复所需引用持久化,并保证同名重建安全。
该结构不提升为跨领域共享的万能 ExternalReference。VM、数据库和 LB 等领域根据真实后端 需要定义自己的受限引用 schema,只有多个领域出现语义完全一致的实际重复后才考虑共享。
Result
result.exitCode 只在后端能够确定主进程退出码时设置。调度失败、取消、后端丢失等情况可以
没有退出码。result.reason 提供简短分类;详细诊断写入 Condition message 和 observability,
不得把完整日志写入 status。
Job UID 是跨后端日志、metrics 和 traces 的主要 correlation identity。Adapter 必须将 namespace、name 和 UID 传入执行环境或后端 metadata;高基数字段如何索引由 observability 平台决定,API 不要求把 UID 配置为日志 label。
删除与 finalizer
Execution controller 在可能创建外部执行前添加
execution.ayatori.ddupan.top/job-cleanup finalizer。
删除一个活动 Job 表示取消并清理,而不是 orphan:
- 请求 adapter 终止执行;
- Observe,确认执行不会继续;
- 删除后端临时资源与短期凭据;
- 移除 finalizer。
首版不提供用户可选 orphan policy。让一次性任务脱离控制面继续运行既难以观察,也可能产生 副作用。后端长期不可达时由管理员根据 runbook 判断并强制移除 finalizer,该操作必须可审计。
TTL controller 只发起 Job 删除,所有手工删除和 TTL 删除都经过相同 finalizer 路径。
Adapter contract 对 API 的保证
每个 execution adapter 必须提供以下语义,而非暴露自身 SDK 类型:
Ensure 幂等地建立以 Job UID 标识的一个逻辑执行
Observe 返回尚未开始、运行、成功、失败、取消或结果未知
Cancel 请求停止且可被重复调用
Delete 清理后端临时资源且可被重复调用
Ensure 的网络超时不能直接触发第二次执行。Adapter 必须能够通过 UID/metadata 查找已创建的
后端对象,或返回 ResultUnknown 交由人工处理。
Kubernetes adapter 与 OpenSandbox adapter 实现后,应复审 contract 和 API。只有两个真实
实现都需要且语义相同的字段才提升为通用能力;后端特有功能优先进入 JobClass 或独立
资源,不增加 rawConfig。
OpenSandbox 支持从 OCI image 创建 sandbox,但不保证每个 image 都能在所选 runtime、架构或
安全 profile 下成功启动。Adapter 对已知不支持的组合应尽早报告;image pull、进程启动或
运行时不兼容等实际后端失败最终统一表现为 Succeeded=False,并以 reason/message 保留可
诊断原因。这不要求 Ayatori 在提交前证明任意 OCI image 一定可运行。
待后续设计
- capability-based class 自动选择;
- 私有 image registry 的凭据和统一 workload identity;
- artifact、workspace 与 cache 的独立 API;
- Job 创建速率、并发、quota、公平调度以及是否集成 Kueue;
- observability correlation 的具体 OpenTelemetry/Loki 字段约定;
- 调用者错过 TTL 时是否需要可选的最小审计记录。
这些问题不阻塞首个 Kubernetes adapter 的 API review;image pull 的最小凭据路径必须在实现 前通过 Kubernetes 与 OpenSandbox adapter 测试验证。