Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,20 @@ Deploy the namespace, worker pool, and API service:
```bash
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
ate-env manifest \
--guest-image gcr.io/$GOOGLE_CLOUD_PROJECT/ate-env-guest@sha256:4f5678b9304a9047551fc95458e7b948c77d6fce5337de1897888daa7e0e4900 \
--api-image gcr.io/$GOOGLE_CLOUD_PROJECT/ate-env-api@sha256:8579e1eebdd652cd2bfc7a130d4ebc2ce0d7a1a9d2efcb9d04b9eedf597a027d \
--worker-image gcr.io/$GOOGLE_CLOUD_PROJECT/ateom-gvisor-715889664656de67e44382a8d6ab981d@sha256:7a5f89e9c8ca875eee611b05fdf003b63b260b631362c83c1099073d003e0372 \
--snapshots-bucket gs://$GOOGLE_CLOUD_PROJECT/ate-env/ | kubectl apply -f -
--api-image gcr.io/$GOOGLE_CLOUD_PROJECT/ate-env-api@sha256:e9c4481903d6ca2c8affbf4323f3cc2925d9eaac6513521535b3c905cdab236b \
--worker-image gcr.io/$GOOGLE_CLOUD_PROJECT/ateom-gvisor-715889664656de67e44382a8d6ab981d@sha256:7a5f89e9c8ca875eee611b05fdf003b63b260b631362c83c1099073d003e0372 | kubectl apply -f -

# Ensure that the pods are running:
kubectl get pods -n ate-env
```

### 2. Register the ActorTemplate in Substrate

Substrate manages ActorTemplates directly in its control plane rather than Kubernetes CRDs. Use `ate-env manifest --template-only` to generate the Substrate ActorTemplate manifest:
Substrate manages ActorTemplates directly in its control plane rather than Kubernetes CRDs. Use `ate-env manifest template` to generate the Substrate ActorTemplate manifest:

```bash
ate-env manifest --template-only \
--guest-image gcr.io/$GOOGLE_CLOUD_PROJECT/ate-env-guest@sha256:4f5678b9304a9047551fc95458e7b948c77d6fce5337de1897888daa7e0e4900 \
ate-env manifest template \
--guest-image gcr.io/$GOOGLE_CLOUD_PROJECT/ate-env-guest@sha256:0b37ad8f0d6ae0bfdd01b97dfccd0139b59aedac6ec0f6d351333f0776acd3dd \
--snapshots-bucket gs://$GOOGLE_CLOUD_PROJECT/ate-env/ | kubectl-ate create actor-template -f -
```

Expand Down Expand Up @@ -139,9 +137,8 @@ Use "ate-env [command] --help" for more information about a command.
$ ate-env manifest --help
Manifest generates Kubernetes manifests for everything environments need on
a cluster that already runs the Agent Substrate system: the target
namespace, a WorkerPool of pre-warmed workers, the ActorTemplate that
environments are created from, and the ate-env-api service. It prints YAML to
stdout without touching the cluster; apply it with kubectl.
namespace, a WorkerPool of pre-warmed workers, and the ate-env-api service.
It prints YAML to stdout without touching the cluster; apply it with kubectl.
```

## API
Expand Down
2 changes: 1 addition & 1 deletion clients/go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func (f *fixture) create(t *testing.T, id string) *env.Env {
sb, err := f.client.Create(t.Context(), &ateenvv1alpha.CreateEnvironmentRequest{
Id: id,
Template: &ateenvv1alpha.Template{
Name: "default-env",
Name: "default-template",
Atespace: "envs",
},
})
Expand Down
6 changes: 3 additions & 3 deletions clients/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ channel yourself (e.g. custom gRPC options), pass
env = await client.create("dev1")
```

The server instantiates the environment from the `default-env`
ActorTemplate in the `default` atespace unless you override it:
The server instantiates the environment from the `default-template`
ActorTemplate in the `ate-env` atespace unless you override it:

