# 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)