Files

22 KiB
Raw Permalink Blame History

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:

  1. 请求 adapter 终止执行;
  2. Observe,确认执行不会继续;
  3. 删除后端临时资源与短期凭据;
  4. 移除 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 测试验证。

成熟实现参考