```python
env = await client.create("dev1", template_name="my-template",
Expand All @@ -144,7 +144,7 @@ env = await client.create("dev1", template_name="my-template",
To get a handle to an environment that already exists (no RPC is made):

```python
env = client.env("dev1") # atespace defaults to "default"
env = client.env("dev1") # atespace defaults to "ate-env"
```

A freshly created (or suspended) environment starts serving on first
Expand Down
6 changes: 3 additions & 3 deletions clients/python/src/ate_env/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

__all__ = ["Client", "DEFAULT_ATESPACE"]

DEFAULT_ATESPACE = "default"
DEFAULT_ATESPACE = "ate-env"


def _normalize_target(endpoint: str) -> str:
Expand Down Expand Up @@ -77,8 +77,8 @@ async def create(
) -> Env:
"""Register and start a new environment; returns a handle to it.

The server fills defaults for the template (name "default-env" in
atespace "default") when none is given.
The server fills defaults for the template (name "default-template" in
atespace "ate-env") when none is given.
"""
req = env_pb2.CreateEnvironmentRequest(id=id, atespace=atespace)
if template_name or template_atespace:
Expand Down
4 changes: 2 additions & 2 deletions clients/python/tests/e2e/test_full_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

TARGET = os.environ.get("ATE_ENV_API_TARGET")
READY_TIMEOUT = float(os.environ.get("ATE_ENV_READY_TIMEOUT", "180"))
# Optional ActorTemplate override; the server default is "default-env".
# Optional ActorTemplate override; the server default is "default-template".
TEMPLATE = os.environ.get("ATE_ENV_TEMPLATE")

pytestmark = pytest.mark.skipif(
Expand Down Expand Up @@ -90,7 +90,7 @@ async def test_full_lifecycle(client):
env_id = f"pye2e-{uuid.uuid4().hex[:8]}"
env = await client.create(env_id, template_name=TEMPLATE)
assert env.id == env_id
assert env.atespace == "default"
assert env.atespace == "ate-env"

try:
info = await env.info()
Expand Down
10 changes: 5 additions & 5 deletions clients/python/tests/fakes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async def _require_env(context: grpc.aio.ServicerContext) -> tuple[str, str]:
env_id = md.get("x-env-id", "")
if not env_id:
await context.abort(grpc.StatusCode.INVALID_ARGUMENT, "x-env-id header is required")
return env_id, md.get("x-env-atespace", "default")
return env_id, md.get("x-env-atespace", "ate-env")


class FakeEnvironmentService(env_pb2_grpc.EnvironmentServiceServicer):
Expand All @@ -32,7 +32,7 @@ def __init__(self):

async def CreateEnvironment(self, request, context):
self.last_create_request = request
atespace = request.atespace or "default"
atespace = request.atespace or "ate-env"
key = (atespace, request.id)
if not request.id:
await context.abort(grpc.StatusCode.INVALID_ARGUMENT, "id is required")
Expand All @@ -41,7 +41,7 @@ async def CreateEnvironment(self, request, context):
grpc.StatusCode.ALREADY_EXISTS,
f'environment "{request.id}" already exists',
)
template = env_pb2.Template(name="default-env", atespace="default")
template = env_pb2.Template(name="default-template", atespace="ate-env")
if request.HasField("template"):
if request.template.name:
template.name = request.template.name
Expand All @@ -67,11 +67,11 @@ async def SuspendEnvironment(self, request, context):

async def DeleteEnvironment(self, request, context):
await self._lookup(request, context)
del self.environments[(request.atespace or "default", request.id)]
del self.environments[(request.atespace or "ate-env", request.id)]
return env_pb2.DeleteEnvironmentResponse()

async def _lookup(self, request, context):
key = (request.atespace or "default", request.id)
key = (request.atespace or "ate-env", request.id)
environment = self.environments.get(key)
if environment is None:
await context.abort(
Expand Down
8 changes: 4 additions & 4 deletions clients/python/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ async def test_create_uses_server_defaults(fake_stack):
client, fakes = fake_stack
env = await client.create("dev1")
assert env.id == "dev1"
assert env.atespace == "default"
stored = fakes.environments.environments[("default", "dev1")]
assert stored.template.name == "default-env"
assert stored.template.atespace == "default"
assert env.atespace == "ate-env"
stored = fakes.environments.environments[("ate-env", "dev1")]
assert stored.template.name == "default-template"
assert stored.template.atespace == "ate-env"


async def test_create_omits_template_when_not_given(fake_stack):
Expand Down
2 changes: 1 addition & 1 deletion clients/python/tests/test_env_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async def test_read_file_multi_chunk(fake_stack):
chunks = [chunk async for chunk in env.read_file("/data.bin")]
assert len(chunks) >= 3
assert b"".join(chunks) == content
assert fakes.filesystem.last_env == ("dev1", "default")
assert fakes.filesystem.last_env == ("dev1", "ate-env")


async def test_read_file_bytes(fake_stack):
Expand Down
9 changes: 5 additions & 4 deletions cmd/ate-env/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"

"github.com/agent-substrate/env/clients/go"
"github.com/agent-substrate/env/internal/apiservice"
ateenvv1alpha "github.com/agent-substrate/env/proto/ateenv/v1alpha"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -48,7 +49,7 @@ func newGuestCommand(id string) *cobra.Command {
},
}
guestCmd.PersistentFlags().StringVar(&endpoint, "api", envOr("SUBSTRATE_ENV_API", "127.0.0.1:7777"), "address of the ate-env-api service (e.g. localhost:7777)")
guestCmd.PersistentFlags().StringVar(&atespace, "atespace", "default", "Substrate atespace")
guestCmd.PersistentFlags().StringVar(&atespace, "atespace", apiservice.DefaultAtespace, "Substrate atespace")

guestCmd.AddCommand(&cobra.Command{
Use: "read <path>",
Expand Down Expand Up @@ -139,7 +140,7 @@ func newCreateCommand() *cobra.Command {
},
}
cmd.Flags().StringVar(&endpoint, "api", envOr("SUBSTRATE_ENV_API", "127.0.0.1:7777"), "address of the ate-env-api service (e.g. localhost:7777)")
cmd.Flags().StringVar(&atespace, "atespace", "default", "Substrate atespace")
cmd.Flags().StringVar(&atespace, "atespace", apiservice.DefaultAtespace, "Substrate atespace")
cmd.Flags().StringVar(&createTemplate, "template", "", "ActorTemplate name (defaults to server default)")
cmd.Flags().StringVar(&createTemplateAtespace, "template-atespace", "", "Substrate atespace of the ActorTemplate (defaults to environment atespace)")
return cmd
Expand Down Expand Up @@ -167,7 +168,7 @@ func newSuspendCommand() *cobra.Command {
},
}
cmd.Flags().StringVar(&endpoint, "api", envOr("SUBSTRATE_ENV_API", "127.0.0.1:7777"), "address of the ate-env-api service (e.g. localhost:7777)")
cmd.Flags().StringVar(&atespace, "atespace", "default", "Substrate atespace")
cmd.Flags().StringVar(&atespace, "atespace", apiservice.DefaultAtespace, "Substrate atespace")
return cmd
}

Expand All @@ -193,7 +194,7 @@ func newDeleteCommand() *cobra.Command {
},
}
cmd.Flags().StringVar(&endpoint, "api", envOr("SUBSTRATE_ENV_API", "127.0.0.1:7777"), "address of the ate-env-api service (e.g. localhost:7777)")
cmd.Flags().StringVar(&atespace, "atespace", "default", "Substrate atespace")
cmd.Flags().StringVar(&atespace, "atespace", apiservice.DefaultAtespace, "Substrate atespace")
return cmd
}

Expand Down
140 changes: 85 additions & 55 deletions cmd/ate-env/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,50 @@ import (
// apiName is the name of the API service Deployment and Service.
const apiName = "ate-env-api"

// manifestConfig holds configuration for generating Kubernetes deployment manifests.
type manifestConfig struct {
namespace string
namespace string
template string
workerPool string
workerImage string
replicas int32
apiImage string
apiReplicas int32
apiPort int32
poolLabels map[string]string
}

func (c *manifestConfig) resolveImages() error {
if c.workerImage == "" || c.apiImage == "" {
return errors.New(`--api-image and --worker-image (or --ateom-image) are required; use the
digest-pinned images published by the latest release (the README
quickstart records them), or build and push your own.`)
}
return nil
}

// templateConfig holds configuration for generating a Substrate ActorTemplate manifest.
type templateConfig struct {
template string
workerPool string
atespace string
guestImage string
workerImage string
snapshotsBucket string
replicas int32
apiImage string
apiReplicas int32
apiPort int32
guestCommand []string
snapshotsBucket string
poolLabels map[string]string
atespace string
templateOnly bool
}

func (c *templateConfig) resolveImages() error {
if c.guestImage == "" {
return errors.New("--guest-image is required; use the digest-pinned ate-env-guest image")
}
if c.snapshotsBucket == "" {
return errors.New("--snapshots-bucket is required; use an object-storage bucket (e.g. gs://bucket/prefix/)")
}
return nil
}

func newManifestCommand() *cobra.Command {
cfg := manifestConfig{}
mCfg := manifestConfig{}

cmd := &cobra.Command{
Use: "manifest",
Expand All @@ -51,61 +76,66 @@ namespace, a WorkerPool of pre-warmed workers, and the ate-env-api service.
It prints YAML to stdout without touching the cluster; apply it with kubectl.

To generate the Substrate ActorTemplate manifest (for kubectl-ate), use
the --template-only flag.`,
the "template" subcommand: ate-env manifest template`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if err := cfg.resolveImages(); err != nil {
return err
}
if cfg.template == "" {
cfg.template = apiservice.DefaultTemplate
if mCfg.template == "" {
mCfg.template = apiservice.DefaultTemplate
}
if cfg.workerPool == "" {
cfg.workerPool = cfg.template + "-workerpool"
if mCfg.workerPool == "" {
mCfg.workerPool = mCfg.template + "-workerpool"
}
cfg.poolLabels = map[string]string{"workload": cfg.template}

if cfg.templateOnly {
return writeActorTemplate(cmd.OutOrStdout(), buildActorTemplate(cfg))
mCfg.poolLabels = map[string]string{"workload": mCfg.template}
if err := mCfg.resolveImages(); err != nil {
return err
}
return writeManifests(cmd.OutOrStdout(), buildManifests(cfg))
return writeManifests(cmd.OutOrStdout(), buildManifests(mCfg))
},
}

cmd.Flags().StringVar(&cfg.namespace, "namespace", apiservice.DefaultNamespace, "Kubernetes namespace to deploy into")
cmd.Flags().StringVar(&cfg.atespace, "atespace", "default", "Substrate atespace for the ActorTemplate")
cmd.Flags().StringVar(&cfg.template, "template", apiservice.DefaultTemplate, "ActorTemplate name")
cmd.Flags().StringVar(&cfg.guestImage, "guest-image", "", "digest-pinned ate-env-guest image (repo@sha256:...)")
cmd.Flags().StringVar(&cfg.workerImage, "worker-image", "", "digest-pinned worker image for the worker pool, e.g. ateom-gvisor built from the Substrate repo")
cmd.Flags().StringVar(&cfg.workerImage, "ateom-image", "", "alias for --worker-image")
cmd.Flags().StringVar(&cfg.snapshotsBucket, "snapshots-bucket", "", "object-storage bucket (with optional prefix) for actor snapshots, e.g. gs://bucket/prefix/")
cmd.Flags().StringVar(&cfg.apiImage, "api-image", "", "digest-pinned ate-env-api image for the API service")
cmd.Flags().Int32Var(&cfg.apiReplicas, "api-replicas", 1, "number of API service replicas")
cmd.Flags().Int32Var(&cfg.apiPort, "api-port", 7777, "port the ate-env-api service listens on")
cmd.Flags().StringVar(&cfg.workerPool, "workerpool", "", "WorkerPool name (defaults to <template>-workerpool)")
cmd.Flags().Int32Var(&cfg.replicas, "replicas", 5, "number of pre-warmed worker pods")
cmd.Flags().StringSliceVar(&cfg.guestCommand, "guest-command", []string{"/ko-app/ate-env-guest"}, "guest container entrypoint")
cmd.Flags().BoolVar(&cfg.templateOnly, "template-only", false, "generate only the Substrate ActorTemplate manifest (for kubectl-ate create actor-template -f -)")
cmd.MarkFlagRequired("snapshots-bucket")
cmd.Flags().StringVar(&mCfg.namespace, "namespace", apiservice.DefaultNamespace, "Kubernetes namespace to deploy into")
cmd.Flags().StringVar(&mCfg.template, "template", apiservice.DefaultTemplate, "ActorTemplate name used for WorkerPool workload labels")
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.apiImage, "api-image", "", "digest-pinned ate-env-api image for the API service")
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")
cmd.Flags().StringVar(&mCfg.workerPool, "workerpool", "", "WorkerPool name (defaults to <template>-workerpool)")
cmd.Flags().Int32Var(&mCfg.replicas, "replicas", 5, "number of pre-warmed worker pods")

cmd.AddCommand(newManifestTemplateCommand())

return cmd
}

// resolveImages verifies that all deployment images are set, either baked
// in at release time or passed as flags.
func (c *manifestConfig) resolveImages() error {
if c.templateOnly {
if c.guestImage == "" {
return errors.New("--guest-image is required; use the digest-pinned ate-env-guest image")
}
return nil
}
if c.guestImage != "" && c.workerImage != "" && c.apiImage != "" {
return nil
func newManifestTemplateCommand() *cobra.Command {
tCfg := templateConfig{}

cmd := &cobra.Command{
Use: "template",
Short: "Generate Substrate ActorTemplate manifest",
Long: `Generate Substrate ActorTemplate manifest to register with kubectl-ate.
It prints YAML to stdout without touching the cluster.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if tCfg.template == "" {
tCfg.template = apiservice.DefaultTemplate
}
tCfg.poolLabels = map[string]string{"workload": tCfg.template}
if err := tCfg.resolveImages(); err != nil {
return err
}
return writeActorTemplate(cmd.OutOrStdout(), buildActorTemplate(tCfg))
},
}
return errors.New(`--guest-image, --api-image, and --worker-image (or --ateom-image) are required; use the
digest-pinned images published by the latest release (the README
quickstart records them), or build and push your own.`)

