atecontroller
atecontroller is the Kubernetes-native half of the control plane. It owns the
infrastructure side of Substrate (the prepared capacity that actors run on) and
nothing about the actors themselves, and it runs as the ate-controller
Deployment in ate-system.
The split follows the resource model, where WorkerPool, SandboxConfig, and
CSIDriverConfig are Kubernetes CRDs (ate.dev/v1) and atespaces, actor
templates, actors, and tags are records in ateapi’s
database. atecontroller reconciles the first group and reports what it sees to
ateapi, and it never touches an ActorTemplate (ateapi builds golden snapshots
itself).
What it runs
flowchart LR
subgraph CTRL["atecontroller process"]
direction TB
WPR["WorkerPool reconciler"]
NPR["NetworkPolicy reconciler"]
SCP["SandboxConfig protection"]
EMT["Egress MITM trust"]
SYNC["Worker syncer"]
end
K8s[(K8s API)] --> WPR
K8s --> NPR
K8s --> SCP
K8s --> EMT
K8s -- watch worker pods<br/>+ WorkerPools --> SYNC
WPR -- SSA apply --> DEP["Deployment<br/>(N worker pods)"]
WPR -- status --> WPS["WorkerPool.status<br/>resolved SandboxConfig"]
NPR -- SSA apply --> NP["NetworkPolicy<br/>ingress from atenet-router only"]
EMT -- writes --> CTB["ClusterTrustBundle"]
SYNC -- CreateWorker / DrainWorker /<br/>MarkWorkerAvailable / DeleteWorker --> API[ateapi]
click API "/components/ateapi/" "ateapi"
The diagram above shows the five loops that run in the one process, and the table below lists what each of them watches and what it produces.
| Loop | Watches | Produces |
|---|---|---|
| WorkerPool reconciler | WorkerPool, its Deployment, SandboxConfig | The worker Deployment, and WorkerPool.status (replicas, resolved SandboxConfig per sandbox class) |
| NetworkPolicy reconciler | WorkerPool | A NetworkPolicy per pool admitting ingress to worker pods only from atenet-router |
| SandboxConfig protection | SandboxConfig, WorkerPool | Removes the protection finalizer from a deleted SandboxConfig once no pool uses it |
| Egress MITM trust | The egress-mitm-ca-pool Secret | A ClusterTrustBundle actors use to trust the egress gateway’s intercepting CA |
| Worker syncer | Worker pods, WorkerPool | Worker records in ateapi, kept in step with the pods |
WorkerPool reconciler
apiVersion: ate.dev/v1kind: WorkerPoolmetadata: name: agent-pool namespace: ate-demo labels: workload: secret-agent # what template/actor selectors matchspec: replicas: 10 workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor sandboxClasses: - name: gvisor # exactly one class today configRef: name: gvisor-default # optional; else the class defaultGiven a pool like the one above, the WorkerPool reconciler goes through the same three steps on each reconcile.
- It resolves each sandbox class to a SandboxConfig (the one named by
configRef, or the newest config of that class annotated as the class default) and adds a protection finalizer to it so it can’t disappear while a pool depends on it. - It records the resolution in
status.sandboxClasses. A switch to a new config goes through apendingConfigReffirst and is confirmed against the API server, so status never names a config whose protection was already released. - It server-side-applies a Deployment with the pool’s name, owned by the pool,
running
spec.workerImage. A new pool gets its Deployment only once every class has a config, and an existing Deployment is applied first on every pass, so a SandboxConfig problem never blocks a scale.
Worker pods don’t take anything from the SandboxConfig directly, since atelet is what fetches the sandbox binaries it names (see SandboxConfig).
So how does an actor end up on a pool’s workers? A WorkerPool doesn’t reference
templates at all, and placement is selector-based instead. The scheduler in
ateapi ANDs a template’s worker_selector with the actor’s own and matches them
against the pool’s labels, which each Worker record mirrors.
The worker syncer
The syncer is how ateapi learns about capacity. It watches pods labeled
ate.dev/worker-pool and the WorkerPools, and it drives each pod’s Worker
record through ateapi’s Control RPCs as the state diagram below shows.
stateDiagram-v2 [*] --> UNAVAILABLE: pod scheduled<br/>CreateWorker UNAVAILABLE --> ACTIVE: pod Ready with an IP<br/>MarkWorkerAvailable ACTIVE --> UNAVAILABLE: pod not Ready<br/>MarkWorkerUnavailable ACTIVE --> DRAINING: pod terminating<br/>DrainWorker UNAVAILABLE --> DRAINING: pod terminating<br/>DrainWorker DRAINING --> [*]: pod gone<br/>DeleteWorker
- Registration happens once the pod is scheduled to a node. A Worker is named by the pod’s UID, so a replacement pod with the same name is a different Worker.
- Readiness gates scheduling, and only a Ready pod with an IP is
ACTIVE. The worker process itself still has to register its epoch, IPs, capacity, and runtimes through atelet before anything is placed on it. - Termination marks the Worker
DRAININGso nothing new lands there, without touching the actors on it (the pod is shutting them down itself). - Deletion calls
DeleteWorkeronce the pod is gone, and ateapi crashes any actor still running there and removes the record. - During cleanup, a terminal pod (Succeeded or Failed), or one whose pool no longer exists, is deleted. On startup the syncer also walks every Worker record in ateapi and removes the ones whose pods vanished while it was down.
Why is this a separate process from ateapi?
atecontroller is a standard Kubernetes operator (informer caches, reconcile loops, owner references, server-side apply, etc), while ateapi is a gRPC server optimized for request latency on database-backed records. Keeping the Kubernetes reconciliation in atecontroller keeps ateapi out of the Kubernetes API’s hot path. The manager runs without leader election, and the install runs a single replica.
Related
- WorkerPool · SandboxConfig · Worker
- Worker lifecycle follows the syncer’s states end to end.
- ateapi is atecontroller’s only gRPC peer, and where golden snapshots are built.
- Golden snapshot covers the ActorTemplate flow, which ateapi runs.