diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 64038a9d..399ecbfb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,9 +16,9 @@ updates: groups: go-deps: patterns: ["*"] - exclude-patterns: ["github.com/agent-substrate/substrate"] + exclude-patterns: ["go.ate.dev/substrate"] ignore: - - dependency-name: github.com/agent-substrate/substrate + - dependency-name: go.ate.dev/substrate - package-ecosystem: pip directory: /clients/python schedule: diff --git a/README.md b/README.md index beb79be9..d80333a6 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ signed, and meant to be used by digest: The digests are in the release notes and in the `images_.txt` asset; the release's `ate-env` binary defaults to them. The worker image -(`--worker-image`, `ateom-gvisor`) comes from the +(`--worker-image`, `ateworker-gvisor`) comes from the [Substrate repo](https://github.com/agent-substrate/substrate) and must match the Substrate version on your cluster. See [docs/release.md](docs/release.md) for verification, pinning and how releases are cut. @@ -65,10 +65,24 @@ verification, pinning and how releases are cut. Prerequisites: a cluster with [Agent Substrate](https://github.com/agent-substrate/substrate) installed and a snapshots bucket. ate-env is built against Substrate -[`da35a983`](https://github.com/agent-substrate/substrate/commit/da35a983) (2026-10-13). The cluster -needs a Substrate recent enough to accept what ate-env writes: the WorkerPool declares -`spec.sandboxClasses`, the ActorTemplate uses `snapshotConfig` and `wakeupProbe`, and `--worker-image` -must be the `ateom` image published by that Substrate install. +[`3a2ca623`](https://github.com/agent-substrate/substrate/commit/3a2ca623) (2026-10-16, Go module +`go.ate.dev/substrate`). The cluster needs a Substrate recent enough to accept what ate-env writes: +the WorkerPool declares `spec.sandboxClasses`, the ActorTemplate uses `snapshotConfig` and +`wakeupProbe`, names its sandbox class as the string `gvisor`, and declares CPU and memory limits. +That means Substrate at or after +[`63a17a92`](https://github.com/agent-substrate/substrate/commit/63a17a92) (2026-10-15), which also +renamed the worker image: `--worker-image` must be the `ateworker-gvisor` image published by that +Substrate install. An older Substrate rejects the string sandbox class (from +[`9ec49759`](https://github.com/agent-substrate/substrate/commit/9ec49759)) or the limits (from +[`c58f374a`](https://github.com/agent-substrate/substrate/commit/c58f374a)); use an ate-env from before +this bump with it. + +When you upgrade an existing cluster across that boundary, register your base templates again. +Substrate cannot update an ActorTemplate in place, and a base registered earlier has no CPU and memory +limits, thus each template `ate-env-api` derives from it for a rootfs image fails with +`resources: Required`. Delete the base and its derived templates (named `-<12 hex digits>`) with +`kubectl-ate delete actor-template`, then register the base again with `ate-env manifest template`. +Each environment created after that derives its template again, with the limits. ### 1. Deploy the Kubernetes Resources @@ -76,12 +90,12 @@ Deploy the namespace, worker pool, and API service: ```bash # With a release binary the API image defaults to the release's. The worker -# image is ateom-gvisor from the Substrate repo at your cluster's version; in +# image is ateworker-gvisor from the Substrate repo at your cluster's version; in # that checkout, `go run ./cmd/ate-setup publish worker-images` pushes it and # prints the digest-pinned ref. Use the digest: a :latest tag is whatever was # built last, and a stale worker image registers workers with no capacity. ate-env manifest \ - --worker-image /ateom-gvisor@sha256: | kubectl apply -f - + --worker-image /ateworker-gvisor@sha256: | kubectl apply -f - # From a source build, add: --api-image ghcr.io/agent-substrate/env/ate-env-api@sha256: # Ensure that the pods are running: @@ -106,6 +120,10 @@ ate-env manifest template \ # From a source build, add: --guest-image ghcr.io/agent-substrate/env/ate-env-guest@sha256: ``` +Each actor gets 2 CPUs and 4 GiB unless you pass `--cpu` and `--memory` (Kubernetes quantities such as +`500m` or `8Gi`; CPU at most `999.999`, as Substrate requires). Substrate requires both limits and books them against worker capacity; templates +derived per rootfs image inherit them. + That template's container is the guest image itself. To run another image unmodified, name it with `--rootfs-image`: the guest is then mounted into it as a read-only OCI image volume at `/ate` and started from there, so the rootfs image is never rebuilt. diff --git a/clients/go/client_test.go b/clients/go/client_test.go index 12caa568..56d46170 100644 --- a/clients/go/client_test.go +++ b/clients/go/client_test.go @@ -32,7 +32,7 @@ import ( "github.com/agent-substrate/env/internal/internaltest/fakerouter" "github.com/agent-substrate/env/internal/mcp" ateenvv1alpha "github.com/agent-substrate/env/proto/ateenv/v1alpha" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" ) diff --git a/cmd/ate-env/manifest.go b/cmd/ate-env/manifest.go index 4894a111..9552aec2 100644 --- a/cmd/ate-env/manifest.go +++ b/cmd/ate-env/manifest.go @@ -22,13 +22,14 @@ import ( "strings" "github.com/agent-substrate/env/internal/apiservice" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/spf13/cobra" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/yaml" @@ -42,6 +43,15 @@ const apiName = "ate-env-api" // one, so a pool declared with it is schedulable on a stock install. const sandboxConfigName = "gvisor-default" +// Default compute limits for each actor. Substrate requires both on every +// ActorTemplate since substrate#2285: the scheduler books them against worker +// capacity. Two CPUs and 4 GiB match the Python client's ResourceLimits +// default; size them for the workload with --cpu and --memory. +const ( + defaultActorCPU = "2" + defaultActorMemory = "4Gi" +) + // manifestConfig holds configuration for generating Kubernetes deployment manifests. type manifestConfig struct { namespace string @@ -56,8 +66,8 @@ type manifestConfig struct { func (c *manifestConfig) resolveImages() error { if c.workerImage == "" { - return errors.New(`--worker-image (or --ateom-image) is required: the worker image is -ateom-gvisor from the Substrate repo and must match the Substrate version + return errors.New(`--worker-image is required: the worker image is +ateworker-gvisor from the Substrate repo and must match the Substrate version deployed on your cluster. From a Substrate checkout at that version run "go run ./cmd/ate-setup publish worker-images": it pushes the images and prints the digest-pinned refs to use. Pin by digest: a registry's :latest @@ -93,6 +103,10 @@ type templateConfig struct { sidecars []string // sidecarReadyz are URLs the guest's /readyz waits for. sidecarReadyz []string + // cpu and memory are the actor's compute limits as Kubernetes quantities. + // Empty means defaultActorCPU and defaultActorMemory. + cpu string + memory string } // runtimeLayer is a parsed --layer value. @@ -139,6 +153,44 @@ func parseLayers(values []string) ([]runtimeLayer, error) { return layers, nil } +// Bounds Substrate applies to an ActorTemplate's limits +// (go.ate.dev/substrate/internal/resources.ValidateLimit and the +// Limits.quantity maxLength), checked here so the CLI never prints a template +// the control plane rejects. +const maxQuantityLen = 32 + +var ( + maxCPULimit = resource.MustParse("999.999") // the wire carries millicores below 1000 cores + maxMemoryLimit = resource.MustParse("9223372036854775807") // the wire carries an int64 byte count +) + +// validateResources checks --cpu and --memory the way Substrate validates +// resources.limits. An empty value means the default. +func (c *templateConfig) validateResources() error { + for _, l := range []struct { + flag, value string + max resource.Quantity + }{{"--cpu", c.cpu, maxCPULimit}, {"--memory", c.memory, maxMemoryLimit}} { + if l.value == "" { + continue + } + if len(l.value) > maxQuantityLen { + return fmt.Errorf("%s %q is longer than %d characters, which Substrate rejects", l.flag, l.value, maxQuantityLen) + } + q, err := resource.ParseQuantity(l.value) + if err != nil { + return fmt.Errorf("%s %q is not a Kubernetes quantity (e.g. 2, 500m, 4Gi): %w", l.flag, l.value, err) + } + if q.Sign() <= 0 { + return fmt.Errorf("%s must be greater than zero, got %q", l.flag, l.value) + } + if q.Cmp(l.max) > 0 { + return fmt.Errorf("%s %q is above Substrate's maximum of %s", l.flag, l.value, l.max.String()) + } + } + return nil +} + func (c *templateConfig) resolveImages() error { if c.guestImage == "" { return errors.New(`--guest-image is required when ate-env is built from source; release @@ -203,8 +255,8 @@ the "template" subcommand: ate-env manifest template`, cmd.Flags().StringVar(&mCfg.namespace, "namespace", apiservice.DefaultNamespace, "Kubernetes namespace to deploy into") cmd.Flags().StringVar(&mCfg.template, "template", apiservice.DefaultTemplate, "ActorTemplate name") - cmd.Flags().StringVar(&mCfg.workerImage, "worker-image", "", "digest-pinned worker image for the worker pool, e.g. ateom-gvisor built from the Substrate repo") - cmd.Flags().StringVar(&mCfg.workerImage, "ateom-image", "", "alias for --worker-image") + cmd.Flags().StringVar(&mCfg.workerImage, "worker-image", "", "digest-pinned worker image for the worker pool, e.g. ateworker-gvisor built from the Substrate repo") + cmd.Flags().StringVar(&mCfg.workerImage, "ateom-image", "", "alias for --worker-image, kept from before Substrate renamed ateom to ateworker") cmd.Flags().StringVar(&mCfg.apiImage, "api-image", defaultAPIImage, imageDefaultHelp("digest-pinned ate-env-api image for the API service", defaultAPIImage)) cmd.Flags().Int32Var(&mCfg.apiReplicas, "api-replicas", 1, "number of API service replicas") cmd.Flags().Int32Var(&mCfg.apiPort, "api-port", 7777, "port the ate-env-api service listens on") @@ -232,6 +284,15 @@ It prints YAML to stdout without touching the cluster.`, if err := tCfg.resolveImages(); err != nil { return err } + // An empty --cpu or --memory would silently mean the default. + for _, f := range []string{"cpu", "memory"} { + if cmd.Flags().Changed(f) && cmd.Flags().Lookup(f).Value.String() == "" { + return fmt.Errorf("--%s must not be empty; omit it for the default", f) + } + } + if err := tCfg.validateResources(); err != nil { + return err + } tmpl, err := buildTemplate(tCfg) if err != nil { return err @@ -250,6 +311,8 @@ It prints YAML to stdout without touching the cluster.`, cmd.Flags().StringArrayVar(&tCfg.layers, "layer", nil, "extra runtime mounted as a read-only image volume, as name=image@sha256:...=/mount/path (repeatable)") cmd.Flags().StringArrayVar(&tCfg.sidecars, "sidecar", nil, "command the guest starts and keeps running next to itself, an absolute binary path plus arguments, e.g. '/opt/opensandbox/execd --port 44772' (repeatable)") cmd.Flags().StringArrayVar(&tCfg.sidecarReadyz, "sidecar-readyz", nil, "URL the actor's readiness waits for, e.g. http://127.0.0.1:44772/ready (repeatable)") + cmd.Flags().StringVar(&tCfg.cpu, "cpu", defaultActorCPU, "CPU limit of each actor, a Kubernetes quantity such as 2 or 500m; Substrate requires one") + cmd.Flags().StringVar(&tCfg.memory, "memory", defaultActorMemory, "memory limit of each actor, a Kubernetes quantity such as 4Gi; Substrate requires one") return cmd } @@ -387,7 +450,7 @@ func buildWorkerPool(cfg manifestConfig) *atev1alpha1.WorkerPool { // the SandboxConfig the workers run; the template names the same // one in sandboxConfig.configName. SandboxClasses: []atev1alpha1.WorkerPoolSandboxClass{{ - Name: atev1alpha1.SandboxClassGvisor, + Name: atev1alpha1.SandboxClassGvisor, // the template's sandboxConfig.sandboxClass is the same string ConfigRef: &atev1alpha1.SandboxConfigReference{Name: sandboxConfigName}, }}, }, @@ -412,7 +475,7 @@ func buildActorTemplate(cfg templateConfig) *ateapipb.ActorTemplate { Name: "PORT", Value: "80", }}, - WakeupProbe: &ateapipb.ContainerWakeupProbe{ + WakeupProbe: &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{ Path: "/readyz", Port: 80, @@ -427,10 +490,21 @@ func buildActorTemplate(cfg templateConfig) *ateapipb.ActorTemplate { PreferredFidelity: ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, }, SandboxConfig: &ateapipb.SandboxConfig{ - SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, + SandboxClass: string(atev1alpha1.SandboxClassGvisor), ConfigName: sandboxConfigName, }, + Resources: &ateapipb.Resources{Limits: []*ateapipb.Limits{ + {Name: "cpu", Quantity: orDefault(cfg.cpu, defaultActorCPU)}, + {Name: "memory", Quantity: orDefault(cfg.memory, defaultActorMemory)}, + }}, + } +} + +func orDefault(v, def string) string { + if v == "" { + return def } + return v } // buildTemplate returns the ActorTemplate for cfg: the guest template from diff --git a/cmd/ate-env/manifest_test.go b/cmd/ate-env/manifest_test.go index 2169d039..95f26594 100644 --- a/cmd/ate-env/manifest_test.go +++ b/cmd/ate-env/manifest_test.go @@ -20,8 +20,8 @@ import ( "testing" "github.com/agent-substrate/env/internal/apiservice" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" ) @@ -116,11 +116,10 @@ func TestBuildManifests(t *testing.T) { if sc.Name != atev1alpha1.SandboxClassGvisor || sc.ConfigRef == nil || sc.ConfigRef.Name != sandboxConfigName { t.Errorf("workerpool sandboxClasses[0] = %+v, want gvisor with configRef %q", sc, sandboxConfigName) } - // The pool's class is a CRD string and the template's a proto enum: two - // vocabularies for one thing. A pool of the wrong class never hosts the - // template's actors. - tmplClass := strings.ToLower(strings.TrimPrefix( - buildActorTemplate(testTemplateConfig()).GetSandboxConfig().GetSandboxClass().String(), "SANDBOX_CLASS_")) + // The pool's class and the template's are the same string since + // substrate#2586 made the proto field a string. A pool of the wrong class + // never hosts the template's actors. + tmplClass := buildActorTemplate(testTemplateConfig()).GetSandboxConfig().GetSandboxClass() if tmplClass != string(sc.Name) { t.Errorf("workerpool sandbox class %q != template sandbox class %q", sc.Name, tmplClass) } @@ -176,6 +175,74 @@ func TestBuildActorTemplate(t *testing.T) { if probe.GetHttpGet() == nil || probe.GetHttpGet().GetPath() != "/readyz" || probe.GetHttpGet().GetPort() != 80 { t.Errorf("wakeupProbe = %+v, want HTTP GET /readyz on port 80", probe) } + // Substrate rejects an ActorTemplate without both limits (substrate#2285). + if got := limits(template); got["cpu"] != defaultActorCPU || got["memory"] != defaultActorMemory || len(got) != 2 { + t.Errorf("resources.limits = %v, want cpu=%s memory=%s", got, defaultActorCPU, defaultActorMemory) + } + if got := template.GetSandboxConfig().GetSandboxClass(); got != "gvisor" { + t.Errorf("sandbox class = %q, want gvisor (Substrate accepts gvisor or microvm)", got) + } +} + +func limits(tmpl *ateapipb.ActorTemplate) map[string]string { + m := map[string]string{} + for _, l := range tmpl.GetResources().GetLimits() { + m[l.GetName()] = l.GetQuantity() + } + return m +} + +func TestBuildActorTemplateResources(t *testing.T) { + cfg := testTemplateConfig() + cfg.cpu, cfg.memory = "500m", "1Gi" + if got := limits(buildActorTemplate(cfg)); got["cpu"] != "500m" || got["memory"] != "1Gi" { + t.Errorf("resources.limits = %v, want cpu=500m memory=1Gi", got) + } +} + +func TestTemplateConfigRejectsBadResources(t *testing.T) { + for _, tc := range []struct{ cpu, memory string }{ + {"two", ""}, + {"", "4GB!"}, + {"0", ""}, + {"", "-1Gi"}, + // Substrate's bounds: cpu below 1000 cores, memory an int64 byte + // count, each quantity at most 32 characters. + {"1000", ""}, + {"1e3", ""}, + {"999.9991", ""}, + {"", "9223372036854775808"}, + {"0.0000000000000000000000000000001", ""}, + } { + cfg := testTemplateConfig() + cfg.cpu, cfg.memory = tc.cpu, tc.memory + if err := cfg.validateResources(); err == nil { + t.Errorf("cpu=%q memory=%q: want error, got nil", tc.cpu, tc.memory) + } + } + for _, tc := range []struct{ cpu, memory string }{ + {"999.999", "9223372036854775807"}, + {"500m", "8Gi"}, + {"", ""}, + } { + cfg := testTemplateConfig() + cfg.cpu, cfg.memory = tc.cpu, tc.memory + if err := cfg.validateResources(); err != nil { + t.Errorf("cpu=%q memory=%q: %v, want accepted", tc.cpu, tc.memory, err) + } + } +} + +func TestManifestTemplateCommandRejectsEmptyResource(t *testing.T) { + for _, flag := range []string{"--cpu=", "--memory="} { + cmd := newManifestCommand() + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + cmd.SetArgs([]string{"template", "--guest-image=example.com/guest@sha256:aaaa", "--snapshots-bucket=gs://b/", flag}) + if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "must not be empty") { + t.Errorf("%s: got %v, want an error about the empty value", flag, err) + } + } } func TestBuildManifestsCustomAPIPort(t *testing.T) { @@ -291,6 +358,8 @@ func TestManifestTemplateCommandExecution(t *testing.T) { "template", "--guest-image=example.com/guest@sha256:aaaa", "--snapshots-bucket=gs://bucket/prefix/", + "--cpu=4", + "--memory=8Gi", }) if err := cmd.Execute(); err != nil { t.Fatalf("execute manifest template: %v", err) @@ -299,6 +368,11 @@ func TestManifestTemplateCommandExecution(t *testing.T) { if !strings.Contains(out, "image: example.com/guest@sha256:aaaa") || !strings.Contains(out, "storageLocation: gs://bucket/prefix/") { t.Errorf("expected guest image and storage location in output:\n%s", out) } + for _, want := range []string{"sandboxClass: gvisor", "name: cpu\n quantity: \"4\"", "name: memory\n quantity: 8Gi"} { + if !strings.Contains(out, want) { + t.Errorf("output lacks %q:\n%s", want, out) + } + } } const testHex64 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" diff --git a/docs/release.md b/docs/release.md index 7532bbcf..2e87d802 100644 --- a/docs/release.md +++ b/docs/release.md @@ -26,7 +26,7 @@ and asks for both flags. `make build-cli` reproduces a release-style binary locally from `ATE_ENV_API_IMAGE` and `ATE_ENV_GUEST_IMAGE`. The worker image (`--worker-image`) is not published here: it is -`ateom-gvisor` from the [Substrate repo](https://github.com/agent-substrate/substrate) +`ateworker-gvisor` from the [Substrate repo](https://github.com/agent-substrate/substrate) and must match the Substrate version deployed on the cluster. From a Substrate checkout at that version, `go run ./cmd/ate-setup publish worker-images` builds and pushes it and prints the digest-pinned ref. Use the digest: a registry's diff --git a/go.mod b/go.mod index 744476bf..f611624d 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/agent-substrate/env go 1.27.0 require ( - github.com/agent-substrate/substrate v0.4.1-0.20261013175625-da35a983ed34 github.com/google/uuid v1.6.0 github.com/modelcontextprotocol/go-sdk v1.8.0 github.com/spf13/cobra v1.10.2 + go.ate.dev/substrate v0.5.1-0.20261016164544-3a2ca6235748 google.golang.org/grpc v1.84.0 google.golang.org/protobuf v1.36.12 k8s.io/api v0.37.1 diff --git a/go.sum b/go.sum index 11e7bca8..edf211fe 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/agent-substrate/substrate v0.4.1-0.20261013175625-da35a983ed34 h1:pGiVJjwYbQMDJQj6le/4izVcIz0tHO1i2jQJ8EyKpCU= -github.com/agent-substrate/substrate v0.4.1-0.20261013175625-da35a983ed34/go.mod h1:XVQwhYEmOlb+ILs9BvSCHXYhgo2F6/N2qN4DmRwogQI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -111,6 +109,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +go.ate.dev/substrate v0.5.1-0.20261016164544-3a2ca6235748 h1:QrTauJhnfn0FL0t/GLzVo4bmeVUYMT6e4x8QSaQF8s0= +go.ate.dev/substrate v0.5.1-0.20261016164544-3a2ca6235748/go.mod h1:EktaeScxg724tF4cjtLV8kdz2xN3FH8NOTk/A95jSBY= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= diff --git a/integrations/nemo-gym/README.md b/integrations/nemo-gym/README.md index 214a64cf..68b247dd 100644 --- a/integrations/nemo-gym/README.md +++ b/integrations/nemo-gym/README.md @@ -53,11 +53,13 @@ On the cluster: ```bash ate-env manifest \ - --guest-image \ --api-image \ - --ateom-image \ - --snapshots-bucket gs:///ate-env/ | kubectl apply -f - + --worker-image | kubectl apply -f - kubectl get pods -n ate-env # api + warm workers Running + # once ate-env-api is Running (it creates the ate-env atespace templates live in): + ate-env manifest template \ + --guest-image \ + --snapshots-bucket gs:///ate-env/ | kubectl-ate create actor-template -f - ``` If your `ateapi` requires authentication (it does on any standard install), the diff --git a/internal/apiservice/imagetemplate.go b/internal/apiservice/imagetemplate.go index bd8f0280..af4d66c2 100644 --- a/internal/apiservice/imagetemplate.go +++ b/internal/apiservice/imagetemplate.go @@ -21,7 +21,7 @@ import ( "regexp" "strings" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/internal/apiservice/imagetemplate_test.go b/internal/apiservice/imagetemplate_test.go index 8da88ee0..d7050e23 100644 --- a/internal/apiservice/imagetemplate_test.go +++ b/internal/apiservice/imagetemplate_test.go @@ -18,7 +18,7 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) @@ -36,11 +36,27 @@ func modeABase() *ateapipb.ActorTemplate { Image: testGuestImage, Command: []string{"/ko-app/ate-env-guest", "-workspace", "/workspace"}, Env: []*ateapipb.EnvVar{{Name: "PORT", Value: "80"}}, - WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Path: "/readyz", Port: 80}}, + WakeupProbe: &ateapipb.WakeupProbe{HttpGet: &ateapipb.HttpGetAction{Path: "/readyz", Port: 80}}, }}, SnapshotConfig: &ateapipb.SnapshotConfig{StorageLocation: "gs://b/p/", PreferredFidelity: ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY}, - SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"}, - Status: &ateapipb.ActorTemplateStatus{}, + SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: "gvisor", ConfigName: "gvisor-default"}, + Resources: &ateapipb.Resources{Limits: []*ateapipb.Limits{ + {Name: "cpu", Quantity: "2"}, {Name: "memory", Quantity: "4Gi"}, + }}, + Status: &ateapipb.ActorTemplateStatus{}, + } +} + +// Substrate requires cpu and memory limits on every ActorTemplate, thus a +// derived template must carry the base's limits unchanged. +func TestDeriveImageTemplateKeepsResources(t *testing.T) { + base := modeABase() + got, err := DeriveImageTemplate(base, "default-template-0123456789ab", testRootfsImage) + if err != nil { + t.Fatal(err) + } + if !proto.Equal(got.GetResources(), base.GetResources()) { + t.Errorf("derived resources = %v, want the base's %v", got.GetResources(), base.GetResources()) } } diff --git a/internal/apiservice/server.go b/internal/apiservice/server.go index a8791870..f3466f60 100644 --- a/internal/apiservice/server.go +++ b/internal/apiservice/server.go @@ -26,7 +26,7 @@ import ( "github.com/agent-substrate/env/internal/ate" ateenvv1alpha "github.com/agent-substrate/env/proto/ateenv/v1alpha" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" @@ -540,11 +540,11 @@ func ActorStatusToEnvStatus(st *ateapipb.ActorStatus) ateenvv1alpha.EnvironmentS case ateapipb.ActorState_ACTOR_STATE_SUSPENDING: return ateenvv1alpha.EnvironmentStatus_ENVIRONMENT_STATUS_SUSPENDING case ateapipb.ActorState_ACTOR_STATE_SUSPENDED: + // Since substrate#2620 a paused actor is SUSPENDED too, with a + // node-local latest snapshot; Substrate has no PAUSED or PAUSING + // state. ate-env never pauses, so ENVIRONMENT_STATUS_PAUSED and + // _PAUSING are no longer produced. return ateenvv1alpha.EnvironmentStatus_ENVIRONMENT_STATUS_SUSPENDED - case ateapipb.ActorState_ACTOR_STATE_PAUSING: - return ateenvv1alpha.EnvironmentStatus_ENVIRONMENT_STATUS_PAUSING - case ateapipb.ActorState_ACTOR_STATE_PAUSED: - return ateenvv1alpha.EnvironmentStatus_ENVIRONMENT_STATUS_PAUSED case ateapipb.ActorState_ACTOR_STATE_CRASHED: return ateenvv1alpha.EnvironmentStatus_ENVIRONMENT_STATUS_CRASHED case ateapipb.ActorState_ACTOR_STATE_DELETING: diff --git a/internal/apiservice/server_test.go b/internal/apiservice/server_test.go index 87a06c83..9ecccd5c 100644 --- a/internal/apiservice/server_test.go +++ b/internal/apiservice/server_test.go @@ -27,7 +27,7 @@ import ( "github.com/agent-substrate/env/internal/internaltest/fakecontrol" "github.com/agent-substrate/env/internal/internaltest/fakerouter" ateenvv1alpha "github.com/agent-substrate/env/proto/ateenv/v1alpha" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" @@ -570,8 +570,6 @@ func TestActorStatusToEnvStatus(t *testing.T) { {"running", &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_RUNNING}, ateenvv1alpha.EnvironmentStatus_ENVIRONMENT_STATUS_RUNNING}, {"suspending", &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_SUSPENDING}, ateenvv1alpha.EnvironmentStatus_ENVIRONMENT_STATUS_SUSPENDING}, {"suspended", &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED}, ateenvv1alpha.EnvironmentStatus_ENVIRONMENT_STATUS_SUSPENDED}, - {"pausing", &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_PAUSING}, ateenvv1alpha.EnvironmentStatus_ENVIRONMENT_STATUS_PAUSING}, - {"paused", &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_PAUSED}, ateenvv1alpha.EnvironmentStatus_ENVIRONMENT_STATUS_PAUSED}, {"crashed", &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_CRASHED}, ateenvv1alpha.EnvironmentStatus_ENVIRONMENT_STATUS_CRASHED}, {"deleting", &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_DELETING}, ateenvv1alpha.EnvironmentStatus_ENVIRONMENT_STATUS_DELETING}, } @@ -598,10 +596,10 @@ func seedGuestTemplate(control *fakecontrol.Server, atespace, name string) { Image: testGuestImage, Command: []string{"/ko-app/ate-env-guest"}, Env: []*ateapipb.EnvVar{{Name: "PORT", Value: "80"}}, - WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Path: "/readyz", Port: 80}}, + WakeupProbe: &ateapipb.WakeupProbe{HttpGet: &ateapipb.HttpGetAction{Path: "/readyz", Port: 80}}, }}, SnapshotConfig: &ateapipb.SnapshotConfig{StorageLocation: "gs://bucket/ate-env/"}, - SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"}, + SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: "gvisor", ConfigName: "gvisor-default"}, }) } diff --git a/internal/apiservice/wakeretry.go b/internal/apiservice/wakeretry.go index fda16dc9..d9c88571 100644 --- a/internal/apiservice/wakeretry.go +++ b/internal/apiservice/wakeretry.go @@ -21,7 +21,7 @@ import ( "time" ateenvv1alpha "github.com/agent-substrate/env/proto/ateenv/v1alpha" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/internal/apiservice/wakeretry_test.go b/internal/apiservice/wakeretry_test.go index 78ac34b4..19d49265 100644 --- a/internal/apiservice/wakeretry_test.go +++ b/internal/apiservice/wakeretry_test.go @@ -23,7 +23,7 @@ import ( "github.com/agent-substrate/env/guest" "github.com/agent-substrate/env/internal/apiservice" ateenvv1alpha "github.com/agent-substrate/env/proto/ateenv/v1alpha" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" diff --git a/internal/ate/client.go b/internal/ate/client.go index 75af1598..c434e753 100644 --- a/internal/ate/client.go +++ b/internal/ate/client.go @@ -35,7 +35,7 @@ import ( "strings" "sync" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" diff --git a/internal/ate/client_test.go b/internal/ate/client_test.go index bad2250b..727e5fcf 100644 --- a/internal/ate/client_test.go +++ b/internal/ate/client_test.go @@ -23,7 +23,7 @@ import ( "github.com/agent-substrate/env/internal/ate" "github.com/agent-substrate/env/internal/internaltest/fakecontrol" "github.com/agent-substrate/env/internal/internaltest/fakerouter" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) type fixture struct { diff --git a/internal/internaltest/fakecontrol/fakecontrol.go b/internal/internaltest/fakecontrol/fakecontrol.go index efda16ec..1a87acc1 100644 --- a/internal/internaltest/fakecontrol/fakecontrol.go +++ b/internal/internaltest/fakecontrol/fakecontrol.go @@ -32,7 +32,7 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" @@ -299,7 +299,8 @@ func (s *Server) PauseActor(ctx context.Context, req *ateapipb.PauseActorRequest if err != nil { return nil, err } - a.Status = &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_PAUSED} + // Substrate reports a paused actor as SUSPENDED (substrate#2620). + a.Status = &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED} return &ateapipb.PauseActorResponse{Actor: clone(a)}, nil } diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go index f069e1ca..3cf48def 100644 --- a/internal/mcp/server_test.go +++ b/internal/mcp/server_test.go @@ -28,8 +28,8 @@ import ( "github.com/agent-substrate/env/internal/internaltest/fakerouter" internalmcp "github.com/agent-substrate/env/internal/mcp" "github.com/agent-substrate/env/internal/tool" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/modelcontextprotocol/go-sdk/mcp" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) func TestMCPServer(t *testing.T) {