pub struct WorkloadSpec {Show 23 fields
pub schema_version: SchemaVersion,
pub name: String,
pub image: ImageRef,
pub tier: TierTag,
pub tenant: TenantId,
pub namespace: NamespaceId,
pub replicas: u32,
pub command: Option<Vec<String>>,
pub entrypoint: Option<Vec<String>>,
pub workdir: Option<PathBuf>,
pub user: Option<String>,
pub env: Vec<EnvVar>,
pub secrets: Vec<SecretMount>,
pub volumes: Vec<VolumeMount>,
pub resources: ResourceLimits,
pub depends_on: Vec<MeshIdent>,
pub healthcheck: Option<Healthcheck>,
pub restart_policy: RestartPolicy,
pub archetype: Option<LifecycleArchetype>,
pub stop_policy: StopPolicy,
pub expose: ExposeSpec,
pub labels: HashMap<String, String>,
pub annotations: HashMap<String, String>,
}Expand description
Complete typed description of a containerd workload handed to yubaba over
RPC. This is also the payload of the kind = "container" variant of
Workload on disk.
Yubaba never accepts compose YAML on its RPC surface — agents, the desktop,
and operator CLIs all hand yubaba WorkloadSpec values. See the arch doc
for the validation layers and evolution rules.
Fields§
§schema_version: SchemaVersionWire-format version; always V1 today. Present at the top level so
rolling clusters can detect and migrate across schema generations.
name: StringDNS-friendly workload name, e.g. "noisetable-api". Regex:
^[a-z0-9]([a-z0-9-]*[a-z0-9])?$, length ≤ 63.
image: ImageRefContainer image to pull.
tier: TierTagTier tag controlling admission control and mesh filtering.
tenant: TenantIdTenant isolation axis (W206). Separates operators’ workloads at the
network / DB / mesh-identity level. Defaults to TenantId::singleton
for specs that predate the axis, so single-tenant clusters keep every
isolation primitive a no-op. Orthogonal to Self::tier (class) and
Self::namespace (routing).
namespace: NamespaceIdNamespace routing/naming axis (W206). A pure naming key — never
affects isolation; disambiguates DNS names and selects config root /
provider zone within a tenant. Defaults to NamespaceId::singleton.
replicas: u32Target replica count. 0 registers the workload without deploying it.
Range: 0–100 (cluster-wide cap; operator can raise it).
command: Option<Vec<String>>Override the image’s CMD. None leaves the image default.
entrypoint: Option<Vec<String>>Override the image’s ENTRYPOINT. None leaves the image default.
workdir: Option<PathBuf>Working directory inside the container.
user: Option<String>User to run as, e.g. "1000:1000" or "appuser".
env: Vec<EnvVar>Environment variables. Values may be literals, secret refs, or mesh-address references resolved by yubaba at deploy time.
secrets: Vec<SecretMount>Secret mounts. Values never appear in the spec JSON — only references.
volumes: Vec<VolumeMount>Volume mounts.
resources: ResourceLimitsHard resource caps enforced by containerd/cgroups.
depends_on: Vec<MeshIdent>Mesh idents that must reach Ready before this workload starts.
healthcheck: Option<Healthcheck>Container liveness/readiness probe.
restart_policy: RestartPolicyWhat yubaba does when the container exits.
archetype: Option<LifecycleArchetype>Explicit lifecycle archetype (R572-F1 / W244): server, appliance,
or job. None means the spec predates this field (or the author
didn’t set it) — callers MUST NOT read this directly to decide
drainability; use WorkloadSpec::effective_archetype, which falls
back to the pre-R572 volumes/restart_policy inference so no
existing spec’s effective meaning changes.
Additive: this field did not exist before R572-F1. Reconciler (F4) and scheduler (F5) branching on the resolved archetype are separate, later tickets — this field alone changes no runtime behavior.
stop_policy: StopPolicyGraceful shutdown configuration.
expose: ExposeSpecNetwork exposure configuration — mesh, public, and operator channels are independent and can be set in any combination.
labels: HashMap<String, String>OCI-style labels, passed through to the container. Opaque to yubaba.
annotations: HashMap<String, String>Yah-specific metadata, conventionally prefixed yah.*. Opaque to
yubaba beyond yah.forge=true which suppresses the Never-restart guard.
Implementations§
Source§impl WorkloadSpec
impl WorkloadSpec
Sourcepub fn for_forge(
forge_id: &str,
image: ImageRef,
tier: TierTag,
ports: Vec<u16>,
) -> Self
pub fn for_forge( forge_id: &str, image: ImageRef, tier: TierTag, ports: Vec<u16>, ) -> Self
Build a WorkloadSpec for a forge run.
Sets the conventional forge fields in one place so callers cannot forget any of them:
restart_policy = Neverarchetype = Some(LifecycleArchetype::Job)— a forge run is exactly thecontainer-kind instance of the job archetype (W244); set explicitly rather than left to infer since this constructor knows its own shapeexpose.public = None,expose.operator = Noneexpose.mesh.identity = "forge.<forge_id>"annotations["yah.forge"] = "true"(suppresses the shape warning)tierandimagecome from the caller;portsbecomes the mesh port list (empty is valid — forge jobs often don’t expose ports)
All other fields are set to safe defaults. Callers can mutate the
returned value to fill in command, env, resources, etc.
Sourcepub fn wants_host_network(&self) -> bool
pub fn wants_host_network(&self) -> bool
Whether this workload requests the host network namespace rather than an isolated one.
Opt-in via annotations["yah.network"] == "host" (see
HOST_NETWORK_ANNOTATION / HOST_NETWORK_VALUE). Default is the
isolated netns every other workload gets — host networking is a
privileged escape hatch for the few infra workloads that must bind a
host port so an on-host ingress (e.g. a Cloudflare tunnel reaching
127.0.0.1:<port>) can route to them without CNI/bridge plumbing.
The backend (kamaji) is responsible for guarding this: host
networking is only honoured for tier == "infra" workloads; a
non-infra workload that sets the annotation is rejected at deploy. See
validate_spec_for_constable.
Sourcepub fn effective_archetype(&self) -> LifecycleArchetype
pub fn effective_archetype(&self) -> LifecycleArchetype
Resolve the lifecycle archetype (R572-F1 / W244): the explicit
Self::archetype if set, otherwise the pre-R572 inference from
volumes/restart_policy this field replaces.
This is the one seam callers should use to ask “can I kill and
reschedule this?” — it is intentionally the only place that
implements the fallback, so behavior for pre-existing specs (no
archetype on disk) is identical to what it was before this field
existed. Consumers (reconciler R572-F4, scheduler R572-F5) branch on
the return value; this crate does not itself change any reconciler or
scheduler behavior.
Sourcepub fn fq_mesh_identity(&self) -> String
pub fn fq_mesh_identity(&self) -> String
Fully-qualified mesh identity <tenant>/<namespace>/<name> (W206 /
R558-F3), where <name> is this workload’s MeshExpose::identity.
Within a tenant, workloads still address each other by the short
identity (namespace disambiguates only on collision); the FQN is what
makes the identity unambiguous across tenants and is exactly what a
MeshPeer::CrossTenant grant names.
Sourcepub fn requires_taint(&self) -> Option<&str>
pub fn requires_taint(&self) -> Option<&str>
The taint this workload requires its node to carry, if any (R594-F2 / W267 sovereign public ingress).
Opt-in via annotations["yah.placement.requires-taint"] = "<taint name>" (see REQUIRES_TAINT_ANNOTATION) — same annotation-based,
zero-blast-radius shape as Self::wants_host_network, chosen so
declaring this requirement does not force a struct-literal edit at
every existing WorkloadSpec { .. } construction site the way a new
plain field would (see R572-F1’s handoff: ~26 sites for one field).
Both halves have since landed: MachineConfig.taints (R572-F3) and the
scheduler’s affinity check in cloud::config::RequiredSpec::matches
(R572-F5), which requires the key in the node’s taints or
mesh_tags.
A key named here is one of only two ways a node taint can influence
placement — the other is the no-<archetype> repulsion form. W305/
R742-T4 makes yah cloud validate reject any node taint that is
neither, so a new affinity key must be added to
cloud::config::AFFINITY_TAINT_KEYS alongside the workload that
requires it.
The public-ingress appliance (W267) is the first user: a
kind = "container" workload with archetype = Some(LifecycleArchetype::Appliance) and
requires_taint() == Some(PUBLIC_IP_TAINT), so yubaba may one day
place it only on machines carrying the "public-ip" taint and kamaji
supervises it like any other container (no new Workload variant —
see Workload::Container’s doc comment).
Sourcepub fn memory_request_mb(&self) -> u32
pub fn memory_request_mb(&self) -> u32
The memory (MiB) a scheduler must find on a node before placing this
workload — its request, as distinct from ResourceLimits::memory_mb,
which is a ceiling the backend turns into a cgroup memory.max.
Opt-in via annotations["yah.placement.memory-request-mb"] (see
MEMORY_REQUEST_ANNOTATION); absent or unparseable falls back to
resources.memory_mb, so every spec that does not set it is admitted
exactly as it was before this accessor existed.
§Why the two numbers must not be the same one
A limit answers “kill it past here”; a request answers “don’t start it somewhere smaller than here”. Generous is the safe direction for the first and the unschedulable direction for the second, so one field serving both makes a deliberately-roomy ceiling into an admission floor.
That is not hypothetical: WorkloadSpec::for_forge sets a 32 GiB
ceiling explicitly reasoned as “above physical RAM on smaller
build-workers ⇒ effectively unlimited there” (R590-B10), and
CloudConfig::admit_workload fed that same 32768 in as the R572-F5
capacity floor. Every build-worker under 32 GiB — the three 8 GiB Pi-5s
and the 16 GiB us-west-003 — became structurally unadmittable for any
offloaded qed step, leaving one 47 GiB node as the fleet’s only legal
target for remote CI. This is R590-B10’s own recorded follow-up
(“thread a per-step memory request … instead of a blanket forge
default”), reduced to the seam that closes the bug.
An annotation rather than a new ResourceLimits field on purpose:
WorkloadSpec crosses a postcard wire that is positional and
carries no field names (R590-B3), so adding a field would break decode
on every fleet node still running an older kamaji. annotations is an
existing map — an extra key rides it safely, and admission already
reads placement inputs from exactly there
(Self::requires_taint, the R594 node-selector).
Sourcepub fn wants_native_exec(&self) -> bool
pub fn wants_native_exec(&self) -> bool
Whether this workload must be run by kamaji’s native (fork+exec) backend on the node’s own userland, rather than by a container backend (R577-T1 / W254).
Opt-in via annotations["yah.exec"] == "native" (see
NATIVE_EXEC_ANNOTATION / NATIVE_EXEC_VALUE) — the same
annotation-shaped, zero-blast-radius marker as
Self::wants_host_network and Self::requires_taint, chosen over
a new plain field for the reason R572-F1 recorded: a field forces a
struct-literal edit at every existing construction site and an
exhaustive-match update in kamaji-proto’s codec, and this marker
needs neither.
§Why an annotation and not a runtime enum on the wire
The remote-execution wire already carries exactly one workload shape —
Workload::Container(WorkloadSpec) — and every layer between the
dispatcher and the node (yubaba admission, mesh assignment, log
ingest, produced-file retrieval, teardown) is written against it. A
Darwin build differs from a Linux build in one respect: there is no
container that can host it, because you cannot containerize the Darwin
kernel. Marking that one difference keeps the rest of the path shared
instead of growing a parallel exec_native RPC that would have to
re-implement all of it.
image stays populated for a native workload and is identity
metadata only — nothing is pulled; the native backend resolves argv
from entrypoint + command (container semantics) and execs it on
the host.
Sourcepub fn wants_nested_sandbox(&self) -> bool
pub fn wants_nested_sandbox(&self) -> bool
Whether this workload builds its own unprivileged container sandbox
inside the one the backend gives it, and therefore needs the two
capabilities plus the no_new_privs relaxation that setting up a
user namespace requires (R636-B2).
Opt-in via annotations["yah.sandbox"] == "nested" (see
NESTED_SANDBOX_ANNOTATION / NESTED_SANDBOX_VALUE) — the same
annotation-shaped, zero-blast-radius marker as
Self::wants_host_network and Self::wants_native_exec.
§What it actually grants, and why exactly that
Rootless BuildKit (the only user today: remote build-image steps
dispatch moby/buildkit:*-rootless) boots through rootlesskit, which
must map a range of sub-uids into a fresh user namespace. It does that
by exec’ing the setuid-root helpers newuidmap / newgidmap, so
it needs CAP_SETUID + CAP_SETGID in the bounding set and
noNewPrivileges = false (with no_new_privs on, the kernel silently
strips the setuid bit and the helper fails with “Could not set caps”).
Each of those three was measured on us-west-002 to be individually
necessary — dropping any one of them puts rootlesskit back to
failing before the first layer:
| grant | rootlesskit result |
|---|---|
baseline (CAP_NET_BIND_SERVICE only, nnp on) | fork/exec /usr/bin/newuidmap: operation not permitted |
+CAP_SETUID only, nnp off | fork/exec /usr/bin/newgidmap: operation not permitted |
+CAP_SETUID +CAP_SETGID, nnp on | newuidmap: Could not set caps |
+CAP_SETUID +CAP_SETGID, nnp off | starts; build runs to completion |
It is deliberately not CAP_SYS_ADMIN: a non-rootless buildkitd
would need that instead, which is a far wider grant. Emptying
/etc/subuid to force rootlesskit’s single-mapping path does not
avoid the helpers either — it just fails earlier with “No subuid
ranges found”.
The backend guards this. Like host networking, it is honoured only
for tier == "infra" workloads; a non-infra workload that sets the
annotation is rejected at deploy. Every other workload keeps the
CAP_NET_BIND_SERVICE-only, no_new_privs baseline.
§Mutually exclusive with Self::wants_native_exec
This grant is defined in terms of an OCI process spec — a
capability set and a noNewPrivileges bit. A native (fork+exec)
workload has no OCI spec, so there is nothing to apply it to; kamaji
refuses a spec carrying both markers rather than accepting a request
for widened privileges and silently dropping it (R577-T1 owns that
refusal). The two are independent annotations — neither implies the
other, which is what
nested_sandbox_marker_is_independent_of_the_other_markers pins — but
they are not a legal pair.
If a future runtime does have a sandbox worth widening (a MacVM under
W254, say), give it its own annotation rather than relaxing that
refusal. The grant this marker names is CAP_SETUID + CAP_SETGID +
no_new_privs off and nothing else; letting it mean a different
privilege set per backend would make “what does yah.sandbox=nested
grant?” unanswerable without knowing which backend received it, which
is precisely what a security-relevant marker must not be.
Trait Implementations§
Source§impl Clone for WorkloadSpec
impl Clone for WorkloadSpec
Source§fn clone(&self) -> WorkloadSpec
fn clone(&self) -> WorkloadSpec
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for WorkloadSpec
impl Debug for WorkloadSpec
Source§impl<'de> Deserialize<'de> for WorkloadSpec
impl<'de> Deserialize<'de> for WorkloadSpec
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Source§impl PartialEq for WorkloadSpec
impl PartialEq for WorkloadSpec
Source§impl Serialize for WorkloadSpec
impl Serialize for WorkloadSpec
impl StructuralPartialEq for WorkloadSpec
Source§impl TS for WorkloadSpec
impl TS for WorkloadSpec
Source§type WithoutGenerics = WorkloadSpec
type WithoutGenerics = WorkloadSpec
WithoutGenerics should just be Self.
If the type does have generic parameters, then all generic parameters must be replaced with
a dummy type, e.g ts_rs::Dummy or (). The only requirement for these dummy types is that
EXPORT_TO must be None. Read moreSource§type OptionInnerType = WorkloadSpec
type OptionInnerType = WorkloadSpec
std::option::Option<T>, then this associated type is set to T.
All other implementations of TS should set this type to Self instead.Source§fn docs() -> Option<String>
fn docs() -> Option<String>
TS is derived, docs are
automatically read from your doc comments or #[doc = ".."] attributesSource§fn decl_concrete(cfg: &Config) -> String
fn decl_concrete(cfg: &Config) -> String
TS::decl().
If this type is not generic, then this function is equivalent to TS::decl().Source§fn decl(cfg: &Config) -> String
fn decl(cfg: &Config) -> String
type User = { user_id: number, ... }.
This function will panic if the type has no declaration. Read moreSource§fn inline(cfg: &Config) -> String
fn inline(cfg: &Config) -> String
{ user_id: number }.
This function will panic if the type cannot be inlined.Source§fn inline_flattened(cfg: &Config) -> String
fn inline_flattened(cfg: &Config) -> String
Source§fn visit_generics(v: &mut impl TypeVisitor)where
Self: 'static,
fn visit_generics(v: &mut impl TypeVisitor)where
Self: 'static,
Source§fn output_path() -> Option<PathBuf>
fn output_path() -> Option<PathBuf>
T should be exported, relative to the output directory.
The returned path does not include any base directory. Read moreSource§fn visit_dependencies(v: &mut impl TypeVisitor)where
Self: 'static,
fn visit_dependencies(v: &mut impl TypeVisitor)where
Self: 'static,
Source§fn dependencies(cfg: &Config) -> Vec<Dependency>where
Self: 'static,
fn dependencies(cfg: &Config) -> Vec<Dependency>where
Self: 'static,
Source§fn export(cfg: &Config) -> Result<(), ExportError>where
Self: 'static,
fn export(cfg: &Config) -> Result<(), ExportError>where
Self: 'static,
TS::export_all. Read moreSource§fn export_all(cfg: &Config) -> Result<(), ExportError>where
Self: 'static,
fn export_all(cfg: &Config) -> Result<(), ExportError>where
Self: 'static,
TS::export. Read more