Commit Graph
42 Commits
Author SHA1 Message Date
Zoe Zhao 4b3423c01a Remove the secretKeyRef env sources feature (#835)
This reverts the ActorTemplate valueFrom.secretKeyRef support added in
#20 (issue #15).

We don't want actors to have any access to secrets, they will be
injected on the egress route instead. Removing this now so we don't need
to copy secrets into substrate resources.
2026-08-11 16:13:30 -07:00
AngelaandJeff Luo d4f2cf1c51 feat: Add control plane telemetry - workerpool desired and ready workers gauges (#826)
Part of #564 (Part 4, Fixes #564)

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR

## Description
This PR implements Part 4 of #564 by adding OpenTelemetry gauge
instrumentation for `ate.workerpool.desired_workers` and
`ate.workerpool.ready_workers` in `atecontroller`.

## Key Changes:
* Added `ReadyReplicas int32` to `WorkerPoolStatus` with
`+kubebuilder:printcolumn:name="Ready"` annotation in
`pkg/api/v1alpha1/workerpool_types.go` and regenerated the CRD manifest.
* Updated `WorkerPoolReconciler.syncStatus` to synchronize
`dep.Status.ReadyReplicas` into `wp.Status.ReadyReplicas`.
* Implemented `InitMetrics(meter)` registering
`ate.workerpool.desired_workers` and `ate.workerpool.ready_workers` as
OpenTelemetry Observable UpDownCounters (`{worker}`) with an
asynchronous observer callback that samples controller-runtime's local
informer cache (`r.Client.List`).
* Labeled metric datapoints using centralized attributes
`ateattr.WorkerPoolNamespaceKey` and `ateattr.WorkerPoolNameKey`.

## Testing
* `go test -buildvcs=false ./cmd/ateapi/internal/controlapi/...`
* `go test -buildvcs=false ./cmd/atenet/internal/router/...`
* `make test`
## E2E  Test
* `./hack/create-kind-cluster.sh`
* `./hack/install-ate-kind.sh --deploy-ate-system --deploy-demo-counter`
* `./hack/run-e2e.sh ./internal/e2e/suites/metrics/...`

---------

Co-authored-by: Jeff Luo <jeffluoo@google.com>
2026-08-11 16:02:50 -04:00
Benjamin Elder da8414bbd2 api: move the pause image from ActorTemplate to SandboxConfig (#848)
The pause image holds the sandbox's namespaces and runs no workload
code. It is an implementation detail of the sandbox, not something actor
authors pick, so it belongs with the sandbox binaries that already moved
off the ActorTemplate onto the cluster-scoped SandboxConfig.

It now travels with those binaries end to end: resolved from the pool's
SandboxConfig, carried on ateletpb.SandboxAssets rather than
WorkloadSpec, and recorded in the per-actor sandbox record so Checkpoint
pins it into the snapshot manifest and Restore rebuilds the sandbox from
the image the snapshot was taken with (the golden's on a DATA_ON_GOLDEN
restore). A record without one is rejected outright rather than pulling
an empty image.


- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-11 10:14:03 -04:00
Julian Gutierrez Oschmann ddeee6c951 Some improvements to the way we allocate and track snapshot ids (#784)
Fixes #773 

Some improvements to the way we create, track and propagate snapshot
ids.

* Consolidate snapshot id and name into a single concept.
* Use consistent terminology across the whole stack (i.e. remove
confusion about prefix vs id vs name).
* Get rid of internal only storage to hide the snapshot URI.
2026-08-08 12:25:45 -07:00
eliranw 934c7418d0 feat: CDI-based NVIDIA GPU passthrough into gVisor actor containers (#502)
## Summary

- `atecontroller` propagates a pool's `nvidia.com/gpu` request onto the
`ateom` container and mounts the host NVIDIA toolkit read-only (path
overridable via `ATE_NVIDIA_TOOLKIT_HOST_PATH`)
- `ateom-gvisor` generates a CDI spec with `nvidia-ctk` and injects the
device nodes, driver-library mounts, and env into each actor container's
OCI spec
- runs the CDI `createContainer` hooks except `update-ldcache` which
needs a privileged ateom, staging the SONAME symlinks it would create
from each library's ELF `DT_SONAME`
- enables `runsc --nvproxy` at sandbox creation

Requesting `nvidia.com/gpu` on the pool is the only configuration
needed; a pool that requests N GPUs makes all N usable.

Two details of the CDI spec are worth calling out, because getting
either wrong fails at runtime rather than at parse time. `nvidia-ctk`
leaves `major`/`minor` unset — CDI delegates that to the OCI runtime —
so each device node is resolved by stat-ing the host; without it the
actor gets `0,0` char devices and NVML reports it cannot communicate
with the driver. And it emits per-index, per-UUID, and `all` devices
that repeat the same nodes, so only `all` is applied. The spec is plain
JSON, so `encoding/json` suffices and no CDI library is vendored.

`update-ldcache` is the one hook that cannot run here: its `ldconfig`
unshares a mount namespace and mounts a private `/proc`, which
`mount_too_revealing()` rejects under the pod's masked `/proc`.
Permitting it would need `procMount: Unmasked`, which Kubernetes only
allows with `hostUsers: false`, and that user namespace breaks the
per-actor cgroup delegation from #496. Skipping it avoids the whole
chain, so a GPU worker keeps the same posture as any other unprivileged
gVisor worker. `create-symlinks` and `enable-cuda-compat` still run
unmodified.

`--nvproxy` must be set when the sandbox is created — the `pause`
container, which holds no GPU devices — so runsc's auto-detection never
fires on its own; without the flag the GPU subcontainer crashes the
sentry on start. GPU detection matches any device index rather than
assuming `/dev/nvidia0`, since a worker sharing a multi-GPU node can be
assigned `/dev/nvidia2` and `/dev/nvidia3`.

GPU pools must set `spec.ateomImage` to a glibc build
(`KO_DEFAULTBASEIMAGE=debian:stable-slim ko build ./cmd/ateom-gvisor`)
because the distroless default cannot exec `nvidia-ctk`; the default
base is unchanged for every other pool. `atelet` also has to run on the
GPU nodes to restore actors there, so its DaemonSet needs a toleration
for whatever taint they carry. Both are documented in the API guide
rather than defaulted.

## Testing

- `make test`
- `env -u NO_COLOR make verify`
- Real GPU, Tesla T4 / driver 580.65.06, through the full actor flow:
`nvidia-smi`, `vectorAdd`, `nbody` at 3.77 TFLOP/s, PyTorch matmul via
cuBLAS at 3.5 TFLOP/s (T4 peak FP32 is ~8.1, so no measurable sandbox
penalty)
- Actor whose entrypoint runs the CUDA sample directly exits 0,
confirming the injected env reaches the workload without help from the
test harness
- Two workers holding two GPUs each on one 4-GPU node see disjoint
device sets

Snapshot and restore work when the workload holds no CUDA context. A
live CUDA context cannot be checkpointed - gVisor fails with `can't save
with live nvproxy clients` and the failed checkpoint terminates the
sandbox so a GPU actor can only be suspended between CUDA workloads.
Documented as a known limitation in the API
guide; a follow-up issue will track lifting it via `cuda-checkpoint`.

Fixes #627 

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR

---------

Signed-off-by: Eliran Wolff <eliranw@nvidia.com>
2026-08-07 15:08:44 -07:00
Haven Xia 3d33153239 Make envtest offline-capable and factor out TestMain boilerplate (#792)
`setup-envtest use` with no version re-resolves "latest" against the
remote release index on every run, so the envtest-backed packages needed
network even with fully cached binaries.

This PR pins the control plane to `1.36.x`, and fold the three
per-package `envtest` into `internal/testenv.Start`, which also honors
`-short`: `go test -short ./...` skips the envtest-backed packages and
is the guaranteed-offline path.

Have Claude to help me fix it

Fixes #789

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-08-07 12:07:31 -07:00
Elijah Rodriguez-Beltran 8152de17df Integrate with substrate
CONV=a6947013-0eab-49f2-9960-11d98ee43dbf
2026-08-07 18:20:22 +00:00
nybidari 5429ee8df7 Update gVisor release which supports multiple durable-dirs (#787)
- Update the gVisor release which has support for multiple durable-dirs.
- Modify the tests to run with gVisor (which were disabled before).
2026-08-06 19:08:02 -07:00
Etienne Perot 2fe26c162b Update gVisor build to fresher version using new release tarball asset. (#684)
This new release requires multiple files and is released as a tarball.
gVisor asset handling code is updated to automatically handle this
release format and extract it as necessary.

This build also contains the start of a series of upcoming
startup/memory performance optimizations to make Substrate sandbox churn
efficient.

Benchmarks between the previous gVisor build and this one:

```
CI benchmark suite:
                                                     │      old       │                    new                    │
                                                          │     sec/op     │     sec/op       vs base                  │
  ResumeActor/glutton_baseline_1_user/p50                   324.7m ±    3%    289.6m ±    2%  -10.79% (p=0.000 n=11)
  ResumeActor/glutton_baseline_1_user/p95                   373.4m ±    9%    328.6m ±    2%  -12.00% (p=0.000 n=11)
  ResumeActor/glutton_baseline_5_users/p50                  319.4m ±    1%    288.6m ±    1%   -9.65% (p=0.000 n=9)
  ResumeActor/glutton_baseline_5_users/p95                  367.6m ±    5%    341.7m ±   11%   -7.05% (p=0.024 n=9)
  ResumeActor/glutton_baseline_10_users/p50                 323.6m ±    1%    293.8m ±    2%   -9.22% (p=0.000 n=9+10)
  ResumeActor/glutton_baseline_10_users/p95                 396.2m ±    4%    372.1m ±    5%   -6.07% (p=0.001 n=9+10)
  ResumeActor/glutton_oversubscribe_15_users/p50            330.8m ±    2%    304.7m ±    1%   -7.89% (p=0.000 n=8+10)
  ResumeActor/glutton_oversubscribe_15_users/p95            402.7m ±    5%    390.4m ±    3%        ~ (p=0.083 n=8+10)
  ResumeActorColdStart/glutton_baseline_1_user/p50          227.0m ±   10%    211.6m ±   19%   -6.79% (p=0.003 n=11)
  ResumeActorColdStart/glutton_baseline_5_users/p50         231.5m ±    5%    216.3m ±    7%   -6.54% (p=0.004 n=9)
  ResumeActorColdStart/glutton_baseline_5_users/p95         246.4m ±    3%    230.2m ±    6%   -6.60% (p=0.000 n=9)
  ResumeActorColdStart/glutton_baseline_10_users/p50        217.4m ±   10%    193.5m ±    7%  -10.99% (p=0.001 n=9+10)
  ResumeActorColdStart/glutton_baseline_10_users/p95        241.8m ±    7%    231.2m ±    9%   -4.39% (p=0.022 n=9+10)
  ResumeActorColdStart/glutton_oversubscribe_15_users/p50   205.9m ±    6%    192.5m ±   28%        ~ (p=0.122 n=8+10)
  ResumeActorColdStart/glutton_oversubscribe_15_users/p95   247.1m ±    4%    229.6m ±  102%   -7.09% (p=0.043 n=8+10)
  SuspendActor/glutton_baseline_1_user/p50                  313.3m ±    7%    294.1m ±    4%   -6.14% (p=0.010 n=11)
  SuspendActor/glutton_baseline_5_users/p50                 304.6m ±    3%    289.1m ±    2%   -5.10% (p=0.000 n=9)
  SuspendActor/glutton_baseline_10_users/p50                309.7m ±    3%    296.9m ±    3%   -4.13% (p=0.000 n=9+10)
  SuspendActor/glutton_oversubscribe_15_users/p50           320.6m ±    3%    305.3m ±    2%   -4.75% (p=0.001 n=8+10)

Manual benchmarks (GKE on a single c3-standard-88 node):

- p50 sandbox lifecycle time (from issuing resume to checkpointed):
  ┌─────────┬──────────┬───────────┬────────┐
  │ workers │   old    │    new    │ delta  │
  ├─────────┼──────────┼───────────┼────────┤
  │ 1       │ 795 ms   │ 565 ms    │ −28.9% │
  ├─────────┼──────────┼───────────┼────────┤
  │ 2       │ 801 ms   │ 590 ms    │ −26.4% │
  ├─────────┼──────────┼───────────┼────────┤
  │ 8       │ 902 ms   │ 759 ms    │ −15.9% │
  ├─────────┼──────────┼───────────┼────────┤
  │ 64      │ 3.25 s   │ 3.14 s    │ −3.4%  │
  ├─────────┼──────────┼───────────┼────────┤
  │ 88      │ 4.47 s   │ 4.30 s    │ −3.8%  │
  └─────────┴──────────┴───────────┴────────┘

- Sandbox starts per second:
  ┌─────────┬────────────┬────────────┬────────┐
  │ workers │    old     │    new     │ delta  │
  ├─────────┼────────────┼────────────┼────────┤
  │ 1       │ 1.256 ± 1% │ 1.755 ± 2% │ +39.7% │
  ├─────────┼────────────┼────────────┼────────┤
  │ 2       │ 2.482 ± 1% │ 3.393 ± 1% │ +36.7% │
  ├─────────┼────────────┼────────────┼────────┤
  │ 8       │ 8.81 ± 2%  │ 10.50 ± 2% │ +19.2% │
  ├─────────┼────────────┼────────────┼────────┤
  │ 64      │ 19.85 ± 3% │ 20.45 ± 1% │ +3.0%  │
  ├─────────┼────────────┼────────────┼────────┤
  │ 88      │ 19.70 ± 1% │ 20.58 ± 2% │ +4.5%  │
  └─────────┴────────────┴────────────┴────────┘
```
2026-08-05 16:10:05 -07:00
sfunkenhauser 3ea6971f17 Remove terminationGracePeriodSeconds and hardcode grace period to 60m (#756)
There's still some debate on whether this is the correct API for
configuring termination grace period. It's possible we only want to
allow users to configure the grace period per actor, instead of per
worker. We want to be careful about exposing the wrong API surface too
early, as it will need to be supported for a long time (potentially
forever).

For now let's hardcode a large value, and we can revisit this decision
later.
2026-08-05 11:40:36 -04:00
Maya WangandMaya Wang 9e3ee7a3d8 feat: readyz: make the overall wait timeout configurable per template (#487)
> **Rescoped again.** This PR previously proposed
`--golden-snapshot-warmup`, a
> tunable wall-clock delay before the golden checkpoint. Per discussion,
that
> direction is dropped: the answer for a workload that cannot report
readiness
> is a readiness endpoint — a small sidecar where the workload itself
cannot be
> changed — not a longer timer. What survives is the piece that
discussion
> agreed on, and which the previous revision already flagged as a
follow-up:
> making the readyz deadline itself configurable.
>
> The warmup work is not in this branch. It is kept locally in case a
workload
> genuinely cannot be given a readiness signal before GA, and would come
back as
> its own PR if so.

## Summary

`readyz.Wait` polls until the container returns 200 or a **hardcoded
30s**
elapses. A workload that legitimately takes longer to bind its HTTP
server
cannot be accommodated without raising the ceiling for every actor in
the
cluster, and losing that race fails the actor start.

How long a workload takes to become ready is a property of that
workload, so
this makes the deadline a per-template setting rather than a package
constant.

Adds optional `timeoutSeconds` to `ContainerReadyz`. **Unset keeps
today's 30s**,
so no existing template changes behavior.

## Changes

The value rides on the existing probe, so it follows the chain the probe
already
takes and no call site needs to know about it:

`ContainerReadyz.timeoutSeconds` → `toAteletReadyz` → `ateletpb.Readyz`
→
`toAteomReadyz` → `ateompb.Readyz` → `readyz.Wait`

- `pkg/api/v1alpha1/actortemplate_types.go` — `TimeoutSeconds *int32`,
  `+optional`, `Minimum=1`, `Maximum=3600`.
- `internal/proto/ateletpb/atelet.proto`,
`internal/proto/ateompb/ateom.proto` —
  `int32 timeout_seconds = 2` on both `Readyz` messages.
- `cmd/ateapi/internal/controlapi/workload_spec.go`,
`cmd/atelet/main.go` — pass
  it through the two conversions.
- `internal/readyz/readyz.go` — `OverallTimeout` becomes
`DefaultOverallTimeout` (still 30s) and `Wait` resolves its deadline
through a
  new `overallTimeout(probe)` helper.
- Regenerated: both `.pb.go`, `zz_generated.deepcopy.go`, and the
  `actortemplates` CRD.

None of the four `readyz.WaitAll` call sites change.

**On the zero value.** Unlike a warmup delay — where zero is a real
request
meaning "checkpoint immediately" — a zero readiness deadline could never
be met,
so it is never something a template author means. A non-positive value
on the
wire is therefore read as "unset" and falls back to the default, and the
CRD
field is a pointer with `Minimum=1` so the API rejects `0` outright
rather than
silently substituting 30s behind the author's back.

**On bounding**, which was the open question left on the previous
revision:
bounded at `3600`. A template asking to wait longer than an hour for
readiness
is expressing a broken workload, not a slow one, and the bound keeps a
typo from
pinning a worker for a day.

## Verification

- `go build ./...`, `go vet ./...`, `gofmt`, `go test ./...` — all pass.
- `internal/readyz/readyz_test.go` — `overallTimeout` resolves unset and
negative to the default and honors an explicit value; `Wait` against a
port
nothing binds gives up at the probe's 1s deadline rather than the 30s
default.
- `workload_spec_test.go`, `cmd/atelet/main_test.go` — the timeout
crosses both
  conversions, and a probe without one stays zero on the wire.
- `actortemplate_validation_test.go` — the bounds are enforced by a real
API
server. This suite runs under envtest against the generated CRD
directory, so
it exercises the regenerated `actortemplates` CRD rather than the Go
markers:
  `300` is accepted, unset is accepted, and `0`, `-1` and `3601` are all
  rejected by apiserver schema validation.

- **On a real cluster, via CI.** `internal/e2e/fixtures/probe` now
declares a
`readyz` probe with `timeoutSeconds: 60`, pointed at the `/healthz` the
probe
binary already serves on `:80`. The kind e2e that runs on every PR
therefore
exercises the value crossing ateapi → atelet → ateom on real binaries,
across
the auth matrix, on both the run and restore paths. This is also the
readyz
  path's first e2e coverage — no fixture declared a probe before.

Wire compatibility degrades safely in both skew directions:
`timeout_seconds` is
a new field 2 on a `Readyz` message that has only ever had field 1, so
an old
ateom ignores it and an old ateapi leaves it zero, which reads as the
30s
default.

No GKE run. What that would add over the above is a workload whose
readiness
genuinely exceeds 30s, and that is the readiness-sidecar work rather
than this
PR.

Fixes #<issue_number_goes_here>

> It's a good idea to open an issue first for discussion.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR

---------

Co-authored-by: Maya Wang <mymaya@google.com>
2026-08-04 07:07:41 -07:00
Dmitry Berkovich 3ed6aa07e1 microvm: API support for resume Data snapshots on the golden snapshot (#451, phase 1) (#683)
Phase 1 of #451, micro-VM only. gVisor support will follow in a separate
PR.

## What

Adds a `snapshotsConfig.onResume` block to the ActorTemplate, selecting
per snapshot situation what supplies the guest state at resume: each
field names what is being resumed *from*, the value names the boot
source. `onPause`/`onCommit` remain pure capture scopes (`Full | Data`),
and the `ActorSnapshot` records plain `Full`/`Data` content; the
golden-combine is strictly a restore-time behavior.

```yaml
snapshotsConfig:
  onPause: Data
  onCommit: Data
  onResume:
    fromData: Golden   # ColdBoot (default) | Golden — Data snapshots resume as
  location: gs://…     # golden memory + fs delta with the actor's data layered on
```

Under `fromData: Golden`, a data-only restore combines the template's
**golden snapshot** (guest memory + full fs delta) with the actor's
captured durable data — the actor comes back with the golden's warm
state over its own data instead of cold-booting. `fromData` applies to
every resume of a Data snapshot — commit or pause checkpoint. Future
situations (e.g. resuming a Full snapshot across a template upgrade,
#477) get sibling fields under `onResume` rather than overloading this
one.

## How

- **CRD** (`pkg/api/v1alpha1`): new `OnResumeConfig`/`ResumeSource`
types; `fromData: Golden` is CEL-gated to `sandboxClass: microvm`.
- **Control plane** (`ateapi`): resume derives the behavior from the
template's `onResume` configuration — for a Data durable snapshot or a
Data pause checkpoint it resolves the golden snapshot's location and
sends the restore-only `DATA_ON_GOLDEN` wire scope, failing early (with
an actionable error) if the golden snapshot is missing or not Full.
Golden actors always commit `Full` regardless of `onCommit` — their
snapshot is the base the combine needs.
- **Wire APIs** (`ateletpb`/`ateompb`): restore-only
`SNAPSHOT_SCOPE_DATA_ON_GOLDEN`, rejected on checkpoints;
`golden_snapshot_uri_prefix` is a top-level `RestoreRequest` field
because the actor's snapshot may be a local pause checkpoint while the
golden snapshot is always external.
- **atelet**: stages a single combined folder — the actor's files (the
durable-dir tar) win name collisions, the golden snapshot supplies the
rest. External restores download both halves concurrently; local (pause)
restores copy the actor's files from the local checkpoint dir while the
golden's files download in parallel. The golden manifest's pinned
sandbox binaries run the restored guest and are recorded on-node for
later checkpoints.
- **ateom-microvm**: `DATA_ON_GOLDEN` restores through the existing Full
path — cloud-hypervisor relaunches from the golden's guest files while
the durable virtio-fs share serves the actor's re-materialized data.
gVisor rejects the scope (defense-in-depth behind the CRD gate).

## Testing

- Unit: envtest CEL cases, converter tables, atelet request validation
(checkpoint rejection, golden-URI rules incl. local+golden),
combined-download test against a fake object store, resume-workflow
golden-resolution cases for both the durable and pause paths.
- e2e (`suites/demo`): OnGolden lifecycle cases for the commit path, the
pause path, and a two-durable-volume variant (micro-VM lane only),
asserting counters across pause/suspend and the recorded Data content
scope.
- Manually verified on a live GKE cluster: Data commit and Data pause
both resume with the golden guest's memory over the actor's data, on
different pods.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-01 20:13:37 -07:00
Benjamin Elder 97772a02f3 Support multiple durable-dir volumes on the micro-VM runtime
An ActorTemplate could declare only one durable-dir volume, and mount it
into a container only once. That is a gVisor limit, not a general one:
atelet declares the mount to gVisor through a single hardcoded annotation
key ("dev.gvisor.spec.mount.durabledir"), so a second volume would silently
overwrite the first.

The micro-VM runtime has no such constraint — every volume is a
subdirectory of the one writable virtio-fs share, and the snapshot tar
already archives that directory whole, so N volumes cost a subdirectory
each and round-trip through checkpoint/restore untouched. Gate the two
"at most one" CEL rules on sandboxClass so micro-VM templates may declare
several while gVisor keeps its cap, and say so in the messages.

What was actually missing was the volume NAME: ateom received mount paths
alone, so it inferred the single name by listing atelet's directory. Carry
each mount's name on the wire (Container.durable_dir_volume_mounts,
replacing the paths-only field, which is reserved rather than retyped since
atelet and ateom are separate images that can skew across a rollout), and
delete the inference. A container's binds now come from its own mounts, and
the actor-wide "has a durable share" question collapses to a bool.

The counter demo grows an optional --second-file-counter-directory, and a
micro-VM-only e2e case runs the lifecycle matrix against an Actor with two
durable volumes, asserting both counters advance together. gVisor skips it:
the template would be rejected at admission.
2026-07-30 20:03:55 -07:00
sfunkenhauser 87698ea9c6 Handle graceful worker termination (#517)
Part of [#23](https://github.com/agent-substrate/substrate/issues/23)

The overall plan is:
1) Trap SIGTERM in the ateom, and forward SIGTERM to all application
containers.
2) Once all application containers exit, ateom will exit itself.
3) The workerpool syncer will mark workers as 'DRAINING' when a pod
deletion is triggered (pod.DeletionTimestamp) to prevent actors from
being scheduled. On the pod Deleted event, the worker and actor (if it
was assigned) will be cleaned up.

This PR implements 3), there are 2 more PRs coming which will implement
1) and 2) for each ateom (gvisor and uVM).
2026-07-29 14:04:45 -07:00
Luiz Oliveira 0dd73e1679 Add pod selector to the WorkerPool scale subresource (#547)
WorkerPool already served /scale, so `kubectl scale` worked, but a HPA
pointed at one did not: the subresource declared no labelSelectorPath,
and HPA needs a selector to find the pods whose metrics it is averaging
or to compute ambiguous selectors.

https://github.com/agent-substrate/substrate/issues/198

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-27 12:02:33 -04:00
Benjamin Elder 9bc9f7ac92 ateom-microvm: mount durable-dir volumes over a writable virtio-fs share
DurableDir volumes shipped for gVisor only; the micro-VM runtime dropped
Container.durable_dir_volumes on the floor and the API rejected the
combination outright. Give the guest the volume:

  - cmd/ateom-microvm/internal/tarutil: archive/restore a directory tree
    preserving modes, ownership, times, symlinks and hardlinks, with
    os.Root-confined extraction. Snapshot support (a later commit) ships the
    volume as a tar; no reusable helper existed, and atelet's image untar does
    not preserve ownership.
  - A SECOND virtiofsd serves the actor's durable-dir directory read-write
    with cache=auto. The kataShared share stays read-only at cache=always,
    which is what its TODO asked for: writable volumes get their own share
    rather than weakening the overlay lower's caching.
  - The guest mounts the new tag at /run/ateom-durable and each WORKLOAD
    container binds <tag>/<volume> at its declared paths. The carrier is
    deliberately left alone: its rootfs is the read-only lower, where the
    agent could not create a missing mount point.
  - Lift the CEL rule banning durableDir on sandboxClass microvm.

ateom learns the volume name from the directory layout atelet prepares: the
wire protocol carries mount paths only, and the API allows at most one
durable-dir volume, so anything but exactly one directory is an error.

Snapshot scopes are unchanged so far, so the contents live only as long as
the actor. Checkpoint and restore follow.
2026-07-24 18:59:35 -07:00
Michelle Au 3230a6cebd Add validation to require that every volume is specified in volumeMounts (#532)
Part of #232

> It's a good idea to open an issue first for discussion.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-07-24 17:14:01 -07:00
Michelle Au 9300387fef Add basic per-actor external volume flow (#405)
Initial part of #232.

Extends the ActorTemplate API to add support for per-actor external
volumes and adds control plane and atelet hooks following the Actor
lifecycle.

Callouts to volume operations are abstracted with a Volume interface.
Right now, only a mock volume plugin for testing has been implemented,
but we will add CSI support next.

- [x] Tests pass
- [ ] Appropriate changes to documentation are included in the PR - Not
going to update documentation until we add CSI support.
2026-07-24 14:40:26 -07:00
Julian Gutierrez Oschmann 509f4c2170 Update ActorTemplate's container spec to mirror k8s semantic for command and args (#462)
* Add `args` field in addition to `command`.
* Implement logic in atelet to resolve both argv from the actor template
fields and the OCI image config (`entrypoint` and `cmd` fields).
* Update demos to not hardcode command as now we honor the image
default, which makes it not necessary.

Fixes #456
2026-07-20 16:15:05 -07:00
Dmitry Berkovich 37b5006d92 DurableDir support (#295)
Implement a layer allowing to take durableDir snapshot only, without
taking snapshot of entire memory. This is a part of the #119 feature.

The "durableDir" support is introduced via adding Volumes support to
ActorTemplate.

## Knowns issues / limitations


1. No MicroVM support for homedir yet. Will be implemented in following
PR.

2. Resume from SNAPSHOT_SCOPE_DATA temporary behavior
Current behavior of resume from durableStorage returns immediately after
gVisor process is started, it means container's bootstrap is not
completed by the time resume API returned to the caller. It causes the
issue that first curl request for suspended actor throws an HTTP
exception, since webserver listening on the port inside container is not
running yet.
PR #330 fixes the issue, for a meantime the workaround is to call `
kubectl ate resume actor` and after it call to the curl itself.

- [X ] Unit tests  pass
- [X ] E2E tests are updated and pass
2026-06-26 20:05:03 -07:00
Benjamin Elder 3d53de85eb microvm MVP cleanup: align rootfs behavior to gvisor, ... (#313)
Follow-up to https://github.com/agent-substrate/substrate/pull/287 /
#123

- align snapshot behavior: use a tmpfs (for writes) on top of read-only
viritio-fs mount for the container image rootfs instead of ext4 images
- enable multiple container support
- cleanup stale comments

> It's a good idea to open an issue first for discussion.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-06-26 20:02:11 -07:00
Dmitry Berkovich 125180ea65 feat: implement container readiness probes with custom HTTP endpoint … (#330)
Extends ActorTemplate.Container spec to allow defining a readyz probe. 
* ateoms (both gVisor & microVM) are updated and wait for readyz signal
if it was enabled on container when creating a new container or resume
from a snapshot.
* actorTemplates controller has been updated not to wait 20 sec during
golden snapshot creation, if readyz is enabled on actor. It assumes,
atelet call returns when all containers passed readyz.

Tested manually both counters: 
* gVisor 
* microvm 

Fixes #316 

- [x] e2e tests pass 
- [X] Appropriate changes to documentation are included in the PR
2026-06-26 18:59:56 -07:00
Benjamin Elder 833920b8e9 address review: controller/test/docs fixes
- workerpool_apply: rename applyMicroVMPodShape -> maybeApplyMicroVMPodShape and
  pass wp.Spec.SandboxClass instead of the whole WorkerPool.
- sandboxconfig validation test: add an arm64 micro-VM asset-set case.
- specconv: TODO to forward Seccomp/Sysctl + Apparmor/SELinux for OCI parity.
- run.go: clarify the dialAgentRetry per-attempt cap vs retry-gap comment.
- roadmap: drop the microVM line (shipped).
2026-06-25 13:44:25 -07:00
Benjamin Elder 36ce855839 api,atecontroller: add the micro-VM sandbox class
WorkerPool renders the micro-VM worker pod shape, and the SandboxConfig
validating admission policy enforces the micro-VM asset set (cloud-hypervisor,
guest kernel, guest image, kata config).
2026-06-25 13:44:25 -07:00
Julian Gutierrez Oschmann b4c82fe383 Add resources field to WorkerPool. (#279)
Allow WorkerPool resources to specify resource requirements its worker
pods.

Partially addresses
https://github.com/agent-substrate/substrate/issues/212.
2026-06-23 15:30:01 -07:00
Benjamin Elder fe93d160a1 feat: add SandboxClass support to ActorTemplate (#285)
Big UX improvement for iterating on #123 

Follow-up to https://github.com/agent-substrate/substrate/pull/270
(previously worker pools were directly referenced by actors so you just
made sure to pick an appropriate workerpool, we discussed there)

> It's a good idea to open an issue first for discussion.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-06-22 13:56:54 -07:00
Julian Gutierrez Oschmann 9f8475493b Decouple ActorTemplates from WorkerPools. (#270)
This change removes the hard-link from templates to worker pools,
allowing pools to be selected using k8s labels.

Both actors and actor templates are augmented with a label selector
field.

When an actor is resumed, we resolve the set of *eligible* worker pools
by evaluating the intersection of both actor and template selectors.
Then we find a free worker from one of them. The overall design is
described in [1].

[1]:
https://github.com/agent-substrate/substrate/issues/47#issuecomment-4712334620
2026-06-18 10:58:34 -07:00
Benjamin Elder 00a90a8eda Decouple Sandbox Binaries / Config from ActorTemplate (#261)
Fixes https://github.com/agent-substrate/substrate/issues/253

> It's a good idea to open an issue first for discussion.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR

Should make something like
https://github.com/agent-substrate/substrate/pull/239 less awkward, and
also https://github.com/agent-substrate/substrate/issues/255 (we don't
have to cram more fields into actortemplate for the shim binary and
update _all_ of the demo actortemplates to include them).
2026-06-17 09:46:46 -07:00
han2ni3bal-pixel 962ff6b1ae Add WorkerPool scheduling fields (#247)
**Updated Original PR Description**
## Summary

This PR adds a small, controlled set of scheduling fields to WorkerPool
so WorkerPool-managed Pods can be placed onto appropriate Kubernetes
nodes.

Added fields:

- `nodeSelector`
- `tolerations`
- `priorityClassName`
- `nodeAffinity`

## Scope

This intentionally does not expose a full `PodTemplateSpec`, and does
not add support for full `affinity`, `podAffinity`, or
`podAntiAffinity`.

Resource requests/limits are intentionally left out of scope for this CL
while the WorkerPool resource model is still being discussed separately.

The goal is to keep the first version small while addressing the
WorkerPool-to-Kubernetes-Node scheduling gap discussed in
[#212](https://github.com/agent-substrate/substrate/issues/212).

`nodeAffinity` is included to support heterogeneous node pools where
`nodeSelector` is too limited, such as requiring or preferring one of
several equivalent accelerator/local-SSD/cache node pools, or expressing
soft preferences during node pool migration.

`priorityClassName` is included to help preserve warm WorkerPool
capacity under cluster pressure. Since WorkerPool pods are the slots
available for actor resume, higher-priority pools can protect
latency-sensitive or interactive actor workloads from displacement by
lower-priority batch workloads, and allow different WorkerPools to
represent different service classes, e.g. interactive vs. batch.

## Related issue

Part of #212.
Related to #47.
2026-06-17 10:33:00 -04:00
Benjamin Elder 70beb3020f test: serialize envtest binary setup to fix concurrent-download race (#263)
## Problem

`go test -v ./...` (the `run-tests` job) intermittently fails during
envtest **setup** — not in any test logic:

- `fork/exec .../kubebuilder-envtest/k8s/1.36.0-linux-amd64/etcd: text
file busy`
- `setup-envtest: ... unable to create file kubectl from archive for
version-platform pair 1.36.0-linux-amd64`

Three packages use envtest (`cmd/ateapi/internal/controlapi`,
`cmd/atecontroller/internal/controllers`, `pkg/api/v1alpha1`). `go test
./...` runs them as separate test binaries **in parallel**, and each
`TestMain` shells out to `setup-envtest use`. On a cold cache they race
to download, extract, and exec the **shared** kubebuilder binaries —
hence `text file busy` (exec'ing etcd while it's still being written)
and the archive-extraction error.

Seen on `main` (e.g. the #259 push run) and on PRs. It only reproduces
on a **cold** cache, which is why it's intermittent and doesn't show up
locally once the cache is warm.

## Fix

Add `internal/envtestbins.BinaryAssetsDir()`, which runs `setup-envtest
use` under a **cross-process file lock** (`flock` on a fixed file under
`os.TempDir()`, shared by the sibling test binaries). The first caller
populates the shared cache while the others wait, then they hit a warm
no-op path lookup. The duplicated setup in all three `TestMain`s now
calls this helper.

The lock scope (user/machine-global) matches the resource it guards —
the kubebuilder binary cache is itself user-global, shared even across
repo checkouts.

## Testing

Reproduced locally by moving the warm cache aside and running the three
packages concurrently: it fails without the lock and passes with it.
Full `go test ./...` green.

Test-only change; no production code touched.
2026-06-16 18:11:11 -07:00
Julian Gutierrez Oschmann 0ec9b650a0 Enforce immutability of ActorTemplate spec (#259)
This change adds a CEL rule to enforce that ActorTemplate's spec field
is immutable.

Addresses some of the issues mentioned in #16.
2026-06-16 17:14:40 -04:00
Tim Hockin 68bb5efc8f Add more CRD validation (#205)
It's not all done but the fields we are playing with now are mostly
done.
2026-06-09 13:19:25 -04:00
Eitan Yarmush 0076b20c1a Support actortemplate secret env (#20)
A couple of TODOs/Discussions which I think are important to have based
on these changes.
1. Do we need a caching client for this to avoid calling the apiserver
every time?
2. Where/how do we want to resolve secrets generally? I think there are
good arguments to be made for the control-plane OR the "node" component.
In my mind this also related to:
https://github.com/agent-substrate/substrate/issues/18

Fixes #15 

> It's a good idea to open an issue first for discussion.

- [ x ] Tests pass
- [ x ] Appropriate changes to documentation are included in the PR
2026-06-08 16:04:50 -07:00
Julian Gutierrez Oschmann b7882d5825 Delete test.
No need to test DeepCopy as it's autogenerated.
2026-06-08 11:35:50 -07:00
Tim Hockin 25c538bca9 Remove unused ports block from API (#201)
Noticed this while digging around
2026-06-08 13:09:44 -04:00
Taahir Ahmed 708e85cf9d Don't use K8s corev1.EnvVar 2026-06-01 19:00:22 -07:00
Tim Hockin 1ac5ba32a1 Fix boilerplate in go files 2026-05-31 19:45:36 -07:00
Tim Hockin faeee7d04f Remove redundant boilerplate file, rename 2026-05-31 19:45:36 -07:00
Taahir Ahmed 9291df0b6d ActorTemplate: Enforce that all images must be pinned 2026-05-28 13:25:10 -07:00
Julian Gutierrez Oschmann 736a4fffc7 Make WorkerPool implement /scale sub-resource. (#83)
Now the worker pool can be scaled via regular `kubectl scale` commands.

Use server-side-apply to apply changes.
Propagate the effective number of replicas from the status of the
managed deployment.

Fixes https://github.com/agent-substrate/substrate/issues/48
2026-05-26 16:53:30 -07:00
Davanum Srinivas 55923825c7 chore(hack): add golangci-lint
Adds golangci-lint v2.12.2 using the per-tool-module pattern under
hack/tools/golangci-lint/. The verifier at hack/verify/golangci-lint.sh
is auto-discovered by hack/verify-all.sh, so the existing CI verify
step picks it up with no workflow YAML change.

Ships .golangci.yaml with the standard linter set, a make lint
target, and a verify wrapper that pins GOOS=linux so ateom-gvisor's
netlink references typecheck on macOS the same way they do on CI.

Adjustments to keep the first run clean: two dead-code deletions
((*runsc).cmdList, MemoryPullCache.gcsClient), two SA9003 empty
branches replaced with slog.Error / t.Logf, one S1008 return-bool
simplification, and inline //nolint for one intentional best-effort
ReleaseLock plus four audit-tracked SA1019 deprecations.

Signed-off-by: Davanum Srinivas <davanum@gmail.com>
2026-05-23 16:19:09 -07:00
Julian Gutierrez Oschmann b1fe163fcf Run tools/reorg tool.
This commit was automatically generated by the tools/reorg tool.

```
go run ./tools/reorg
go generate ./...
bash benchmarking/locust/generate_protos.sh
```
2026-05-22 14:43:26 -07:00