Files
ayatori/docs/api/job-v1alpha1.md

527 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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。
## 示例
```yaml
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
```yaml
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`
必填,创建后不可变。
```yaml
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` 等命令。
#### 环境变量
```yaml
- 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`。允许的状态迁移只有:
```text
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
```yaml
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。只有确认任务已经停止,才能
转为成功、失败或取消。暂时无法联系后端不等于后端执行失败。
### 正常转移
```text
Resolving
│ 引用与策略解析完成
▼
Scheduling
│ 后端逻辑执行已建立并持久化引用
▼
Starting
│ adapter 确认任务主体开始
▼
Running ───────────────→ Succeeded
└──────────────────→ Failed
```
后端在任务主体开始前就确定失败,例如 image pull、runtime 不兼容或 provisioning 失败,可以从
`Scheduling` 或 `Starting` 直接进入 `Failed`,此时 `startTime` 允许为空。
### 取消转移
```text
Resolving ─┐
Scheduling ─┤
Starting ─┼→ Cancelling → Cancelled
Running ─┤
ResultUnknown ─┘
```
尚未创建后端执行时,取消可以立即确认。已经存在或可能存在后端执行时,必须反复执行
Cancel/Observe,确认不会继续运行后才能进入 `Cancelled`。取消请求与成功完成并发时,以先从
后端确认到的不可逆事实为准:已经成功完成的任务保持 `Succeeded`,不能改写成 `Cancelled`。
### 不确定结果与恢复
```text
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 定义的不透明外部引用。
```yaml
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:
1. 请求 adapter 终止执行;
2. Observe,确认执行不会继续;
3. 删除后端临时资源与短期凭据;
4. 移除 finalizer。
首版不提供用户可选 orphan policy。让一次性任务脱离控制面继续运行既难以观察,也可能产生
副作用。后端长期不可达时由管理员根据 runbook 判断并强制移除 finalizer,该操作必须可审计。
TTL controller 只发起 Job 删除,所有手工删除和 TTL 删除都经过相同 finalizer 路径。
## Adapter contract 对 API 的保证
每个 execution adapter 必须提供以下语义,而非暴露自身 SDK 类型:
```text
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 测试验证。
## 成熟实现参考
- [Kubernetes Job](https://kubernetes.io/docs/concepts/workloads/controllers/job/)
- [Kubernetes Job API](https://kubernetes.io/docs/reference/kubernetes-api/batch/job-v1/)
- [Tekton Pipeline API](https://tekton.dev/docs/pipelines/pipeline-api/)
- [Kueue Workload](https://kueue.sigs.k8s.io/docs/concepts/workload/)
- [OpenSandbox API specifications](https://github.com/opensandbox-group/OpenSandbox/blob/main/docs/api/index.md)