cmd.Flags().StringVar(&tCfg.template, "template", apiservice.DefaultTemplate, "ActorTemplate name")
cmd.Flags().StringVar(&tCfg.atespace, "atespace", apiservice.DefaultAtespace, "Substrate atespace for the ActorTemplate")
cmd.Flags().StringVar(&tCfg.guestImage, "guest-image", "", "digest-pinned ate-env-guest image (repo@sha256:...)")
cmd.Flags().StringSliceVar(&tCfg.guestCommand, "guest-command", []string{"/ko-app/ate-env-guest"}, "guest container entrypoint")
cmd.Flags().StringVar(&tCfg.snapshotsBucket, "snapshots-bucket", "", "object-storage bucket (with optional prefix) for actor snapshots, e.g. gs://bucket/prefix/")

return cmd
}

// buildManifests returns the Kubernetes objects that make up a deployment,
Expand Down Expand Up @@ -231,10 +261,10 @@ func buildWorkerPool(cfg manifestConfig) *atev1alpha1.WorkerPool {
}
}

func buildActorTemplate(cfg manifestConfig) *ateapipb.ActorTemplate {
func buildActorTemplate(cfg templateConfig) *ateapipb.ActorTemplate {
atespace := cfg.atespace
if atespace == "" {
atespace = "default"
atespace = apiservice.DefaultAtespace
}
return &ateapipb.ActorTemplate{
Metadata: &ateapipb.ResourceMetadata{
Expand Down
Loading