atecontroller
atecontroller is the Kubernetes-native half of the control plane.
Everything declarative about Substrate goes through it: WorkerPool and
ActorTemplate are the CRDs it reconciles, and reconciling them produces real
K8s resources (Deployments) and ateapi state (golden snapshots).
What it owns
It runs exactly two reconcilers - one per CRD:
flowchart LR
subgraph CTRL["atecontroller process"]
direction TB
WPR["WorkerPool reconciler"]
ATR["ActorTemplate reconciler"]
end
K8s[(K8s API)] --> WPR
K8s --> ATR
WPR -- SSA apply --> DEP["K8s Deployment<br/>(N worker pods)"]
ATR -- CreateAtespace /<br/>CreateActor /<br/>ResumeActor /<br/>SuspendActor --> API[ateapi]
click API "/components/ateapi/" "ateapi"
There is no atespace or SandboxConfig reconciler here. SandboxConfig is a
cluster-scoped CRD, but ateapi consumes it directly (via a lister) when
scheduling - atecontroller does not reconcile it.
WorkerPool reconciler
apiVersion: ate.dev/v1alpha1kind: WorkerPoolmetadata: name: defaultspec: replicas: 10 ateomImage: ghcr.io/.../ateom-gvisor:vX sandboxClass: gvisorThe reconciler:
- Reads desired state from
WorkerPool.spec. - Server-side-applies a K8s
Deploymentnamed<workerpool-name>(same name as the pool; owning it, so pod changes re-trigger reconcile). - Updates
WorkerPool.status.replicaswith the Deployment’s actual replica count.
That’s it. No data plane involvement, no Redis writes. The pods themselves get registered in Redis by ateapi’s worker-pool syncer once they go Ready.
Crucially, a WorkerPool is decoupled from ActorTemplate: templates no longer
reference a pool. Instead, placement is selector-based - the scheduler ANDs a
template’s workerSelector with the actor’s own worker_selector and matches
against pool labels (and sandboxClass).
ActorTemplate reconciler
apiVersion: ate.dev/v1alpha1kind: ActorTemplatemetadata: name: my-agentspec: sandboxClass: gvisor workerSelector: { matchLabels: { pool: default } } containers: [...] pauseImage: ... snapshotsConfig: { location: gs://my-bucket/actors }This one is more interesting - it runs a golden snapshot bootstrap as a
small phase machine (ActorTemplate.Status.Phase), described in
Golden snapshot:
stateDiagram-v2 [*] --> PhaseInitial PhaseInitial --> PhaseResumeGoldenActor: CreateAtespace(ate-golden)<br/>+ CreateActor() PhaseResumeGoldenActor --> PhaseWaitGoldenActor: ResumeActor()<br/>(stamp TakeGoldenSnapshotAt) PhaseWaitGoldenActor --> PhaseReady: requeue fires, SuspendActor() PhaseReady --> [*]
Two things worth calling out:
- The golden actor lives in the reserved
ate-goldenatespace. InPhaseInitialthe reconciler first ensures that atespace exists (CreateAtespace, toleratingAlreadyExists), then creates a golden actor there whose name is a fresh UUID (Status.GoldenActorID). - The warmup wait is readyz-gated. The reconciler stamps
Status.TakeGoldenSnapshotAtand requeues (rather than blocking on a sleep) before it suspends the golden actor to capture the snapshot. That delay is 20s only as a fallback: if every container in the template declares areadyzprobe, the delay is 0, becauseResumeActoralready blocked until the workload reported readiness. A template with no containers keeps the 20s default.
The reconciler never assigns a PhaseFailed; on error it just returns and lets
controller-runtime requeue. The result: every ready ActorTemplate has a
Status.GoldenSnapshot URI (read from the suspended golden actor’s
latest_snapshot_info.external.snapshot_uri_prefix) that new actors of this
template can restore from in milliseconds.
Why is this a separate process from ateapi?
It uses the Kubernetes informer cache and has reconcile-loop semantics - the standard operator pattern. ateapi is stateless gRPC, optimized for QPS. Different concerns, different scaling profiles. (The manager is constructed without leader election, so if you scale this to multiple replicas they will all reconcile concurrently.)
Related
- WorkerPool · ActorTemplate
- Golden snapshot bootstrap - the ActorTemplate reconciler’s interesting flow.
- Atespace - including the reserved
ate-goldenatespace. - ateapi - atecontroller’s only gRPC client.