From d9ace453c41accce961bf5dbcbb6a5e580aaf9fe Mon Sep 17 00:00:00 2001 From: zoezhao Date: Mon, 31 Aug 2026 09:50:43 -0700 Subject: [PATCH 1/9] ateapi: allow repointing a suspended Actor at a replacement template Make Actor.actor_template mutable so a suspended Actor can be updated to reference a different ActorTemplate, taking effect on the next resume. UpdateActor resolves a repointed template ref up front, mirroring CreateActor's check. To restore safely across a template swap, a successful resume (including the first boot) records the template (and its UID) the sprint booted with in ActorStatus.current_actor_template(_uid) when it commits RUNNING. The next resume compares that UID against the currently resolved template: when they differ, the snapshot was taken on a different template, so its guest state cannot be layered onto the new template's golden snapshot. The restore wire scope is forced to SNAPSHOT_SCOPE_DATA in that case, and the golden URI is only sent when the scope actually is DATA_ON_GOLDEN. The same scope derivation applies in both the local- and durable-snapshot restore branches. Recording at sprint start rather than at suspend/pause finalization keeps the record correct however the sprint ends, so crashed finalizations need no special casing. The status field mirrors the spec/status split the way StatefulSet's currentRevision does: spec.actor_template is the desired template, status.current_actor_template is the one the guest state is actually built on. --- benchmarking/locust/common/ateapi_pb2.py | 368 +++++++++--------- cmd/ateapi/internal/controlapi/actor.go | 9 + cmd/ateapi/internal/controlapi/actor_test.go | 48 +++ .../controlapi/functionaltest/actor_test.go | 69 +++- .../internal/controlapi/workflow_resume.go | 54 ++- .../controlapi/workflow_resume_test.go | 35 ++ .../controlapi/zz_generated.validation.go | 58 +++ .../e2e/suites/demo/updatetemplate_test.go | 231 +++++++++++ pkg/proto/ateapipb/ateapi.pb.go | 368 ++++++++++-------- pkg/proto/ateapipb/ateapi.proto | 14 + 10 files changed, 880 insertions(+), 374 deletions(-) create mode 100644 internal/e2e/suites/demo/updatetemplate_test.go diff --git a/benchmarking/locust/common/ateapi_pb2.py b/benchmarking/locust/common/ateapi_pb2.py index 418c42664c..d6f87e1cd4 100644 --- a/benchmarking/locust/common/ateapi_pb2.py +++ b/benchmarking/locust/common/ateapi_pb2.py @@ -40,7 +40,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x61teapi.proto\x12\x06\x61teapi\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x86\x01\n\x11LocalSnapshotInfo\x12\x15\n\rsnapshot_name\x18\x01 \x01(\t\x12%\n\x1dnode_vms_with_local_snapshots\x18\x02 \x03(\t\x12\x33\n\rcontent_scope\x18\x03 \x01(\x0e\x32\x1c.ateapi.SnapshotContentScope\"w\n\x08Selector\x12\x37\n\x0cmatch_labels\x18\x01 \x03(\x0b\x32!.ateapi.Selector.MatchLabelsEntry\x1a\x32\n\x10MatchLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb2\x01\n\x10ResourceMetadata\x12\x10\n\x08\x61tespace\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0b\n\x03uid\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\x03\x12/\n\x0b\x63reate_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0bupdate_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdc\x02\n\x0e\x45xternalVolume\x12\x13\n\x0bvolume_name\x18\x01 \x01(\t\x12\x19\n\x11storage_volume_id\x18\x02 \x01(\t\x12\x13\n\x0bvolume_type\x18\x03 \x01(\t\x12-\n\x06status\x18\x04 \x01(\x0e\x32\x1d.ateapi.ExternalVolume.Status\x12\x41\n\x0evolume_context\x18\x05 \x03(\x0b\x32).ateapi.ExternalVolume.VolumeContextEntry\x1a\x34\n\x12VolumeContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"]\n\x06Status\x12\x16\n\x12STATUS_UNSPECIFIED\x10\x00\x12\x12\n\x0eSTATUS_PENDING\x10\x01\x12\x12\n\x0eSTATUS_CREATED\x10\x02\x12\x13\n\x0fSTATUS_DELETING\x10\x03\"\xde\x01\n\x05\x41\x63tor\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\x12)\n\x0e\x61\x63tor_template\x18\x04 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12)\n\x0fworker_selector\x18\x05 \x01(\x0b\x32\x10.ateapi.Selector\x12.\n\x13source_snapshot_tag\x18\x06 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12#\n\x06status\x18\x07 \x01(\x0b\x32\x13.ateapi.ActorStatus\"]\n\x0c\x45gressPolicy\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\x12!\n\x05rules\x18\x02 \x03(\x0b\x32\x12.ateapi.EgressRule\"\x82\x01\n\nEgressRule\x12\'\n\thostnames\x18\x01 \x01(\x0b\x32\x14.ateapi.HostnameRule\x12&\n\tip_blocks\x18\x02 \x01(\x0b\x32\x13.ateapi.IPBlockRule\x12#\n\x03\x61ll\x18\x03 \x01(\x0b\x32\x16.google.protobuf.Empty\"L\n\x0cHostnameRule\x12\x10\n\x08patterns\x18\x01 \x03(\t\x12*\n\x07\x65\x66\x66\x65\x63ts\x18\x02 \x01(\x0b\x32\x19.ateapi.EgressRuleEffects\"\x1c\n\x0bIPBlockRule\x12\r\n\x05\x63idrs\x18\x01 \x03(\t\"U\n\x11\x45gressRuleEffects\x12@\n\x15inject_static_headers\x18\x01 \x03(\x0b\x32!.ateapi.CredentialHeaderInjection\"S\n\x19\x43redentialHeaderInjection\x12\x0e\n\x06header\x18\x01 \x01(\t\x12\x0e\n\x06prefix\x18\x02 \x01(\t\x12\x16\n\x0e\x63redential_uri\x18\x03 \x01(\t\"\xb3\x03\n\x0b\x41\x63torStatus\x12!\n\x05state\x18\x01 \x01(\x0e\x32\x12.ateapi.ActorState\x12\x33\n\x11worker_assignment\x18\x02 \x01(\x0b\x32\x18.ateapi.WorkerAssignment\x12!\n\x19in_progress_snapshot_name\x18\x03 \x01(\t\x12*\n\x0flatest_snapshot\x18\x04 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x36\n\x13local_snapshot_info\x18\x05 \x01(\x0b\x32\x19.ateapi.LocalSnapshotInfo\x12\x31\n)in_progress_snapshot_source_actor_version\x18\x06 \x01(\x03\x12-\n\ractor_volumes\x18\x07 \x03(\x0b\x32\x16.ateapi.ExternalVolume\x12\'\n\x1fin_progress_local_snapshot_name\x18\x08 \x01(\t\x12:\n\x0fsource_snapshot\x18\t \x01(\x0b\x32!.ateapi.ActorSourceSnapshotStatus\"V\n\x19\x41\x63torSourceSnapshotStatus\x12#\n\x08snapshot\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x14\n\x0csnapshot_uid\x18\x02 \x01(\t\"\xa7\x01\n\x10WorkerAssignment\x12!\n\x06worker\x18\x06 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x18\n\x10worker_namespace\x18\x01 \x01(\t\x12\x13\n\x0bworker_pool\x18\x02 \x01(\t\x12\x12\n\nworker_pod\x18\x03 \x01(\t\x12\x16\n\x0eworker_pod_uid\x18\x04 \x01(\t\x12\x15\n\rworker_pod_ip\x18\x05 \x01(\t\"h\n\rActorSnapshot\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\x12+\n\x06status\x18\x02 \x01(\x0b\x32\x1b.ateapi.ActorSnapshotStatus\"\x88\x02\n\x13\x41\x63torSnapshotStatus\x12\'\n\x0csource_actor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x18\n\x10source_actor_uid\x18\x02 \x01(\t\x12\x1c\n\x14source_actor_version\x18\x03 \x01(\x03\x12\x1a\n\x12\x61\x63tor_template_uid\x18\x06 \x01(\t\x12\x33\n\rcontent_scope\x18\x07 \x01(\x0e\x32\x1c.ateapi.SnapshotContentScope\x12\x14\n\x0csnapshot_uri\x18\x08 \x01(\t\x12)\n\x0e\x61\x63tor_template\x18\t \x01(\x0b\x32\x11.ateapi.ObjectRef\"\x91\x01\n\x10\x41\x63torSnapshotTag\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\x12#\n\x08snapshot\x18\x02 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12,\n\x05scope\x18\x03 \x01(\x0e\x32\x1d.ateapi.ActorSnapshotTagScope\"6\n\x08\x41tespace\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\"+\n\tObjectRef\x12\x10\n\x08\x61tespace\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"\xe3\x02\n\rActorTemplate\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\x12)\n\x0fworker_selector\x18\x02 \x01(\x0b\x32\x10.ateapi.Selector\x12%\n\ncontainers\x18\x03 \x03(\x0b\x32\x11.ateapi.Container\x12\x1f\n\x07volumes\x18\x04 \x03(\x0b\x32\x0e.ateapi.Volume\x12\x31\n\x10snapshots_config\x18\x05 \x01(\x0b\x32\x17.ateapi.SnapshotsConfig\x12-\n\x0esandbox_config\x18\x06 \x01(\x0b\x32\x15.ateapi.SandboxConfig\x12$\n\tresources\x18\x07 \x01(\x0b\x32\x11.ateapi.Resources\x12+\n\x06status\x18\x08 \x01(\x0b\x32\x1b.ateapi.ActorTemplateStatus\"+\n\tResources\x12\x1e\n\x06limits\x18\x01 \x03(\x0b\x32\x0e.ateapi.Limits\"(\n\x06Limits\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08quantity\x18\x02 \x01(\t\"\x96\x01\n\x14GoldenSnapshotStatus\x12*\n\x0fgolden_snapshot\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12;\n\x17take_golden_snapshot_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rerror_message\x18\x03 \x01(\t\"S\n\x13\x41\x63torTemplateStatus\x12<\n\x16golden_snapshot_status\x18\x01 \x01(\x0b\x32\x1c.ateapi.GoldenSnapshotStatus\"Q\n\rSandboxConfig\x12+\n\rsandbox_class\x18\x01 \x01(\x0e\x32\x14.ateapi.SandboxClass\x12\x13\n\x0b\x63onfig_name\x18\x02 \x01(\t\"\xb7\x01\n\x0fSnapshotsConfig\x12.\n\x08on_pause\x18\x01 \x01(\x0e\x32\x1c.ateapi.SnapshotContentScope\x12/\n\ton_commit\x18\x02 \x01(\x0e\x32\x1c.ateapi.SnapshotContentScope\x12)\n\ton_resume\x18\x03 \x01(\x0b\x32\x16.ateapi.OnResumeConfig\x12\x18\n\x10storage_location\x18\x04 \x01(\t\"9\n\x0eOnResumeConfig\x12\'\n\tfrom_data\x18\x01 \x01(\x0e\x32\x14.ateapi.ResumeSource\"\x92\x02\n\tContainer\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05image\x18\x02 \x01(\t\x12\x0f\n\x07\x63ommand\x18\x03 \x03(\t\x12\x0c\n\x04\x61rgs\x18\x04 \x03(\t\x12\x1b\n\x03\x65nv\x18\x05 \x03(\x0b\x32\x0e.ateapi.EnvVar\x12\'\n\x06readyz\x18\x06 \x01(\x0b\x32\x17.ateapi.ContainerReadyz\x12*\n\rvolume_mounts\x18\x07 \x03(\x0b\x32\x13.ateapi.VolumeMount\x12\x31\n\x10security_context\x18\x08 \x01(\x0b\x32\x17.ateapi.SecurityContext\x12$\n\tresources\x18\t \x01(\x0b\x32\x11.ateapi.Resources\"=\n\x0fSecurityContext\x12*\n\x0c\x63\x61pabilities\x18\x01 \x01(\x0b\x32\x14.ateapi.Capabilities\")\n\x0c\x43\x61pabilities\x12\x0b\n\x03\x61\x64\x64\x18\x01 \x03(\t\x12\x0c\n\x04\x64rop\x18\x02 \x03(\t\"%\n\x06\x45nvVar\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"S\n\x0f\x43ontainerReadyz\x12\'\n\x08http_get\x18\x01 \x01(\x0b\x32\x15.ateapi.HTTPGetAction\x12\x17\n\x0ftimeout_seconds\x18\x02 \x01(\x05\"+\n\rHTTPGetAction\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04port\x18\x02 \x01(\x05\"\xec\x01\n\x06Volume\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x33\n\x0b\x64urable_dir\x18\x02 \x01(\x0b\x32\x1e.ateapi.DurableDirVolumeSource\x12@\n\x18\x65xternal_volume_template\x18\x03 \x01(\x0b\x32\x1e.ateapi.ExternalVolumeTemplate\x12\x33\n\x0bsystem_info\x18\x05 \x01(\x0b\x32\x1e.ateapi.SystemInfoVolumeSource\x12(\n\x05image\x18\x06 \x01(\x0b\x32\x19.ateapi.ImageVolumeSource\"&\n\x11ImageVolumeSource\x12\x11\n\treference\x18\x01 \x01(\t\"\x18\n\x16\x44urableDirVolumeSource\"F\n\x16\x45xternalVolumeTemplate\x12\x10\n\x08\x63\x61pacity\x18\x01 \x01(\t\x12\x1a\n\x12storage_class_name\x18\x02 \x01(\t\"L\n\x16SystemInfoVolumeSource\x12\x32\n\x0c\x64\x61ta_sources\x18\x01 \x03(\x0b\x32\x1c.ateapi.SystemInfoDataSource\"\x84\x01\n\x14SystemInfoDataSource\x12\x37\n\x0e\x61\x63tor_metadata\x18\x01 \x01(\x0b\x32\x1f.ateapi.ActorMetadataDataSource\x12\x33\n\x0ctrust_bundle\x18\x02 \x01(\x0b\x32\x1d.ateapi.TrustBundleDataSource\"C\n\x17\x41\x63torMetadataDataSource\x12(\n\x05items\x18\x01 \x03(\x0b\x32\x19.ateapi.ActorMetadataItem\"L\n\x11\x41\x63torMetadataItem\x12)\n\x05\x66ield\x18\x01 \x01(\x0e\x32\x1a.ateapi.ActorMetadataField\x12\x0c\n\x04path\x18\x02 \x01(\t\"3\n\x15TrustBundleDataSource\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\"/\n\x0bVolumeMount\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\nmount_path\x18\x02 \x01(\t\";\n\x15\x43reateAtespaceRequest\x12\"\n\x08\x61tespace\x18\x01 \x01(\x0b\x32\x10.ateapi.Atespace\"9\n\x12GetAtespaceRequest\x12#\n\x08\x61tespace\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"=\n\x14ListAtespacesRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\"U\n\x15ListAtespacesResponse\x12#\n\tatespaces\x18\x01 \x03(\x0b\x32\x10.ateapi.Atespace\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"<\n\x15\x44\x65leteAtespaceRequest\x12#\n\x08\x61tespace\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"K\n\x1a\x43reateActorTemplateRequest\x12-\n\x0e\x61\x63tor_template\x18\x01 \x01(\x0b\x32\x15.ateapi.ActorTemplate\"D\n\x17GetActorTemplateRequest\x12)\n\x0e\x61\x63tor_template\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"T\n\x19ListActorTemplatesRequest\x12\x10\n\x08\x61tespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t\"e\n\x1aListActorTemplatesResponse\x12.\n\x0f\x61\x63tor_templates\x18\x01 \x03(\x0b\x32\x15.ateapi.ActorTemplate\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"G\n\x1a\x44\x65leteActorTemplateRequest\x12)\n\x0e\x61\x63tor_template\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"3\n\x0fGetActorRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"2\n\x12\x43reateActorRequest\x12\x1c\n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\r.ateapi.Actor\"2\n\x12UpdateActorRequest\x12\x1c\n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\r.ateapi.Actor\"7\n\x13SuspendActorRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"4\n\x14SuspendActorResponse\x12\x1c\n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\r.ateapi.Actor\"5\n\x11PauseActorRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"2\n\x12PauseActorResponse\x12\x1c\n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\r.ateapi.Actor\"D\n\x12ResumeActorRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x0c\n\x04\x62oot\x18\x02 \x01(\x08\"D\n\x13ResumeActorResponse\x12\x1c\n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\r.ateapi.Actor\x12\x0f\n\x07resumed\x18\x02 \x01(\x08\"I\n\x12\x44\x65leteActorRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x11\n\tany_state\x18\x02 \x01(\x08\"?\n\x1bGetActorEgressPolicyRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"o\n\x1e\x43reateActorEgressPolicyRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12+\n\regress_policy\x18\x02 \x01(\x0b\x32\x14.ateapi.EgressPolicy\"o\n\x1eUpdateActorEgressPolicyRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12+\n\regress_policy\x18\x02 \x01(\x0b\x32\x14.ateapi.EgressPolicy\"B\n\x1e\x44\x65leteActorEgressPolicyRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"D\n\x17GetActorSnapshotRequest\x12)\n\x0e\x61\x63tor_snapshot\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"K\n\x1aGetActorSnapshotTagRequest\x12-\n\x12\x61\x63tor_snapshot_tag\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"T\n\x19ListActorSnapshotsRequest\x12\x10\n\x08\x61tespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t\"e\n\x1aListActorSnapshotsResponse\x12.\n\x0f\x61\x63tor_snapshots\x18\x01 \x03(\x0b\x32\x15.ateapi.ActorSnapshot\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"U\n\x1d\x43reateActorSnapshotTagRequest\x12\x34\n\x12\x61\x63tor_snapshot_tag\x18\x01 \x01(\x0b\x32\x18.ateapi.ActorSnapshotTag\"U\n\x1dUpdateActorSnapshotTagRequest\x12\x34\n\x12\x61\x63tor_snapshot_tag\x18\x01 \x01(\x0b\x32\x18.ateapi.ActorSnapshotTag\"N\n\x1d\x44\x65leteActorSnapshotTagRequest\x12-\n\x12\x61\x63tor_snapshot_tag\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"-\n\rDeleteOptions\x12\x0f\n\x07version\x18\x01 \x01(\x03\x12\x0b\n\x03uid\x18\x02 \x01(\t\";\n\x12ListWorkersRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\"O\n\x13ListWorkersResponse\x12\x1f\n\x07workers\x18\x01 \x03(\x0b\x32\x0e.ateapi.Worker\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"5\n\x10GetWorkerRequest\x12!\n\x06worker\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"5\n\x13\x43reateWorkerRequest\x12\x1e\n\x06worker\x18\x01 \x01(\x0b\x32\x0e.ateapi.Worker\"5\n\x13UpdateWorkerRequest\x12\x1e\n\x06worker\x18\x01 \x01(\x0b\x32\x0e.ateapi.Worker\"`\n\x13\x44\x65leteWorkerRequest\x12!\n\x06worker\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12&\n\x07options\x18\x02 \x01(\x0b\x32\x15.ateapi.DeleteOptions\"7\n\x12\x44rainWorkerRequest\x12!\n\x06worker\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"L\n\x11ListActorsRequest\x12\x10\n\x08\x61tespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t\"L\n\x12ListActorsResponse\x12\x1d\n\x06\x61\x63tors\x18\x01 \x03(\x0b\x32\r.ateapi.Actor\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"\xf0\x02\n\x06Worker\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\x12\x18\n\x10worker_namespace\x18\x02 \x01(\t\x12\x13\n\x0bworker_pool\x18\x03 \x01(\t\x12\x12\n\nworker_pod\x18\x04 \x01(\t\x12\x16\n\x0eworker_pod_uid\x18\x05 \x01(\t\x12\x11\n\tnode_name\x18\x06 \x01(\t\x12\n\n\x02ip\x18\x07 \x01(\t\x12\x15\n\rsandbox_class\x18\x08 \x01(\t\x12*\n\x06labels\x18\t \x03(\x0b\x32\x1a.ateapi.Worker.LabelsEntry\x12(\n\x08\x63\x61pacity\x18\n \x01(\x0b\x32\x16.ateapi.WorkerCapacity\x12$\n\x06status\x18\x0b \x01(\x0b\x32\x14.ateapi.WorkerStatus\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"_\n\x0cWorkerStatus\x12\"\n\x05state\x18\x01 \x01(\x0e\x32\x13.ateapi.WorkerState\x12+\n\nassignment\x18\x02 \x01(\x0b\x32\x17.ateapi.ActorAssignment\"9\n\x0eWorkerCapacity\x12\x11\n\tcpu_milli\x18\x01 \x01(\x03\x12\x14\n\x0cmemory_bytes\x18\x02 \x01(\x03\"u\n\x0f\x41\x63torAssignment\x12 \n\x05\x61\x63tor\x18\x02 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x11\n\tactor_uid\x18\x03 \x01(\t\x12-\n\x12\x61\x63tor_template_ref\x18\x04 \x01(\x0b\x32\x11.ateapi.ObjectRef\"[\n\x0eMintJWTRequest\x12\x10\n\x08\x61udience\x18\x01 \x03(\t\x12\x10\n\x08\x61tespace\x18\x02 \x01(\t\x12\x12\n\nactor_name\x18\x03 \x01(\t\x12\x11\n\tactor_uid\x18\x04 \x01(\t\"$\n\x0fMintJWTResponse\x12\x11\n\tactor_jwt\x18\x01 \x01(\t\"\xa7\x01\n\x0fMintCertRequest\x12!\n\x06worker\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12#\n\x1b\x63\x65rtificate_signing_request\x18\x02 \x01(\x0c\x12\x1a\n\x12\x65xpected_actor_uid\x18\x03 \x01(\t\x12\x30\n\x07purpose\x18\x04 \x01(\x0e\x32\x1f.ateapi.ActorCertificatePurpose\".\n\x10MintCertResponse\x12\x1a\n\x12\x61\x63tor_certificates\x18\x01 \x03(\x0c*\x80\x01\n\x14SnapshotContentScope\x12&\n\"SNAPSHOT_CONTENT_SCOPE_UNSPECIFIED\x10\x00\x12\x1f\n\x1bSNAPSHOT_CONTENT_SCOPE_FULL\x10\x01\x12\x1f\n\x1bSNAPSHOT_CONTENT_SCOPE_DATA\x10\x02*\x90\x01\n\x15\x41\x63torSnapshotTagScope\x12(\n$ACTOR_SNAPSHOT_TAG_SCOPE_UNSPECIFIED\x10\x00\x12%\n!ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE\x10\x01\x12&\n\"ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED\x10\x02*\xf7\x01\n\nActorState\x12\x1b\n\x17\x41\x43TOR_STATE_UNSPECIFIED\x10\x00\x12\x18\n\x14\x41\x43TOR_STATE_RESUMING\x10\x01\x12\x17\n\x13\x41\x43TOR_STATE_RUNNING\x10\x02\x12\x1a\n\x16\x41\x43TOR_STATE_SUSPENDING\x10\x03\x12\x19\n\x15\x41\x43TOR_STATE_SUSPENDED\x10\x04\x12\x17\n\x13\x41\x43TOR_STATE_PAUSING\x10\x05\x12\x16\n\x12\x41\x43TOR_STATE_PAUSED\x10\x06\x12\x17\n\x13\x41\x43TOR_STATE_CRASHED\x10\x07\x12\x18\n\x14\x41\x43TOR_STATE_DELETING\x10\x08*b\n\x0cSandboxClass\x12\x1d\n\x19SANDBOX_CLASS_UNSPECIFIED\x10\x00\x12\x18\n\x14SANDBOX_CLASS_GVISOR\x10\x01\x12\x19\n\x15SANDBOX_CLASS_MICROVM\x10\x02*d\n\x0cResumeSource\x12\x1d\n\x19RESUME_SOURCE_UNSPECIFIED\x10\x00\x12\x1b\n\x17RESUME_SOURCE_COLD_BOOT\x10\x01\x12\x18\n\x14RESUME_SOURCE_GOLDEN\x10\x02*\x9a\x01\n\x12\x41\x63torMetadataField\x12$\n ACTOR_METADATA_FIELD_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x41\x43TOR_METADATA_FIELD_NAME\x10\x01\x12!\n\x1d\x41\x43TOR_METADATA_FIELD_ATESPACE\x10\x02\x12\x1c\n\x18\x41\x43TOR_METADATA_FIELD_UID\x10\x03*_\n\x0bWorkerState\x12\x1c\n\x18WORKER_STATE_UNSPECIFIED\x10\x00\x12\x17\n\x13WORKER_STATE_ACTIVE\x10\x01\x12\x19\n\x15WORKER_STATE_DRAINING\x10\x02*k\n\x17\x41\x63torCertificatePurpose\x12)\n%ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED\x10\x00\x12%\n!ACTOR_CERTIFICATE_PURPOSE_ATUNNEL\x10\x01\x32\x84\x13\n\x07\x43ontrol\x12\x34\n\x08GetActor\x12\x17.ateapi.GetActorRequest\x1a\r.ateapi.Actor\"\x00\x12:\n\x0b\x43reateActor\x12\x1a.ateapi.CreateActorRequest\x1a\r.ateapi.Actor\"\x00\x12:\n\x0bUpdateActor\x12\x1a.ateapi.UpdateActorRequest\x1a\r.ateapi.Actor\"\x00\x12K\n\x0cSuspendActor\x12\x1b.ateapi.SuspendActorRequest\x1a\x1c.ateapi.SuspendActorResponse\"\x00\x12\x45\n\nPauseActor\x12\x19.ateapi.PauseActorRequest\x1a\x1a.ateapi.PauseActorResponse\"\x00\x12H\n\x0bResumeActor\x12\x1a.ateapi.ResumeActorRequest\x1a\x1b.ateapi.ResumeActorResponse\"\x00\x12:\n\x0b\x44\x65leteActor\x12\x1a.ateapi.DeleteActorRequest\x1a\r.ateapi.Actor\"\x00\x12S\n\x14GetActorEgressPolicy\x12#.ateapi.GetActorEgressPolicyRequest\x1a\x14.ateapi.EgressPolicy\"\x00\x12Y\n\x17\x43reateActorEgressPolicy\x12&.ateapi.CreateActorEgressPolicyRequest\x1a\x14.ateapi.EgressPolicy\"\x00\x12Y\n\x17UpdateActorEgressPolicy\x12&.ateapi.UpdateActorEgressPolicyRequest\x1a\x14.ateapi.EgressPolicy\"\x00\x12Y\n\x17\x44\x65leteActorEgressPolicy\x12&.ateapi.DeleteActorEgressPolicyRequest\x1a\x14.ateapi.EgressPolicy\"\x00\x12L\n\x10GetActorSnapshot\x12\x1f.ateapi.GetActorSnapshotRequest\x1a\x15.ateapi.ActorSnapshot\"\x00\x12U\n\x13GetActorSnapshotTag\x12\".ateapi.GetActorSnapshotTagRequest\x1a\x18.ateapi.ActorSnapshotTag\"\x00\x12]\n\x12ListActorSnapshots\x12!.ateapi.ListActorSnapshotsRequest\x1a\".ateapi.ListActorSnapshotsResponse\"\x00\x12[\n\x16\x43reateActorSnapshotTag\x12%.ateapi.CreateActorSnapshotTagRequest\x1a\x18.ateapi.ActorSnapshotTag\"\x00\x12[\n\x16UpdateActorSnapshotTag\x12%.ateapi.UpdateActorSnapshotTagRequest\x1a\x18.ateapi.ActorSnapshotTag\"\x00\x12[\n\x16\x44\x65leteActorSnapshotTag\x12%.ateapi.DeleteActorSnapshotTagRequest\x1a\x18.ateapi.ActorSnapshotTag\"\x00\x12H\n\x0bListWorkers\x12\x1a.ateapi.ListWorkersRequest\x1a\x1b.ateapi.ListWorkersResponse\"\x00\x12\x37\n\tGetWorker\x12\x18.ateapi.GetWorkerRequest\x1a\x0e.ateapi.Worker\"\x00\x12=\n\x0c\x43reateWorker\x12\x1b.ateapi.CreateWorkerRequest\x1a\x0e.ateapi.Worker\"\x00\x12=\n\x0cUpdateWorker\x12\x1b.ateapi.UpdateWorkerRequest\x1a\x0e.ateapi.Worker\"\x00\x12=\n\x0c\x44\x65leteWorker\x12\x1b.ateapi.DeleteWorkerRequest\x1a\x0e.ateapi.Worker\"\x00\x12;\n\x0b\x44rainWorker\x12\x1a.ateapi.DrainWorkerRequest\x1a\x0e.ateapi.Worker\"\x00\x12\x45\n\nListActors\x12\x19.ateapi.ListActorsRequest\x1a\x1a.ateapi.ListActorsResponse\"\x00\x12\x43\n\x0e\x43reateAtespace\x12\x1d.ateapi.CreateAtespaceRequest\x1a\x10.ateapi.Atespace\"\x00\x12=\n\x0bGetAtespace\x12\x1a.ateapi.GetAtespaceRequest\x1a\x10.ateapi.Atespace\"\x00\x12N\n\rListAtespaces\x12\x1c.ateapi.ListAtespacesRequest\x1a\x1d.ateapi.ListAtespacesResponse\"\x00\x12\x43\n\x0e\x44\x65leteAtespace\x12\x1d.ateapi.DeleteAtespaceRequest\x1a\x10.ateapi.Atespace\"\x00\x12R\n\x13\x43reateActorTemplate\x12\".ateapi.CreateActorTemplateRequest\x1a\x15.ateapi.ActorTemplate\"\x00\x12L\n\x10GetActorTemplate\x12\x1f.ateapi.GetActorTemplateRequest\x1a\x15.ateapi.ActorTemplate\"\x00\x12]\n\x12ListActorTemplates\x12!.ateapi.ListActorTemplatesRequest\x1a\".ateapi.ListActorTemplatesResponse\"\x00\x12R\n\x13\x44\x65leteActorTemplate\x12\".ateapi.DeleteActorTemplateRequest\x1a\x15.ateapi.ActorTemplate\"\x00\x32\x8a\x01\n\rActorIdentity\x12:\n\x07MintJWT\x12\x16.ateapi.MintJWTRequest\x1a\x17.ateapi.MintJWTResponse\x12=\n\x08MintCert\x12\x17.ateapi.MintCertRequest\x1a\x18.ateapi.MintCertResponseB9Z7github.com/agent-substrate/substrate/pkg/proto/ateapipbb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x61teapi.proto\x12\x06\x61teapi\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x86\x01\n\x11LocalSnapshotInfo\x12\x15\n\rsnapshot_name\x18\x01 \x01(\t\x12%\n\x1dnode_vms_with_local_snapshots\x18\x02 \x03(\t\x12\x33\n\rcontent_scope\x18\x03 \x01(\x0e\x32\x1c.ateapi.SnapshotContentScope\"w\n\x08Selector\x12\x37\n\x0cmatch_labels\x18\x01 \x03(\x0b\x32!.ateapi.Selector.MatchLabelsEntry\x1a\x32\n\x10MatchLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb2\x01\n\x10ResourceMetadata\x12\x10\n\x08\x61tespace\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0b\n\x03uid\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\x03\x12/\n\x0b\x63reate_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0bupdate_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdc\x02\n\x0e\x45xternalVolume\x12\x13\n\x0bvolume_name\x18\x01 \x01(\t\x12\x19\n\x11storage_volume_id\x18\x02 \x01(\t\x12\x13\n\x0bvolume_type\x18\x03 \x01(\t\x12-\n\x06status\x18\x04 \x01(\x0e\x32\x1d.ateapi.ExternalVolume.Status\x12\x41\n\x0evolume_context\x18\x05 \x03(\x0b\x32).ateapi.ExternalVolume.VolumeContextEntry\x1a\x34\n\x12VolumeContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"]\n\x06Status\x12\x16\n\x12STATUS_UNSPECIFIED\x10\x00\x12\x12\n\x0eSTATUS_PENDING\x10\x01\x12\x12\n\x0eSTATUS_CREATED\x10\x02\x12\x13\n\x0fSTATUS_DELETING\x10\x03\"\xde\x01\n\x05\x41\x63tor\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\x12)\n\x0e\x61\x63tor_template\x18\x04 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12)\n\x0fworker_selector\x18\x05 \x01(\x0b\x32\x10.ateapi.Selector\x12.\n\x13source_snapshot_tag\x18\x06 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12#\n\x06status\x18\x07 \x01(\x0b\x32\x13.ateapi.ActorStatus\"]\n\x0c\x45gressPolicy\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\x12!\n\x05rules\x18\x02 \x03(\x0b\x32\x12.ateapi.EgressRule\"\x82\x01\n\nEgressRule\x12\'\n\thostnames\x18\x01 \x01(\x0b\x32\x14.ateapi.HostnameRule\x12&\n\tip_blocks\x18\x02 \x01(\x0b\x32\x13.ateapi.IPBlockRule\x12#\n\x03\x61ll\x18\x03 \x01(\x0b\x32\x16.google.protobuf.Empty\"L\n\x0cHostnameRule\x12\x10\n\x08patterns\x18\x01 \x03(\t\x12*\n\x07\x65\x66\x66\x65\x63ts\x18\x02 \x01(\x0b\x32\x19.ateapi.EgressRuleEffects\"\x1c\n\x0bIPBlockRule\x12\r\n\x05\x63idrs\x18\x01 \x03(\t\"U\n\x11\x45gressRuleEffects\x12@\n\x15inject_static_headers\x18\x01 \x03(\x0b\x32!.ateapi.CredentialHeaderInjection\"S\n\x19\x43redentialHeaderInjection\x12\x0e\n\x06header\x18\x01 \x01(\t\x12\x0e\n\x06prefix\x18\x02 \x01(\t\x12\x16\n\x0e\x63redential_uri\x18\x03 \x01(\t\"\x8a\x04\n\x0b\x41\x63torStatus\x12!\n\x05state\x18\x01 \x01(\x0e\x32\x12.ateapi.ActorState\x12\x33\n\x11worker_assignment\x18\x02 \x01(\x0b\x32\x18.ateapi.WorkerAssignment\x12!\n\x19in_progress_snapshot_name\x18\x03 \x01(\t\x12*\n\x0flatest_snapshot\x18\x04 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x36\n\x13local_snapshot_info\x18\x05 \x01(\x0b\x32\x19.ateapi.LocalSnapshotInfo\x12\x31\n)in_progress_snapshot_source_actor_version\x18\x06 \x01(\x03\x12-\n\ractor_volumes\x18\x07 \x03(\x0b\x32\x16.ateapi.ExternalVolume\x12\'\n\x1fin_progress_local_snapshot_name\x18\x08 \x01(\t\x12:\n\x0fsource_snapshot\x18\t \x01(\x0b\x32!.ateapi.ActorSourceSnapshotStatus\x12\x31\n\x16\x63urrent_actor_template\x18\n \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\"\n\x1a\x63urrent_actor_template_uid\x18\x0b \x01(\t\"V\n\x19\x41\x63torSourceSnapshotStatus\x12#\n\x08snapshot\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x14\n\x0csnapshot_uid\x18\x02 \x01(\t\"\xa7\x01\n\x10WorkerAssignment\x12!\n\x06worker\x18\x06 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x18\n\x10worker_namespace\x18\x01 \x01(\t\x12\x13\n\x0bworker_pool\x18\x02 \x01(\t\x12\x12\n\nworker_pod\x18\x03 \x01(\t\x12\x16\n\x0eworker_pod_uid\x18\x04 \x01(\t\x12\x15\n\rworker_pod_ip\x18\x05 \x01(\t\"h\n\rActorSnapshot\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\x12+\n\x06status\x18\x02 \x01(\x0b\x32\x1b.ateapi.ActorSnapshotStatus\"\x88\x02\n\x13\x41\x63torSnapshotStatus\x12\'\n\x0csource_actor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x18\n\x10source_actor_uid\x18\x02 \x01(\t\x12\x1c\n\x14source_actor_version\x18\x03 \x01(\x03\x12\x1a\n\x12\x61\x63tor_template_uid\x18\x06 \x01(\t\x12\x33\n\rcontent_scope\x18\x07 \x01(\x0e\x32\x1c.ateapi.SnapshotContentScope\x12\x14\n\x0csnapshot_uri\x18\x08 \x01(\t\x12)\n\x0e\x61\x63tor_template\x18\t \x01(\x0b\x32\x11.ateapi.ObjectRef\"\x91\x01\n\x10\x41\x63torSnapshotTag\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\x12#\n\x08snapshot\x18\x02 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12,\n\x05scope\x18\x03 \x01(\x0e\x32\x1d.ateapi.ActorSnapshotTagScope\"6\n\x08\x41tespace\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\"+\n\tObjectRef\x12\x10\n\x08\x61tespace\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"\xe3\x02\n\rActorTemplate\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\x12)\n\x0fworker_selector\x18\x02 \x01(\x0b\x32\x10.ateapi.Selector\x12%\n\ncontainers\x18\x03 \x03(\x0b\x32\x11.ateapi.Container\x12\x1f\n\x07volumes\x18\x04 \x03(\x0b\x32\x0e.ateapi.Volume\x12\x31\n\x10snapshots_config\x18\x05 \x01(\x0b\x32\x17.ateapi.SnapshotsConfig\x12-\n\x0esandbox_config\x18\x06 \x01(\x0b\x32\x15.ateapi.SandboxConfig\x12$\n\tresources\x18\x07 \x01(\x0b\x32\x11.ateapi.Resources\x12+\n\x06status\x18\x08 \x01(\x0b\x32\x1b.ateapi.ActorTemplateStatus\"+\n\tResources\x12\x1e\n\x06limits\x18\x01 \x03(\x0b\x32\x0e.ateapi.Limits\"(\n\x06Limits\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08quantity\x18\x02 \x01(\t\"\x96\x01\n\x14GoldenSnapshotStatus\x12*\n\x0fgolden_snapshot\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12;\n\x17take_golden_snapshot_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rerror_message\x18\x03 \x01(\t\"S\n\x13\x41\x63torTemplateStatus\x12<\n\x16golden_snapshot_status\x18\x01 \x01(\x0b\x32\x1c.ateapi.GoldenSnapshotStatus\"Q\n\rSandboxConfig\x12+\n\rsandbox_class\x18\x01 \x01(\x0e\x32\x14.ateapi.SandboxClass\x12\x13\n\x0b\x63onfig_name\x18\x02 \x01(\t\"\xb7\x01\n\x0fSnapshotsConfig\x12.\n\x08on_pause\x18\x01 \x01(\x0e\x32\x1c.ateapi.SnapshotContentScope\x12/\n\ton_commit\x18\x02 \x01(\x0e\x32\x1c.ateapi.SnapshotContentScope\x12)\n\ton_resume\x18\x03 \x01(\x0b\x32\x16.ateapi.OnResumeConfig\x12\x18\n\x10storage_location\x18\x04 \x01(\t\"9\n\x0eOnResumeConfig\x12\'\n\tfrom_data\x18\x01 \x01(\x0e\x32\x14.ateapi.ResumeSource\"\x92\x02\n\tContainer\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05image\x18\x02 \x01(\t\x12\x0f\n\x07\x63ommand\x18\x03 \x03(\t\x12\x0c\n\x04\x61rgs\x18\x04 \x03(\t\x12\x1b\n\x03\x65nv\x18\x05 \x03(\x0b\x32\x0e.ateapi.EnvVar\x12\'\n\x06readyz\x18\x06 \x01(\x0b\x32\x17.ateapi.ContainerReadyz\x12*\n\rvolume_mounts\x18\x07 \x03(\x0b\x32\x13.ateapi.VolumeMount\x12\x31\n\x10security_context\x18\x08 \x01(\x0b\x32\x17.ateapi.SecurityContext\x12$\n\tresources\x18\t \x01(\x0b\x32\x11.ateapi.Resources\"=\n\x0fSecurityContext\x12*\n\x0c\x63\x61pabilities\x18\x01 \x01(\x0b\x32\x14.ateapi.Capabilities\")\n\x0c\x43\x61pabilities\x12\x0b\n\x03\x61\x64\x64\x18\x01 \x03(\t\x12\x0c\n\x04\x64rop\x18\x02 \x03(\t\"%\n\x06\x45nvVar\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"S\n\x0f\x43ontainerReadyz\x12\'\n\x08http_get\x18\x01 \x01(\x0b\x32\x15.ateapi.HTTPGetAction\x12\x17\n\x0ftimeout_seconds\x18\x02 \x01(\x05\"+\n\rHTTPGetAction\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04port\x18\x02 \x01(\x05\"\xec\x01\n\x06Volume\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x33\n\x0b\x64urable_dir\x18\x02 \x01(\x0b\x32\x1e.ateapi.DurableDirVolumeSource\x12@\n\x18\x65xternal_volume_template\x18\x03 \x01(\x0b\x32\x1e.ateapi.ExternalVolumeTemplate\x12\x33\n\x0bsystem_info\x18\x05 \x01(\x0b\x32\x1e.ateapi.SystemInfoVolumeSource\x12(\n\x05image\x18\x06 \x01(\x0b\x32\x19.ateapi.ImageVolumeSource\"&\n\x11ImageVolumeSource\x12\x11\n\treference\x18\x01 \x01(\t\"\x18\n\x16\x44urableDirVolumeSource\"F\n\x16\x45xternalVolumeTemplate\x12\x10\n\x08\x63\x61pacity\x18\x01 \x01(\t\x12\x1a\n\x12storage_class_name\x18\x02 \x01(\t\"L\n\x16SystemInfoVolumeSource\x12\x32\n\x0c\x64\x61ta_sources\x18\x01 \x03(\x0b\x32\x1c.ateapi.SystemInfoDataSource\"\x84\x01\n\x14SystemInfoDataSource\x12\x37\n\x0e\x61\x63tor_metadata\x18\x01 \x01(\x0b\x32\x1f.ateapi.ActorMetadataDataSource\x12\x33\n\x0ctrust_bundle\x18\x02 \x01(\x0b\x32\x1d.ateapi.TrustBundleDataSource\"C\n\x17\x41\x63torMetadataDataSource\x12(\n\x05items\x18\x01 \x03(\x0b\x32\x19.ateapi.ActorMetadataItem\"L\n\x11\x41\x63torMetadataItem\x12)\n\x05\x66ield\x18\x01 \x01(\x0e\x32\x1a.ateapi.ActorMetadataField\x12\x0c\n\x04path\x18\x02 \x01(\t\"3\n\x15TrustBundleDataSource\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\"/\n\x0bVolumeMount\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\nmount_path\x18\x02 \x01(\t\";\n\x15\x43reateAtespaceRequest\x12\"\n\x08\x61tespace\x18\x01 \x01(\x0b\x32\x10.ateapi.Atespace\"9\n\x12GetAtespaceRequest\x12#\n\x08\x61tespace\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"=\n\x14ListAtespacesRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\"U\n\x15ListAtespacesResponse\x12#\n\tatespaces\x18\x01 \x03(\x0b\x32\x10.ateapi.Atespace\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"<\n\x15\x44\x65leteAtespaceRequest\x12#\n\x08\x61tespace\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"K\n\x1a\x43reateActorTemplateRequest\x12-\n\x0e\x61\x63tor_template\x18\x01 \x01(\x0b\x32\x15.ateapi.ActorTemplate\"D\n\x17GetActorTemplateRequest\x12)\n\x0e\x61\x63tor_template\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"T\n\x19ListActorTemplatesRequest\x12\x10\n\x08\x61tespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t\"e\n\x1aListActorTemplatesResponse\x12.\n\x0f\x61\x63tor_templates\x18\x01 \x03(\x0b\x32\x15.ateapi.ActorTemplate\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"G\n\x1a\x44\x65leteActorTemplateRequest\x12)\n\x0e\x61\x63tor_template\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"3\n\x0fGetActorRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"2\n\x12\x43reateActorRequest\x12\x1c\n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\r.ateapi.Actor\"2\n\x12UpdateActorRequest\x12\x1c\n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\r.ateapi.Actor\"7\n\x13SuspendActorRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"4\n\x14SuspendActorResponse\x12\x1c\n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\r.ateapi.Actor\"5\n\x11PauseActorRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"2\n\x12PauseActorResponse\x12\x1c\n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\r.ateapi.Actor\"D\n\x12ResumeActorRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x0c\n\x04\x62oot\x18\x02 \x01(\x08\"D\n\x13ResumeActorResponse\x12\x1c\n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\r.ateapi.Actor\x12\x0f\n\x07resumed\x18\x02 \x01(\x08\"I\n\x12\x44\x65leteActorRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x11\n\tany_state\x18\x02 \x01(\x08\"?\n\x1bGetActorEgressPolicyRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"o\n\x1e\x43reateActorEgressPolicyRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12+\n\regress_policy\x18\x02 \x01(\x0b\x32\x14.ateapi.EgressPolicy\"o\n\x1eUpdateActorEgressPolicyRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12+\n\regress_policy\x18\x02 \x01(\x0b\x32\x14.ateapi.EgressPolicy\"B\n\x1e\x44\x65leteActorEgressPolicyRequest\x12 \n\x05\x61\x63tor\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"D\n\x17GetActorSnapshotRequest\x12)\n\x0e\x61\x63tor_snapshot\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"K\n\x1aGetActorSnapshotTagRequest\x12-\n\x12\x61\x63tor_snapshot_tag\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"T\n\x19ListActorSnapshotsRequest\x12\x10\n\x08\x61tespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t\"e\n\x1aListActorSnapshotsResponse\x12.\n\x0f\x61\x63tor_snapshots\x18\x01 \x03(\x0b\x32\x15.ateapi.ActorSnapshot\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"U\n\x1d\x43reateActorSnapshotTagRequest\x12\x34\n\x12\x61\x63tor_snapshot_tag\x18\x01 \x01(\x0b\x32\x18.ateapi.ActorSnapshotTag\"U\n\x1dUpdateActorSnapshotTagRequest\x12\x34\n\x12\x61\x63tor_snapshot_tag\x18\x01 \x01(\x0b\x32\x18.ateapi.ActorSnapshotTag\"N\n\x1d\x44\x65leteActorSnapshotTagRequest\x12-\n\x12\x61\x63tor_snapshot_tag\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"-\n\rDeleteOptions\x12\x0f\n\x07version\x18\x01 \x01(\x03\x12\x0b\n\x03uid\x18\x02 \x01(\t\";\n\x12ListWorkersRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\"O\n\x13ListWorkersResponse\x12\x1f\n\x07workers\x18\x01 \x03(\x0b\x32\x0e.ateapi.Worker\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"5\n\x10GetWorkerRequest\x12!\n\x06worker\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"5\n\x13\x43reateWorkerRequest\x12\x1e\n\x06worker\x18\x01 \x01(\x0b\x32\x0e.ateapi.Worker\"5\n\x13UpdateWorkerRequest\x12\x1e\n\x06worker\x18\x01 \x01(\x0b\x32\x0e.ateapi.Worker\"`\n\x13\x44\x65leteWorkerRequest\x12!\n\x06worker\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12&\n\x07options\x18\x02 \x01(\x0b\x32\x15.ateapi.DeleteOptions\"7\n\x12\x44rainWorkerRequest\x12!\n\x06worker\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\"L\n\x11ListActorsRequest\x12\x10\n\x08\x61tespace\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t\"L\n\x12ListActorsResponse\x12\x1d\n\x06\x61\x63tors\x18\x01 \x03(\x0b\x32\r.ateapi.Actor\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"\xf0\x02\n\x06Worker\x12*\n\x08metadata\x18\x01 \x01(\x0b\x32\x18.ateapi.ResourceMetadata\x12\x18\n\x10worker_namespace\x18\x02 \x01(\t\x12\x13\n\x0bworker_pool\x18\x03 \x01(\t\x12\x12\n\nworker_pod\x18\x04 \x01(\t\x12\x16\n\x0eworker_pod_uid\x18\x05 \x01(\t\x12\x11\n\tnode_name\x18\x06 \x01(\t\x12\n\n\x02ip\x18\x07 \x01(\t\x12\x15\n\rsandbox_class\x18\x08 \x01(\t\x12*\n\x06labels\x18\t \x03(\x0b\x32\x1a.ateapi.Worker.LabelsEntry\x12(\n\x08\x63\x61pacity\x18\n \x01(\x0b\x32\x16.ateapi.WorkerCapacity\x12$\n\x06status\x18\x0b \x01(\x0b\x32\x14.ateapi.WorkerStatus\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"_\n\x0cWorkerStatus\x12\"\n\x05state\x18\x01 \x01(\x0e\x32\x13.ateapi.WorkerState\x12+\n\nassignment\x18\x02 \x01(\x0b\x32\x17.ateapi.ActorAssignment\"9\n\x0eWorkerCapacity\x12\x11\n\tcpu_milli\x18\x01 \x01(\x03\x12\x14\n\x0cmemory_bytes\x18\x02 \x01(\x03\"u\n\x0f\x41\x63torAssignment\x12 \n\x05\x61\x63tor\x18\x02 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12\x11\n\tactor_uid\x18\x03 \x01(\t\x12-\n\x12\x61\x63tor_template_ref\x18\x04 \x01(\x0b\x32\x11.ateapi.ObjectRef\"[\n\x0eMintJWTRequest\x12\x10\n\x08\x61udience\x18\x01 \x03(\t\x12\x10\n\x08\x61tespace\x18\x02 \x01(\t\x12\x12\n\nactor_name\x18\x03 \x01(\t\x12\x11\n\tactor_uid\x18\x04 \x01(\t\"$\n\x0fMintJWTResponse\x12\x11\n\tactor_jwt\x18\x01 \x01(\t\"\xa7\x01\n\x0fMintCertRequest\x12!\n\x06worker\x18\x01 \x01(\x0b\x32\x11.ateapi.ObjectRef\x12#\n\x1b\x63\x65rtificate_signing_request\x18\x02 \x01(\x0c\x12\x1a\n\x12\x65xpected_actor_uid\x18\x03 \x01(\t\x12\x30\n\x07purpose\x18\x04 \x01(\x0e\x32\x1f.ateapi.ActorCertificatePurpose\".\n\x10MintCertResponse\x12\x1a\n\x12\x61\x63tor_certificates\x18\x01 \x03(\x0c*\x80\x01\n\x14SnapshotContentScope\x12&\n\"SNAPSHOT_CONTENT_SCOPE_UNSPECIFIED\x10\x00\x12\x1f\n\x1bSNAPSHOT_CONTENT_SCOPE_FULL\x10\x01\x12\x1f\n\x1bSNAPSHOT_CONTENT_SCOPE_DATA\x10\x02*\x90\x01\n\x15\x41\x63torSnapshotTagScope\x12(\n$ACTOR_SNAPSHOT_TAG_SCOPE_UNSPECIFIED\x10\x00\x12%\n!ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE\x10\x01\x12&\n\"ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED\x10\x02*\xf7\x01\n\nActorState\x12\x1b\n\x17\x41\x43TOR_STATE_UNSPECIFIED\x10\x00\x12\x18\n\x14\x41\x43TOR_STATE_RESUMING\x10\x01\x12\x17\n\x13\x41\x43TOR_STATE_RUNNING\x10\x02\x12\x1a\n\x16\x41\x43TOR_STATE_SUSPENDING\x10\x03\x12\x19\n\x15\x41\x43TOR_STATE_SUSPENDED\x10\x04\x12\x17\n\x13\x41\x43TOR_STATE_PAUSING\x10\x05\x12\x16\n\x12\x41\x43TOR_STATE_PAUSED\x10\x06\x12\x17\n\x13\x41\x43TOR_STATE_CRASHED\x10\x07\x12\x18\n\x14\x41\x43TOR_STATE_DELETING\x10\x08*b\n\x0cSandboxClass\x12\x1d\n\x19SANDBOX_CLASS_UNSPECIFIED\x10\x00\x12\x18\n\x14SANDBOX_CLASS_GVISOR\x10\x01\x12\x19\n\x15SANDBOX_CLASS_MICROVM\x10\x02*d\n\x0cResumeSource\x12\x1d\n\x19RESUME_SOURCE_UNSPECIFIED\x10\x00\x12\x1b\n\x17RESUME_SOURCE_COLD_BOOT\x10\x01\x12\x18\n\x14RESUME_SOURCE_GOLDEN\x10\x02*\x9a\x01\n\x12\x41\x63torMetadataField\x12$\n ACTOR_METADATA_FIELD_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x41\x43TOR_METADATA_FIELD_NAME\x10\x01\x12!\n\x1d\x41\x43TOR_METADATA_FIELD_ATESPACE\x10\x02\x12\x1c\n\x18\x41\x43TOR_METADATA_FIELD_UID\x10\x03*_\n\x0bWorkerState\x12\x1c\n\x18WORKER_STATE_UNSPECIFIED\x10\x00\x12\x17\n\x13WORKER_STATE_ACTIVE\x10\x01\x12\x19\n\x15WORKER_STATE_DRAINING\x10\x02*k\n\x17\x41\x63torCertificatePurpose\x12)\n%ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED\x10\x00\x12%\n!ACTOR_CERTIFICATE_PURPOSE_ATUNNEL\x10\x01\x32\x84\x13\n\x07\x43ontrol\x12\x34\n\x08GetActor\x12\x17.ateapi.GetActorRequest\x1a\r.ateapi.Actor\"\x00\x12:\n\x0b\x43reateActor\x12\x1a.ateapi.CreateActorRequest\x1a\r.ateapi.Actor\"\x00\x12:\n\x0bUpdateActor\x12\x1a.ateapi.UpdateActorRequest\x1a\r.ateapi.Actor\"\x00\x12K\n\x0cSuspendActor\x12\x1b.ateapi.SuspendActorRequest\x1a\x1c.ateapi.SuspendActorResponse\"\x00\x12\x45\n\nPauseActor\x12\x19.ateapi.PauseActorRequest\x1a\x1a.ateapi.PauseActorResponse\"\x00\x12H\n\x0bResumeActor\x12\x1a.ateapi.ResumeActorRequest\x1a\x1b.ateapi.ResumeActorResponse\"\x00\x12:\n\x0b\x44\x65leteActor\x12\x1a.ateapi.DeleteActorRequest\x1a\r.ateapi.Actor\"\x00\x12S\n\x14GetActorEgressPolicy\x12#.ateapi.GetActorEgressPolicyRequest\x1a\x14.ateapi.EgressPolicy\"\x00\x12Y\n\x17\x43reateActorEgressPolicy\x12&.ateapi.CreateActorEgressPolicyRequest\x1a\x14.ateapi.EgressPolicy\"\x00\x12Y\n\x17UpdateActorEgressPolicy\x12&.ateapi.UpdateActorEgressPolicyRequest\x1a\x14.ateapi.EgressPolicy\"\x00\x12Y\n\x17\x44\x65leteActorEgressPolicy\x12&.ateapi.DeleteActorEgressPolicyRequest\x1a\x14.ateapi.EgressPolicy\"\x00\x12L\n\x10GetActorSnapshot\x12\x1f.ateapi.GetActorSnapshotRequest\x1a\x15.ateapi.ActorSnapshot\"\x00\x12U\n\x13GetActorSnapshotTag\x12\".ateapi.GetActorSnapshotTagRequest\x1a\x18.ateapi.ActorSnapshotTag\"\x00\x12]\n\x12ListActorSnapshots\x12!.ateapi.ListActorSnapshotsRequest\x1a\".ateapi.ListActorSnapshotsResponse\"\x00\x12[\n\x16\x43reateActorSnapshotTag\x12%.ateapi.CreateActorSnapshotTagRequest\x1a\x18.ateapi.ActorSnapshotTag\"\x00\x12[\n\x16UpdateActorSnapshotTag\x12%.ateapi.UpdateActorSnapshotTagRequest\x1a\x18.ateapi.ActorSnapshotTag\"\x00\x12[\n\x16\x44\x65leteActorSnapshotTag\x12%.ateapi.DeleteActorSnapshotTagRequest\x1a\x18.ateapi.ActorSnapshotTag\"\x00\x12H\n\x0bListWorkers\x12\x1a.ateapi.ListWorkersRequest\x1a\x1b.ateapi.ListWorkersResponse\"\x00\x12\x37\n\tGetWorker\x12\x18.ateapi.GetWorkerRequest\x1a\x0e.ateapi.Worker\"\x00\x12=\n\x0c\x43reateWorker\x12\x1b.ateapi.CreateWorkerRequest\x1a\x0e.ateapi.Worker\"\x00\x12=\n\x0cUpdateWorker\x12\x1b.ateapi.UpdateWorkerRequest\x1a\x0e.ateapi.Worker\"\x00\x12=\n\x0c\x44\x65leteWorker\x12\x1b.ateapi.DeleteWorkerRequest\x1a\x0e.ateapi.Worker\"\x00\x12;\n\x0b\x44rainWorker\x12\x1a.ateapi.DrainWorkerRequest\x1a\x0e.ateapi.Worker\"\x00\x12\x45\n\nListActors\x12\x19.ateapi.ListActorsRequest\x1a\x1a.ateapi.ListActorsResponse\"\x00\x12\x43\n\x0e\x43reateAtespace\x12\x1d.ateapi.CreateAtespaceRequest\x1a\x10.ateapi.Atespace\"\x00\x12=\n\x0bGetAtespace\x12\x1a.ateapi.GetAtespaceRequest\x1a\x10.ateapi.Atespace\"\x00\x12N\n\rListAtespaces\x12\x1c.ateapi.ListAtespacesRequest\x1a\x1d.ateapi.ListAtespacesResponse\"\x00\x12\x43\n\x0e\x44\x65leteAtespace\x12\x1d.ateapi.DeleteAtespaceRequest\x1a\x10.ateapi.Atespace\"\x00\x12R\n\x13\x43reateActorTemplate\x12\".ateapi.CreateActorTemplateRequest\x1a\x15.ateapi.ActorTemplate\"\x00\x12L\n\x10GetActorTemplate\x12\x1f.ateapi.GetActorTemplateRequest\x1a\x15.ateapi.ActorTemplate\"\x00\x12]\n\x12ListActorTemplates\x12!.ateapi.ListActorTemplatesRequest\x1a\".ateapi.ListActorTemplatesResponse\"\x00\x12R\n\x13\x44\x65leteActorTemplate\x12\".ateapi.DeleteActorTemplateRequest\x1a\x15.ateapi.ActorTemplate\"\x00\x32\x8a\x01\n\rActorIdentity\x12:\n\x07MintJWT\x12\x16.ateapi.MintJWTRequest\x1a\x17.ateapi.MintJWTResponse\x12=\n\x08MintCert\x12\x17.ateapi.MintCertRequest\x1a\x18.ateapi.MintCertResponseB9Z7github.com/agent-substrate/substrate/pkg/proto/ateapipbb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -54,22 +54,22 @@ _globals['_EXTERNALVOLUME_VOLUMECONTEXTENTRY']._serialized_options = b'8\001' _globals['_WORKER_LABELSENTRY']._loaded_options = None _globals['_WORKER_LABELSENTRY']._serialized_options = b'8\001' - _globals['_SNAPSHOTCONTENTSCOPE']._serialized_start=9270 - _globals['_SNAPSHOTCONTENTSCOPE']._serialized_end=9398 - _globals['_ACTORSNAPSHOTTAGSCOPE']._serialized_start=9401 - _globals['_ACTORSNAPSHOTTAGSCOPE']._serialized_end=9545 - _globals['_ACTORSTATE']._serialized_start=9548 - _globals['_ACTORSTATE']._serialized_end=9795 - _globals['_SANDBOXCLASS']._serialized_start=9797 - _globals['_SANDBOXCLASS']._serialized_end=9895 - _globals['_RESUMESOURCE']._serialized_start=9897 - _globals['_RESUMESOURCE']._serialized_end=9997 - _globals['_ACTORMETADATAFIELD']._serialized_start=10000 - _globals['_ACTORMETADATAFIELD']._serialized_end=10154 - _globals['_WORKERSTATE']._serialized_start=10156 - _globals['_WORKERSTATE']._serialized_end=10251 - _globals['_ACTORCERTIFICATEPURPOSE']._serialized_start=10253 - _globals['_ACTORCERTIFICATEPURPOSE']._serialized_end=10360 + _globals['_SNAPSHOTCONTENTSCOPE']._serialized_start=9357 + _globals['_SNAPSHOTCONTENTSCOPE']._serialized_end=9485 + _globals['_ACTORSNAPSHOTTAGSCOPE']._serialized_start=9488 + _globals['_ACTORSNAPSHOTTAGSCOPE']._serialized_end=9632 + _globals['_ACTORSTATE']._serialized_start=9635 + _globals['_ACTORSTATE']._serialized_end=9882 + _globals['_SANDBOXCLASS']._serialized_start=9884 + _globals['_SANDBOXCLASS']._serialized_end=9982 + _globals['_RESUMESOURCE']._serialized_start=9984 + _globals['_RESUMESOURCE']._serialized_end=10084 + _globals['_ACTORMETADATAFIELD']._serialized_start=10087 + _globals['_ACTORMETADATAFIELD']._serialized_end=10241 + _globals['_WORKERSTATE']._serialized_start=10243 + _globals['_WORKERSTATE']._serialized_end=10338 + _globals['_ACTORCERTIFICATEPURPOSE']._serialized_start=10340 + _globals['_ACTORCERTIFICATEPURPOSE']._serialized_end=10447 _globals['_LOCALSNAPSHOTINFO']._serialized_start=87 _globals['_LOCALSNAPSHOTINFO']._serialized_end=221 _globals['_SELECTOR']._serialized_start=223 @@ -99,171 +99,171 @@ _globals['_CREDENTIALHEADERINJECTION']._serialized_start=1524 _globals['_CREDENTIALHEADERINJECTION']._serialized_end=1607 _globals['_ACTORSTATUS']._serialized_start=1610 - _globals['_ACTORSTATUS']._serialized_end=2045 - _globals['_ACTORSOURCESNAPSHOTSTATUS']._serialized_start=2047 - _globals['_ACTORSOURCESNAPSHOTSTATUS']._serialized_end=2133 - _globals['_WORKERASSIGNMENT']._serialized_start=2136 - _globals['_WORKERASSIGNMENT']._serialized_end=2303 - _globals['_ACTORSNAPSHOT']._serialized_start=2305 - _globals['_ACTORSNAPSHOT']._serialized_end=2409 - _globals['_ACTORSNAPSHOTSTATUS']._serialized_start=2412 - _globals['_ACTORSNAPSHOTSTATUS']._serialized_end=2676 - _globals['_ACTORSNAPSHOTTAG']._serialized_start=2679 - _globals['_ACTORSNAPSHOTTAG']._serialized_end=2824 - _globals['_ATESPACE']._serialized_start=2826 - _globals['_ATESPACE']._serialized_end=2880 - _globals['_OBJECTREF']._serialized_start=2882 - _globals['_OBJECTREF']._serialized_end=2925 - _globals['_ACTORTEMPLATE']._serialized_start=2928 - _globals['_ACTORTEMPLATE']._serialized_end=3283 - _globals['_RESOURCES']._serialized_start=3285 - _globals['_RESOURCES']._serialized_end=3328 - _globals['_LIMITS']._serialized_start=3330 - _globals['_LIMITS']._serialized_end=3370 - _globals['_GOLDENSNAPSHOTSTATUS']._serialized_start=3373 - _globals['_GOLDENSNAPSHOTSTATUS']._serialized_end=3523 - _globals['_ACTORTEMPLATESTATUS']._serialized_start=3525 - _globals['_ACTORTEMPLATESTATUS']._serialized_end=3608 - _globals['_SANDBOXCONFIG']._serialized_start=3610 - _globals['_SANDBOXCONFIG']._serialized_end=3691 - _globals['_SNAPSHOTSCONFIG']._serialized_start=3694 - _globals['_SNAPSHOTSCONFIG']._serialized_end=3877 - _globals['_ONRESUMECONFIG']._serialized_start=3879 - _globals['_ONRESUMECONFIG']._serialized_end=3936 - _globals['_CONTAINER']._serialized_start=3939 - _globals['_CONTAINER']._serialized_end=4213 - _globals['_SECURITYCONTEXT']._serialized_start=4215 - _globals['_SECURITYCONTEXT']._serialized_end=4276 - _globals['_CAPABILITIES']._serialized_start=4278 - _globals['_CAPABILITIES']._serialized_end=4319 - _globals['_ENVVAR']._serialized_start=4321 - _globals['_ENVVAR']._serialized_end=4358 - _globals['_CONTAINERREADYZ']._serialized_start=4360 - _globals['_CONTAINERREADYZ']._serialized_end=4443 - _globals['_HTTPGETACTION']._serialized_start=4445 - _globals['_HTTPGETACTION']._serialized_end=4488 - _globals['_VOLUME']._serialized_start=4491 - _globals['_VOLUME']._serialized_end=4727 - _globals['_IMAGEVOLUMESOURCE']._serialized_start=4729 - _globals['_IMAGEVOLUMESOURCE']._serialized_end=4767 - _globals['_DURABLEDIRVOLUMESOURCE']._serialized_start=4769 - _globals['_DURABLEDIRVOLUMESOURCE']._serialized_end=4793 - _globals['_EXTERNALVOLUMETEMPLATE']._serialized_start=4795 - _globals['_EXTERNALVOLUMETEMPLATE']._serialized_end=4865 - _globals['_SYSTEMINFOVOLUMESOURCE']._serialized_start=4867 - _globals['_SYSTEMINFOVOLUMESOURCE']._serialized_end=4943 - _globals['_SYSTEMINFODATASOURCE']._serialized_start=4946 - _globals['_SYSTEMINFODATASOURCE']._serialized_end=5078 - _globals['_ACTORMETADATADATASOURCE']._serialized_start=5080 - _globals['_ACTORMETADATADATASOURCE']._serialized_end=5147 - _globals['_ACTORMETADATAITEM']._serialized_start=5149 - _globals['_ACTORMETADATAITEM']._serialized_end=5225 - _globals['_TRUSTBUNDLEDATASOURCE']._serialized_start=5227 - _globals['_TRUSTBUNDLEDATASOURCE']._serialized_end=5278 - _globals['_VOLUMEMOUNT']._serialized_start=5280 - _globals['_VOLUMEMOUNT']._serialized_end=5327 - _globals['_CREATEATESPACEREQUEST']._serialized_start=5329 - _globals['_CREATEATESPACEREQUEST']._serialized_end=5388 - _globals['_GETATESPACEREQUEST']._serialized_start=5390 - _globals['_GETATESPACEREQUEST']._serialized_end=5447 - _globals['_LISTATESPACESREQUEST']._serialized_start=5449 - _globals['_LISTATESPACESREQUEST']._serialized_end=5510 - _globals['_LISTATESPACESRESPONSE']._serialized_start=5512 - _globals['_LISTATESPACESRESPONSE']._serialized_end=5597 - _globals['_DELETEATESPACEREQUEST']._serialized_start=5599 - _globals['_DELETEATESPACEREQUEST']._serialized_end=5659 - _globals['_CREATEACTORTEMPLATEREQUEST']._serialized_start=5661 - _globals['_CREATEACTORTEMPLATEREQUEST']._serialized_end=5736 - _globals['_GETACTORTEMPLATEREQUEST']._serialized_start=5738 - _globals['_GETACTORTEMPLATEREQUEST']._serialized_end=5806 - _globals['_LISTACTORTEMPLATESREQUEST']._serialized_start=5808 - _globals['_LISTACTORTEMPLATESREQUEST']._serialized_end=5892 - _globals['_LISTACTORTEMPLATESRESPONSE']._serialized_start=5894 - _globals['_LISTACTORTEMPLATESRESPONSE']._serialized_end=5995 - _globals['_DELETEACTORTEMPLATEREQUEST']._serialized_start=5997 - _globals['_DELETEACTORTEMPLATEREQUEST']._serialized_end=6068 - _globals['_GETACTORREQUEST']._serialized_start=6070 - _globals['_GETACTORREQUEST']._serialized_end=6121 - _globals['_CREATEACTORREQUEST']._serialized_start=6123 - _globals['_CREATEACTORREQUEST']._serialized_end=6173 - _globals['_UPDATEACTORREQUEST']._serialized_start=6175 - _globals['_UPDATEACTORREQUEST']._serialized_end=6225 - _globals['_SUSPENDACTORREQUEST']._serialized_start=6227 - _globals['_SUSPENDACTORREQUEST']._serialized_end=6282 - _globals['_SUSPENDACTORRESPONSE']._serialized_start=6284 - _globals['_SUSPENDACTORRESPONSE']._serialized_end=6336 - _globals['_PAUSEACTORREQUEST']._serialized_start=6338 - _globals['_PAUSEACTORREQUEST']._serialized_end=6391 - _globals['_PAUSEACTORRESPONSE']._serialized_start=6393 - _globals['_PAUSEACTORRESPONSE']._serialized_end=6443 - _globals['_RESUMEACTORREQUEST']._serialized_start=6445 - _globals['_RESUMEACTORREQUEST']._serialized_end=6513 - _globals['_RESUMEACTORRESPONSE']._serialized_start=6515 - _globals['_RESUMEACTORRESPONSE']._serialized_end=6583 - _globals['_DELETEACTORREQUEST']._serialized_start=6585 - _globals['_DELETEACTORREQUEST']._serialized_end=6658 - _globals['_GETACTOREGRESSPOLICYREQUEST']._serialized_start=6660 - _globals['_GETACTOREGRESSPOLICYREQUEST']._serialized_end=6723 - _globals['_CREATEACTOREGRESSPOLICYREQUEST']._serialized_start=6725 - _globals['_CREATEACTOREGRESSPOLICYREQUEST']._serialized_end=6836 - _globals['_UPDATEACTOREGRESSPOLICYREQUEST']._serialized_start=6838 - _globals['_UPDATEACTOREGRESSPOLICYREQUEST']._serialized_end=6949 - _globals['_DELETEACTOREGRESSPOLICYREQUEST']._serialized_start=6951 - _globals['_DELETEACTOREGRESSPOLICYREQUEST']._serialized_end=7017 - _globals['_GETACTORSNAPSHOTREQUEST']._serialized_start=7019 - _globals['_GETACTORSNAPSHOTREQUEST']._serialized_end=7087 - _globals['_GETACTORSNAPSHOTTAGREQUEST']._serialized_start=7089 - _globals['_GETACTORSNAPSHOTTAGREQUEST']._serialized_end=7164 - _globals['_LISTACTORSNAPSHOTSREQUEST']._serialized_start=7166 - _globals['_LISTACTORSNAPSHOTSREQUEST']._serialized_end=7250 - _globals['_LISTACTORSNAPSHOTSRESPONSE']._serialized_start=7252 - _globals['_LISTACTORSNAPSHOTSRESPONSE']._serialized_end=7353 - _globals['_CREATEACTORSNAPSHOTTAGREQUEST']._serialized_start=7355 - _globals['_CREATEACTORSNAPSHOTTAGREQUEST']._serialized_end=7440 - _globals['_UPDATEACTORSNAPSHOTTAGREQUEST']._serialized_start=7442 - _globals['_UPDATEACTORSNAPSHOTTAGREQUEST']._serialized_end=7527 - _globals['_DELETEACTORSNAPSHOTTAGREQUEST']._serialized_start=7529 - _globals['_DELETEACTORSNAPSHOTTAGREQUEST']._serialized_end=7607 - _globals['_DELETEOPTIONS']._serialized_start=7609 - _globals['_DELETEOPTIONS']._serialized_end=7654 - _globals['_LISTWORKERSREQUEST']._serialized_start=7656 - _globals['_LISTWORKERSREQUEST']._serialized_end=7715 - _globals['_LISTWORKERSRESPONSE']._serialized_start=7717 - _globals['_LISTWORKERSRESPONSE']._serialized_end=7796 - _globals['_GETWORKERREQUEST']._serialized_start=7798 - _globals['_GETWORKERREQUEST']._serialized_end=7851 - _globals['_CREATEWORKERREQUEST']._serialized_start=7853 - _globals['_CREATEWORKERREQUEST']._serialized_end=7906 - _globals['_UPDATEWORKERREQUEST']._serialized_start=7908 - _globals['_UPDATEWORKERREQUEST']._serialized_end=7961 - _globals['_DELETEWORKERREQUEST']._serialized_start=7963 - _globals['_DELETEWORKERREQUEST']._serialized_end=8059 - _globals['_DRAINWORKERREQUEST']._serialized_start=8061 - _globals['_DRAINWORKERREQUEST']._serialized_end=8116 - _globals['_LISTACTORSREQUEST']._serialized_start=8118 - _globals['_LISTACTORSREQUEST']._serialized_end=8194 - _globals['_LISTACTORSRESPONSE']._serialized_start=8196 - _globals['_LISTACTORSRESPONSE']._serialized_end=8272 - _globals['_WORKER']._serialized_start=8275 - _globals['_WORKER']._serialized_end=8643 - _globals['_WORKER_LABELSENTRY']._serialized_start=8598 - _globals['_WORKER_LABELSENTRY']._serialized_end=8643 - _globals['_WORKERSTATUS']._serialized_start=8645 - _globals['_WORKERSTATUS']._serialized_end=8740 - _globals['_WORKERCAPACITY']._serialized_start=8742 - _globals['_WORKERCAPACITY']._serialized_end=8799 - _globals['_ACTORASSIGNMENT']._serialized_start=8801 - _globals['_ACTORASSIGNMENT']._serialized_end=8918 - _globals['_MINTJWTREQUEST']._serialized_start=8920 - _globals['_MINTJWTREQUEST']._serialized_end=9011 - _globals['_MINTJWTRESPONSE']._serialized_start=9013 - _globals['_MINTJWTRESPONSE']._serialized_end=9049 - _globals['_MINTCERTREQUEST']._serialized_start=9052 - _globals['_MINTCERTREQUEST']._serialized_end=9219 - _globals['_MINTCERTRESPONSE']._serialized_start=9221 - _globals['_MINTCERTRESPONSE']._serialized_end=9267 - _globals['_CONTROL']._serialized_start=10363 - _globals['_CONTROL']._serialized_end=12799 - _globals['_ACTORIDENTITY']._serialized_start=12802 - _globals['_ACTORIDENTITY']._serialized_end=12940 + _globals['_ACTORSTATUS']._serialized_end=2132 + _globals['_ACTORSOURCESNAPSHOTSTATUS']._serialized_start=2134 + _globals['_ACTORSOURCESNAPSHOTSTATUS']._serialized_end=2220 + _globals['_WORKERASSIGNMENT']._serialized_start=2223 + _globals['_WORKERASSIGNMENT']._serialized_end=2390 + _globals['_ACTORSNAPSHOT']._serialized_start=2392 + _globals['_ACTORSNAPSHOT']._serialized_end=2496 + _globals['_ACTORSNAPSHOTSTATUS']._serialized_start=2499 + _globals['_ACTORSNAPSHOTSTATUS']._serialized_end=2763 + _globals['_ACTORSNAPSHOTTAG']._serialized_start=2766 + _globals['_ACTORSNAPSHOTTAG']._serialized_end=2911 + _globals['_ATESPACE']._serialized_start=2913 + _globals['_ATESPACE']._serialized_end=2967 + _globals['_OBJECTREF']._serialized_start=2969 + _globals['_OBJECTREF']._serialized_end=3012 + _globals['_ACTORTEMPLATE']._serialized_start=3015 + _globals['_ACTORTEMPLATE']._serialized_end=3370 + _globals['_RESOURCES']._serialized_start=3372 + _globals['_RESOURCES']._serialized_end=3415 + _globals['_LIMITS']._serialized_start=3417 + _globals['_LIMITS']._serialized_end=3457 + _globals['_GOLDENSNAPSHOTSTATUS']._serialized_start=3460 + _globals['_GOLDENSNAPSHOTSTATUS']._serialized_end=3610 + _globals['_ACTORTEMPLATESTATUS']._serialized_start=3612 + _globals['_ACTORTEMPLATESTATUS']._serialized_end=3695 + _globals['_SANDBOXCONFIG']._serialized_start=3697 + _globals['_SANDBOXCONFIG']._serialized_end=3778 + _globals['_SNAPSHOTSCONFIG']._serialized_start=3781 + _globals['_SNAPSHOTSCONFIG']._serialized_end=3964 + _globals['_ONRESUMECONFIG']._serialized_start=3966 + _globals['_ONRESUMECONFIG']._serialized_end=4023 + _globals['_CONTAINER']._serialized_start=4026 + _globals['_CONTAINER']._serialized_end=4300 + _globals['_SECURITYCONTEXT']._serialized_start=4302 + _globals['_SECURITYCONTEXT']._serialized_end=4363 + _globals['_CAPABILITIES']._serialized_start=4365 + _globals['_CAPABILITIES']._serialized_end=4406 + _globals['_ENVVAR']._serialized_start=4408 + _globals['_ENVVAR']._serialized_end=4445 + _globals['_CONTAINERREADYZ']._serialized_start=4447 + _globals['_CONTAINERREADYZ']._serialized_end=4530 + _globals['_HTTPGETACTION']._serialized_start=4532 + _globals['_HTTPGETACTION']._serialized_end=4575 + _globals['_VOLUME']._serialized_start=4578 + _globals['_VOLUME']._serialized_end=4814 + _globals['_IMAGEVOLUMESOURCE']._serialized_start=4816 + _globals['_IMAGEVOLUMESOURCE']._serialized_end=4854 + _globals['_DURABLEDIRVOLUMESOURCE']._serialized_start=4856 + _globals['_DURABLEDIRVOLUMESOURCE']._serialized_end=4880 + _globals['_EXTERNALVOLUMETEMPLATE']._serialized_start=4882 + _globals['_EXTERNALVOLUMETEMPLATE']._serialized_end=4952 + _globals['_SYSTEMINFOVOLUMESOURCE']._serialized_start=4954 + _globals['_SYSTEMINFOVOLUMESOURCE']._serialized_end=5030 + _globals['_SYSTEMINFODATASOURCE']._serialized_start=5033 + _globals['_SYSTEMINFODATASOURCE']._serialized_end=5165 + _globals['_ACTORMETADATADATASOURCE']._serialized_start=5167 + _globals['_ACTORMETADATADATASOURCE']._serialized_end=5234 + _globals['_ACTORMETADATAITEM']._serialized_start=5236 + _globals['_ACTORMETADATAITEM']._serialized_end=5312 + _globals['_TRUSTBUNDLEDATASOURCE']._serialized_start=5314 + _globals['_TRUSTBUNDLEDATASOURCE']._serialized_end=5365 + _globals['_VOLUMEMOUNT']._serialized_start=5367 + _globals['_VOLUMEMOUNT']._serialized_end=5414 + _globals['_CREATEATESPACEREQUEST']._serialized_start=5416 + _globals['_CREATEATESPACEREQUEST']._serialized_end=5475 + _globals['_GETATESPACEREQUEST']._serialized_start=5477 + _globals['_GETATESPACEREQUEST']._serialized_end=5534 + _globals['_LISTATESPACESREQUEST']._serialized_start=5536 + _globals['_LISTATESPACESREQUEST']._serialized_end=5597 + _globals['_LISTATESPACESRESPONSE']._serialized_start=5599 + _globals['_LISTATESPACESRESPONSE']._serialized_end=5684 + _globals['_DELETEATESPACEREQUEST']._serialized_start=5686 + _globals['_DELETEATESPACEREQUEST']._serialized_end=5746 + _globals['_CREATEACTORTEMPLATEREQUEST']._serialized_start=5748 + _globals['_CREATEACTORTEMPLATEREQUEST']._serialized_end=5823 + _globals['_GETACTORTEMPLATEREQUEST']._serialized_start=5825 + _globals['_GETACTORTEMPLATEREQUEST']._serialized_end=5893 + _globals['_LISTACTORTEMPLATESREQUEST']._serialized_start=5895 + _globals['_LISTACTORTEMPLATESREQUEST']._serialized_end=5979 + _globals['_LISTACTORTEMPLATESRESPONSE']._serialized_start=5981 + _globals['_LISTACTORTEMPLATESRESPONSE']._serialized_end=6082 + _globals['_DELETEACTORTEMPLATEREQUEST']._serialized_start=6084 + _globals['_DELETEACTORTEMPLATEREQUEST']._serialized_end=6155 + _globals['_GETACTORREQUEST']._serialized_start=6157 + _globals['_GETACTORREQUEST']._serialized_end=6208 + _globals['_CREATEACTORREQUEST']._serialized_start=6210 + _globals['_CREATEACTORREQUEST']._serialized_end=6260 + _globals['_UPDATEACTORREQUEST']._serialized_start=6262 + _globals['_UPDATEACTORREQUEST']._serialized_end=6312 + _globals['_SUSPENDACTORREQUEST']._serialized_start=6314 + _globals['_SUSPENDACTORREQUEST']._serialized_end=6369 + _globals['_SUSPENDACTORRESPONSE']._serialized_start=6371 + _globals['_SUSPENDACTORRESPONSE']._serialized_end=6423 + _globals['_PAUSEACTORREQUEST']._serialized_start=6425 + _globals['_PAUSEACTORREQUEST']._serialized_end=6478 + _globals['_PAUSEACTORRESPONSE']._serialized_start=6480 + _globals['_PAUSEACTORRESPONSE']._serialized_end=6530 + _globals['_RESUMEACTORREQUEST']._serialized_start=6532 + _globals['_RESUMEACTORREQUEST']._serialized_end=6600 + _globals['_RESUMEACTORRESPONSE']._serialized_start=6602 + _globals['_RESUMEACTORRESPONSE']._serialized_end=6670 + _globals['_DELETEACTORREQUEST']._serialized_start=6672 + _globals['_DELETEACTORREQUEST']._serialized_end=6745 + _globals['_GETACTOREGRESSPOLICYREQUEST']._serialized_start=6747 + _globals['_GETACTOREGRESSPOLICYREQUEST']._serialized_end=6810 + _globals['_CREATEACTOREGRESSPOLICYREQUEST']._serialized_start=6812 + _globals['_CREATEACTOREGRESSPOLICYREQUEST']._serialized_end=6923 + _globals['_UPDATEACTOREGRESSPOLICYREQUEST']._serialized_start=6925 + _globals['_UPDATEACTOREGRESSPOLICYREQUEST']._serialized_end=7036 + _globals['_DELETEACTOREGRESSPOLICYREQUEST']._serialized_start=7038 + _globals['_DELETEACTOREGRESSPOLICYREQUEST']._serialized_end=7104 + _globals['_GETACTORSNAPSHOTREQUEST']._serialized_start=7106 + _globals['_GETACTORSNAPSHOTREQUEST']._serialized_end=7174 + _globals['_GETACTORSNAPSHOTTAGREQUEST']._serialized_start=7176 + _globals['_GETACTORSNAPSHOTTAGREQUEST']._serialized_end=7251 + _globals['_LISTACTORSNAPSHOTSREQUEST']._serialized_start=7253 + _globals['_LISTACTORSNAPSHOTSREQUEST']._serialized_end=7337 + _globals['_LISTACTORSNAPSHOTSRESPONSE']._serialized_start=7339 + _globals['_LISTACTORSNAPSHOTSRESPONSE']._serialized_end=7440 + _globals['_CREATEACTORSNAPSHOTTAGREQUEST']._serialized_start=7442 + _globals['_CREATEACTORSNAPSHOTTAGREQUEST']._serialized_end=7527 + _globals['_UPDATEACTORSNAPSHOTTAGREQUEST']._serialized_start=7529 + _globals['_UPDATEACTORSNAPSHOTTAGREQUEST']._serialized_end=7614 + _globals['_DELETEACTORSNAPSHOTTAGREQUEST']._serialized_start=7616 + _globals['_DELETEACTORSNAPSHOTTAGREQUEST']._serialized_end=7694 + _globals['_DELETEOPTIONS']._serialized_start=7696 + _globals['_DELETEOPTIONS']._serialized_end=7741 + _globals['_LISTWORKERSREQUEST']._serialized_start=7743 + _globals['_LISTWORKERSREQUEST']._serialized_end=7802 + _globals['_LISTWORKERSRESPONSE']._serialized_start=7804 + _globals['_LISTWORKERSRESPONSE']._serialized_end=7883 + _globals['_GETWORKERREQUEST']._serialized_start=7885 + _globals['_GETWORKERREQUEST']._serialized_end=7938 + _globals['_CREATEWORKERREQUEST']._serialized_start=7940 + _globals['_CREATEWORKERREQUEST']._serialized_end=7993 + _globals['_UPDATEWORKERREQUEST']._serialized_start=7995 + _globals['_UPDATEWORKERREQUEST']._serialized_end=8048 + _globals['_DELETEWORKERREQUEST']._serialized_start=8050 + _globals['_DELETEWORKERREQUEST']._serialized_end=8146 + _globals['_DRAINWORKERREQUEST']._serialized_start=8148 + _globals['_DRAINWORKERREQUEST']._serialized_end=8203 + _globals['_LISTACTORSREQUEST']._serialized_start=8205 + _globals['_LISTACTORSREQUEST']._serialized_end=8281 + _globals['_LISTACTORSRESPONSE']._serialized_start=8283 + _globals['_LISTACTORSRESPONSE']._serialized_end=8359 + _globals['_WORKER']._serialized_start=8362 + _globals['_WORKER']._serialized_end=8730 + _globals['_WORKER_LABELSENTRY']._serialized_start=8685 + _globals['_WORKER_LABELSENTRY']._serialized_end=8730 + _globals['_WORKERSTATUS']._serialized_start=8732 + _globals['_WORKERSTATUS']._serialized_end=8827 + _globals['_WORKERCAPACITY']._serialized_start=8829 + _globals['_WORKERCAPACITY']._serialized_end=8886 + _globals['_ACTORASSIGNMENT']._serialized_start=8888 + _globals['_ACTORASSIGNMENT']._serialized_end=9005 + _globals['_MINTJWTREQUEST']._serialized_start=9007 + _globals['_MINTJWTREQUEST']._serialized_end=9098 + _globals['_MINTJWTRESPONSE']._serialized_start=9100 + _globals['_MINTJWTRESPONSE']._serialized_end=9136 + _globals['_MINTCERTREQUEST']._serialized_start=9139 + _globals['_MINTCERTREQUEST']._serialized_end=9306 + _globals['_MINTCERTRESPONSE']._serialized_start=9308 + _globals['_MINTCERTRESPONSE']._serialized_end=9354 + _globals['_CONTROL']._serialized_start=10450 + _globals['_CONTROL']._serialized_end=12886 + _globals['_ACTORIDENTITY']._serialized_start=12889 + _globals['_ACTORIDENTITY']._serialized_end=13027 # @@protoc_insertion_point(module_scope) diff --git a/cmd/ateapi/internal/controlapi/actor.go b/cmd/ateapi/internal/controlapi/actor.go index ec12686c28..31db3bfe8b 100644 --- a/cmd/ateapi/internal/controlapi/actor.go +++ b/cmd/ateapi/internal/controlapi/actor.go @@ -277,6 +277,15 @@ func (s *ServiceImpl) UpdateActor(ctx context.Context, actorRef resources.ActorR // Do any further work on the resource. + // A repointed template ref must resolve, mirroring CreateActor's check + // (same non-atomicity caveat; resume re-resolves and fails cleanly). + if !proto.Equal(oldVal.GetActorTemplate(), newVal.GetActorTemplate()) { + // TODO: Add validation that compares the new and previous ActorTemplate. + if _, err := resolveActorTemplate(ctx, s.store, newVal); err != nil { + return err + } + } + // Validate the final value before storing it. if errs := validateActorUpdate(ctx, field.NewPath("actor"), newVal, oldVal, true); len(errs) > 0 { return toGRPCInternalError(errs) diff --git a/cmd/ateapi/internal/controlapi/actor_test.go b/cmd/ateapi/internal/controlapi/actor_test.go index 936f1b090a..af8b055505 100644 --- a/cmd/ateapi/internal/controlapi/actor_test.go +++ b/cmd/ateapi/internal/controlapi/actor_test.go @@ -867,6 +867,54 @@ func TestUpdateActor(t *testing.T) { } } +// TestUpdateActor_RepointTemplate covers the mutable actor_template ref: an +// update may point the actor at a different template (it takes effect on the +// next ResumeActor), but the new ref must resolve. +func TestUpdateActor_RepointTemplate(t *testing.T) { + ctx := context.Background() + persistence, cleanup := storetest.SetupTestStore(t) + t.Cleanup(cleanup) + + storetest.MustCreateAtespace(t, ctx, persistence, testAtespace) + for _, name := range []string{"tmpl-a", "tmpl-b"} { + if _, err := persistence.CreateActorTemplate(ctx, &ateapipb.ActorTemplate{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: name}, + Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1"}}, + SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"}, + }); err != nil { + t.Fatalf("creating template %s: %v", name, err) + } + } + + created := storetest.MustCreateActor(t, ctx, persistence, &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: testActorID}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-a"}, + Status: &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED}, + }) + svc := &RPCService{impl: newServiceImpl(persistence, nil)} + + // Repointing at a template that does not exist is rejected. + _, err := svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ + Metadata: created.GetMetadata(), + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "absent"}, + }}) + if got := status.Code(err); got != codes.FailedPrecondition { + t.Fatalf("UpdateActor to an absent template = %v, want FailedPrecondition (err: %v)", got, err) + } + + // Repointing at an existing template succeeds. + updated, err := svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ + Metadata: created.GetMetadata(), + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-b"}, + }}) + if err != nil { + t.Fatalf("UpdateActor failed: %v", err) + } + if got, want := updated.GetActorTemplate().GetName(), "tmpl-b"; got != want { + t.Errorf("updated actor_template.name = %q, want %q", got, want) + } +} + // TestUpdateActor_DeleteRecreateRace checks that an update is not applied // if an actor was deleted and recreated during the update operation. func TestUpdateActor_DeleteRecreateRace(t *testing.T) { diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go index ecb127fd17..d8a07846a1 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go @@ -665,6 +665,63 @@ func TestUpdateActor_Success(t *testing.T) { } } +// TestUpdateActor_RepointTemplate verifies UpdateActor can point an actor at +// a different substrate ActorTemplate (effective on the next ResumeActor), +// and that a ref to an absent template is rejected. +func TestUpdateActor_RepointTemplate(t *testing.T) { + ns := namespaceForTest("ns-update-repoint") + tc := setupTest(t, ns) + defer tc.cleanup() + + ctx := context.Background() + for _, name := range []string{"tmpl-a", "tmpl-b"} { + if _, err := tc.client.CreateActorTemplate(ctx, &ateapipb.CreateActorTemplateRequest{ + ActorTemplate: &ateapipb.ActorTemplate{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: name}, + Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1"}}, + SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"}, + SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"}, + }, + }); err != nil { + t.Fatalf("CreateActorTemplate %s failed: %v", name, err) + } + } + + created, err := tc.client.CreateActor(ctx, &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "repoint-actor"}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-a"}, + }}) + if err != nil { + t.Fatalf("CreateActor failed: %v", err) + } + + // A ref to a template that does not exist is rejected. + _, err = tc.client.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ + Metadata: created.GetMetadata(), + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "absent"}, + }}) + if got := status.Code(err); got != codes.FailedPrecondition { + t.Fatalf("UpdateActor with an absent template ref = %v, want FailedPrecondition (err: %v)", got, err) + } + + updated, err := tc.client.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ + Metadata: created.GetMetadata(), + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-b"}, + }}) + if err != nil { + t.Fatalf("UpdateActor failed: %v", err) + } + + want := &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "repoint-actor", Version: 2}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-b"}, + Status: &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED}, + } + if diff := cmp.Diff(want, updated, protocmp.Transform(), ignoreUID, ignoreTimestamps); diff != "" { + t.Errorf("UpdateActor response mismatch (-want +got):\n%s", diff) + } +} + // TestUpdateActor verifies a typical RMW UpdateActor flow: a // client reads an actor, modifies it and send an UpdateActor request. // Output-only fields it sets are ignored, and the mutable actor_template ref @@ -2014,7 +2071,7 @@ func TestSuspendActor(t *testing.T) { tc := setupTest(t, ns) defer tc.cleanup() - createTemplate(t, tc, ns) + tmpl := createTemplate(t, tc, ns) workerName := createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") name := "id1" @@ -2159,7 +2216,11 @@ func TestSuspendActor(t *testing.T) { want := &ateapipb.Actor{ Metadata: &ateapipb.ResourceMetadata{Name: name, Atespace: testAtespace}, ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"}, - Status: &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED}, + Status: &ateapipb.ActorStatus{ + State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED, + CurrentActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"}, + CurrentActorTemplateUid: tmpl.GetMetadata().GetUid(), + }, } if diff := cmp.Diff(want, getResp, @@ -2203,7 +2264,7 @@ func TestPauseActor(t *testing.T) { tc := setupTest(t, ns) defer tc.cleanup() - createTemplate(t, tc, ns) + tmpl := createTemplate(t, tc, ns) createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") @@ -2251,6 +2312,8 @@ func TestPauseActor(t *testing.T) { NodeVmsWithLocalSnapshots: []string{"node1"}, ContentScope: ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_FULL, }, + CurrentActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"}, + CurrentActorTemplateUid: tmpl.GetMetadata().GetUid(), }, } diff --git a/cmd/ateapi/internal/controlapi/workflow_resume.go b/cmd/ateapi/internal/controlapi/workflow_resume.go index 80334d25a0..07f69f4380 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume.go @@ -126,7 +126,7 @@ func (w *ActorWorkflow) ResumeActor(ctx context.Context, actorRef resources.Acto return nil, false, err } var running *ateapipb.Actor - if running, err = w.finalizeRunning(leaseCtx, actorRef); err != nil { + if running, err = w.finalizeRunning(leaseCtx, actorRef, actorTemplate); err != nil { return nil, false, err } actor = running @@ -671,6 +671,12 @@ func (w *ActorWorkflow) ensureAteletRestored(ctx context.Context, actorRef resou return tele, err } + // A snapshot taken under a since-replaced template holds guest state that + // cannot be layered onto the new template's golden snapshot, so the + // restore is forced to data-only. + templateReplaced := actor.GetStatus().GetCurrentActorTemplate() != nil && + actor.GetStatus().GetCurrentActorTemplateUid() != actorTemplate.GetMetadata().GetUid() + if local := actor.GetStatus().GetLocalSnapshotInfo(); local != nil { slog.InfoContext(ctx, "Actor has snapshot; Restoring from snapshot") tele.SnapshotKind = ateattr.SnapshotKindLocal @@ -691,16 +697,17 @@ func (w *ActorWorkflow) ensureAteletRestored(ctx context.Context, actorRef resou req.Config = &ateletpb.RestoreRequest_LocalConfig{ LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: local.GetSnapshotName()}, } - // The wire scope describes the restore OPERATION. When the template's - // onResume configuration selected the golden snapshot as the boot - // source, loadActorForResume resolved the golden URI, and the - // pause snapshot restores as DATA_ON_GOLDEN — atelet combines the - // golden snapshot's guest state with the actor's data. Otherwise the - // scope mirrors what the pause captured. - req.Scope = actorSnapshotContentScopeToAtelet(actorTemplate.GetSnapshotsConfig().GetOnPause()) - if !src.GoldenSnapshotURI.IsZero() { + // The wire scope describes the restore OPERATION: DATA_ON_GOLDEN when + // loadActorForResume resolved a golden URI per the template's onResume + // configuration, else what the pause captured. + switch { + case templateReplaced: + req.Scope = ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA + case !src.GoldenSnapshotURI.IsZero(): req.Scope = ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA_ON_GOLDEN req.GoldenSnapshotUri = src.GoldenSnapshotURI.String() + default: + req.Scope = actorSnapshotContentScopeToAtelet(actorTemplate.GetSnapshotsConfig().GetOnPause()) } tele.WireSnapshotScope = ateattr.SnapshotScopeValue(req.Scope) @@ -714,13 +721,16 @@ func (w *ActorWorkflow) ensureAteletRestored(ctx context.Context, actorRef resou if actor.GetStatus().GetLatestSnapshot() != nil { tele.SnapshotKind = ateattr.SnapshotKindLatest } - - // Same wire-scope derivation as the local branch above: the snapshot - // restores as DATA_ON_GOLDEN when the golden URI was resolved per the - // template's onResume configuration. - scope := actorSnapshotContentScopeToAtelet(src.Scope) - if !src.GoldenSnapshotURI.IsZero() { + var scope ateletpb.SnapshotScope + var goldenSnapshotURI string + switch { + case templateReplaced: + scope = ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA + case !src.GoldenSnapshotURI.IsZero(): scope = ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA_ON_GOLDEN + goldenSnapshotURI = src.GoldenSnapshotURI.String() + default: + scope = actorSnapshotContentScopeToAtelet(src.Scope) } tele.WireSnapshotScope = ateattr.SnapshotScopeValue(scope) req := &ateletpb.RestoreRequest{ @@ -738,7 +748,7 @@ func (w *ActorWorkflow) ensureAteletRestored(ctx context.Context, actorRef resou }, Scope: scope, // Empty unless this is a Golden data resume. - GoldenSnapshotUri: src.GoldenSnapshotURI.String(), + GoldenSnapshotUri: goldenSnapshotURI, ActorUid: actor.GetMetadata().Uid, EgressGateway: egressGateway, CpuMilli: cpuMilli, @@ -783,8 +793,9 @@ func (w *ActorWorkflow) egressGateway() *ateletpb.EgressGateway { return &ateletpb.EgressGateway{Address: w.egressGatewayAddress} } -// finalizeRunning re-reads the actor for a fresh version and commits RUNNING. -func (w *ActorWorkflow) finalizeRunning(ctx context.Context, actorRef resources.ActorRef) (_ *ateapipb.Actor, err error) { +// finalizeRunning re-reads the actor for a fresh version and commits RUNNING, +// recording the template the sprint booted with. +func (w *ActorWorkflow) finalizeRunning(ctx context.Context, actorRef resources.ActorRef, actorTemplate *ateapipb.ActorTemplate) (_ *ateapipb.Actor, err error) { ctx, done := stepSpan(ctx, "FinalizeRunning") defer func() { err = done(err) }() @@ -795,6 +806,13 @@ func (w *ActorWorkflow) finalizeRunning(ctx context.Context, actorRef resources. storedActor, err := w.store.UpdateActor(ctx, actorRef, store.PreconditionFrom(latestActor), func(toUpdate *ateapipb.Actor) error { toUpdate.Status.State = ateapipb.ActorState_ACTOR_STATE_RUNNING + // Recorded at sprint start so the next resume can detect a repointed + // template by UID; the snapshot it restores was taken under this one. + toUpdate.Status.CurrentActorTemplate = &ateapipb.ObjectRef{ + Atespace: actorTemplate.GetMetadata().GetAtespace(), + Name: actorTemplate.GetMetadata().GetName(), + } + toUpdate.Status.CurrentActorTemplateUid = actorTemplate.GetMetadata().GetUid() return nil }) if err != nil { diff --git a/cmd/ateapi/internal/controlapi/workflow_resume_test.go b/cmd/ateapi/internal/controlapi/workflow_resume_test.go index dc129e0fe7..98dee2bc7d 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume_test.go @@ -93,6 +93,41 @@ func TestResumeActor_RunningFastPathDoesNotAcquireLease(t *testing.T) { } } +// TestFinalizeRunning_RecordsSprintTemplate verifies committing RUNNING stamps +// the template the sprint booted with, overwriting the previous sprint's +// record, so the next resume can detect a repointed template by UID. +func TestFinalizeRunning_RecordsSprintTemplate(t *testing.T) { + ctx := context.Background() + persistence := newTestPersistence(t) + storetest.MustCreateActor(t, ctx, persistence, &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "id1"}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl-2"}, + Status: &ateapipb.ActorStatus{ + State: ateapipb.ActorState_ACTOR_STATE_RESUMING, + CurrentActorTemplate: &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl-1"}, + CurrentActorTemplateUid: "tmpl-uid-1", + }, + }) + w := &ActorWorkflow{store: persistence} + + got, err := w.finalizeRunning(ctx, resources.ActorRef{Atespace: "team-a", Name: "id1"}, &ateapipb.ActorTemplate{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "tmpl-2", Uid: "tmpl-uid-2"}, + }) + if err != nil { + t.Fatalf("finalizeRunning: %v", err) + } + if got.GetStatus().GetState() != ateapipb.ActorState_ACTOR_STATE_RUNNING { + t.Errorf("state = %v, want RUNNING", got.GetStatus().GetState()) + } + want := &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl-2"} + if ref := got.GetStatus().GetCurrentActorTemplate(); !proto.Equal(ref, want) { + t.Errorf("CurrentActorTemplate = %v, want %v", ref, want) + } + if uid := got.GetStatus().GetCurrentActorTemplateUid(); uid != "tmpl-uid-2" { + t.Errorf("CurrentActorTemplateUid = %q, want %q", uid, "tmpl-uid-2") + } +} + type updateWorkerErrorStore struct { store.Interface err error diff --git a/cmd/ateapi/internal/controlapi/zz_generated.validation.go b/cmd/ateapi/internal/controlapi/zz_generated.validation.go index 0d5d7dd3a0..4fffe646e4 100644 --- a/cmd/ateapi/internal/controlapi/zz_generated.validation.go +++ b/cmd/ateapi/internal/controlapi/zz_generated.validation.go @@ -920,6 +920,64 @@ func Validate_ActorStatus( errs = append(errs, fn(fldPath.Child("source_snapshot"), obj.SourceSnapshot, oldVal, oldObj != nil)...) } + { // field ateapipb.ActorStatus.CurrentActorTemplate + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.ObjectRef, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_ObjectRef(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ActorStatus) *ateapipb.ObjectRef { + return oldObj.CurrentActorTemplate + }) + errs = append(errs, fn(fldPath.Child("current_actor_template"), obj.CurrentActorTemplate, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ActorStatus.CurrentActorTemplateUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ActorStatus) *string { + return &oldObj.CurrentActorTemplateUid + }) + errs = append(errs, fn(fldPath.Child("current_actor_template_uid"), &obj.CurrentActorTemplateUid, oldVal, oldObj != nil)...) + } + return errs } diff --git a/internal/e2e/suites/demo/updatetemplate_test.go b/internal/e2e/suites/demo/updatetemplate_test.go new file mode 100644 index 0000000000..52d346ff1a --- /dev/null +++ b/internal/e2e/suites/demo/updatetemplate_test.go @@ -0,0 +1,231 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package demo + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/agent-substrate/substrate/internal/e2e" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" +) + +// TestUpdateTemplateLifecycle covers repointing a suspended actor at a +// different ActorTemplate: the actor runs and writes to its durable-dir data +// volume under template A, suspends, is repointed at template B via +// UpdateActor, and resumes. The resume must detect the template change (the +// recorded current_actor_template_uid no longer matches) and restore +// data-only: the durable dir survives while the guest cold-boots from +// template B. +func TestUpdateTemplateLifecycle(t *testing.T) { + tests := []struct { + name string + onCommit ateapipb.SnapshotContentScope + }{ + { + name: "onCommit:Data", + onCommit: ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA, + }, + { + name: "onCommit:Full", + onCommit: ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_FULL, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + runUpdateTemplateTestCase(t, test.onCommit) + }) + } +} + +func runUpdateTemplateTestCase(t *testing.T, onCommit ateapipb.SnapshotContentScope) { + nsObj := e2e.CreateNamespace(t) + ctx := context.Background() + clients := e2e.GetClients() + + env, err := e2e.CheckEnv("BUCKET_NAME") + if err != nil { + t.Fatalf("CheckEnv failed: %v", err) + } + + // Template A: the plain counter. Template B: the same workload, but its + // command additionally validates that the file template A's sprint left + // in the durable dir is readable — the response's "file content" both + // proves B's spec took effect and re-reads the preserved data. + nameA, nameB := "update-a-"+nsObj.Name, "update-b-"+nsObj.Name + createdA := createUpdateTestTemplate(ctx, t, clients, nsObj, nameA, "update-a", env["BUCKET_NAME"], onCommit, nil) + createdB := createUpdateTestTemplate(ctx, t, clients, nsObj, nameB, "update-b", env["BUCKET_NAME"], onCommit, func(tmpl *ateapipb.ActorTemplate) { + ctr := tmpl.Containers[0] + ctr.Command = append(ctr.Command, "--validate-existing-file-path=/home/counter/a.txt") + }) + + // + // Create an Actor from template A; a fresh actor sits SUSPENDED. + // + actorID := "update-" + nsObj.Name + refA := &ateapipb.ObjectRef{Atespace: demoAtespace, Name: nameA} + refB := &ateapipb.ObjectRef{Atespace: demoAtespace, Name: nameB} + + t.Logf("Creating Actor %q from substrate template %q...", actorID, nameA) + createResp, err := clients.SubstrateAPI.CreateActor(ctx, &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: demoAtespace, Name: actorID}, + ActorTemplate: refA, + }}) + if err != nil { + t.Fatalf("failed to create Actor: %v", err) + } + defer func() { + clients.SubstrateAPI.DeleteActor(ctx, &ateapipb.DeleteActorRequest{ + Actor: &ateapipb.ObjectRef{Atespace: demoAtespace, Name: actorID}, + }) + }() + if got := createResp.GetStatus().GetState(); got != ateapipb.ActorState_ACTOR_STATE_SUSPENDED { + t.Fatalf("created Actor state = %v, want SUSPENDED", got) + } + + // Run under template A and write to the data volume (every call bumps the + // file counter the workload keeps in the durable dir). + t.Logf("Resuming Actor %q under template A...", actorID) + if _, err := e2e.ResumeActorAwaitCapacity(t, ctx, clients, &ateapipb.ResumeActorRequest{ + Actor: &ateapipb.ObjectRef{Atespace: demoAtespace, Name: actorID}, + }); err != nil { + t.Fatalf("failed to resume Actor: %v", err) + } + // The first resume also absorbs the freshly created pool's worker + // startup, so it gets a longer budget than the steady-state waits. + waitForActorStateWithTimeout(ctx, t, clients, actorID, ateapipb.ActorState_ACTOR_STATE_RUNNING, 120*time.Second) + + for i := 1; i <= 2; i++ { + resp, err := callActor(t, resources.ActorRef{Atespace: demoAtespace, Name: actorID}) + if err != nil { + t.Fatalf("failed to call actor (call %d): %v", i, err) + } + validateCounterResponse(t, resp, "under template A", i, i) + } + + // + // Suspend; the record of the template the sprint booted with (stamped by + // the resume above) must survive the suspend. + // + t.Logf("Suspending Actor %q...", actorID) + if _, err := clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{ + Actor: &ateapipb.ObjectRef{Atespace: demoAtespace, Name: actorID}, + }); err != nil { + t.Fatalf("failed to suspend Actor: %v", err) + } + waitForActorState(ctx, t, clients, actorID, ateapipb.ActorState_ACTOR_STATE_SUSPENDED) + + suspended, err := clients.SubstrateAPI.GetActor(ctx, &ateapipb.GetActorRequest{ + Actor: &ateapipb.ObjectRef{Atespace: demoAtespace, Name: actorID}, + }) + if err != nil { + t.Fatalf("failed to get suspended Actor: %v", err) + } + if got, want := suspended.GetStatus().GetCurrentActorTemplateUid(), createdA.GetMetadata().GetUid(); got != want { + t.Errorf("suspended Actor current_actor_template_uid = %q, want template A's %q", got, want) + } + if got := suspended.GetStatus().GetCurrentActorTemplate().GetName(); got != nameA { + t.Errorf("suspended Actor current_actor_template = %q, want %q", got, nameA) + } + if suspended.GetStatus().GetLatestSnapshot() == nil { + t.Error("suspended Actor has no latest snapshot") + } + if wa := suspended.GetStatus().GetWorkerAssignment(); wa != nil { + t.Errorf("suspended Actor still carries a worker assignment: %v", wa) + } + + // + // Repoint the suspended actor at template B. + // + t.Logf("Repointing Actor %q at template %q...", actorID, nameB) + updated, err := clients.SubstrateAPI.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ + Metadata: suspended.GetMetadata(), + ActorTemplate: refB, + }}) + if err != nil { + t.Fatalf("failed to update Actor's template: %v", err) + } + if got := updated.GetActorTemplate().GetName(); got != nameB { + t.Errorf("updated Actor actor_template = %q, want %q", got, nameB) + } + if got := updated.GetStatus().GetState(); got != ateapipb.ActorState_ACTOR_STATE_SUSPENDED { + t.Errorf("updated Actor state = %v, want SUSPENDED", got) + } + + // + // Resume under template B: the guest cold-boots from B (memory counter + // resets) while the durable dir carries over (file counter continues, + // and B's --validate-existing-file-path reads A's file back). + // + t.Logf("Resuming Actor %q under template B...", actorID) + if _, err := e2e.ResumeActorAwaitCapacity(t, ctx, clients, &ateapipb.ResumeActorRequest{ + Actor: &ateapipb.ObjectRef{Atespace: demoAtespace, Name: actorID}, + }); err != nil { + t.Fatalf("failed to resume Actor after template update: %v", err) + } + waitForActorState(ctx, t, clients, actorID, ateapipb.ActorState_ACTOR_STATE_RUNNING) + + resp, err := callActor(t, resources.ActorRef{Atespace: demoAtespace, Name: actorID}) + if err != nil { + t.Fatalf("failed to call actor after template update: %v", err) + } + validateCounterResponse(t, resp, "after template update", 1, 3) + if want := "file content: 3"; !strings.Contains(resp, want) { + t.Errorf("[after template update] expected %q (template B validating the preserved file), got response: %s", want, resp) + } + + // A second suspend closes the loop: the resume under B stamped B as the + // sprint's template, and the suspend preserves it. + t.Logf("Suspending Actor %q again...", actorID) + if _, err := clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{ + Actor: &ateapipb.ObjectRef{Atespace: demoAtespace, Name: actorID}, + }); err != nil { + t.Fatalf("failed to suspend Actor again: %v", err) + } + waitForActorState(ctx, t, clients, actorID, ateapipb.ActorState_ACTOR_STATE_SUSPENDED) + suspended, err = clients.SubstrateAPI.GetActor(ctx, &ateapipb.GetActorRequest{ + Actor: &ateapipb.ObjectRef{Atespace: demoAtespace, Name: actorID}, + }) + if err != nil { + t.Fatalf("failed to get re-suspended Actor: %v", err) + } + if got, want := suspended.GetStatus().GetCurrentActorTemplateUid(), createdB.GetMetadata().GetUid(); got != want { + t.Errorf("re-suspended Actor current_actor_template_uid = %q, want template B's %q", got, want) + } +} + +// createUpdateTestTemplate creates a per-test WorkerPool plus a substrate +// ActorTemplate copying the deployed counter fixture's resolved runtime, +// capturing at the scope under test on both pause and commit. +func createUpdateTestTemplate(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj *e2e.Namespace, name, poolName, bucket string, onCommit ateapipb.SnapshotContentScope, modify func(*ateapipb.ActorTemplate)) *ateapipb.ActorTemplate { + t.Helper() + return e2e.CreateSubstrateCounterTemplate(ctx, t, clients, nsObj.Name, e2e.SubstrateTemplateOptions{ + Atespace: demoAtespace, + Name: name, + PoolName: poolName, + PoolReplicas: 2, + Labels: map[string]string{"demo": nsObj.Name}, + SnapshotsConfig: &ateapipb.SnapshotsConfig{ + StorageLocation: "gs://" + bucket + "/ate-demo-" + name, + OnPause: onCommit, + OnCommit: onCommit, + }, + Modify: modify, + }) +} diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 4749f80209..ea5121e4c9 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -1473,8 +1473,20 @@ type ActorStatus struct { // Not immutable: create validates the final object against the request // with op=Update, and immutable rejects that nil->set. SourceSnapshot *ActorSourceSnapshotStatus `protobuf:"bytes,9,opt,name=source_snapshot,json=sourceSnapshot,proto3" json:"source_snapshot,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The actor template the Actor's guest state is currently built on: the + // one its latest sprint booted with, recorded when the resume (or first + // boot) commits RUNNING. Diverges from the Actor's actor_template ref when + // a suspended Actor is repointed at a replacement template. + // + // +k8s:optional + CurrentActorTemplate *ObjectRef `protobuf:"bytes,10,opt,name=current_actor_template,json=currentActorTemplate,proto3" json:"current_actor_template,omitempty"` + // The UID of the actor template the Actor's guest state is currently + // built on, recorded when the resume (or first boot) commits RUNNING. + // + // +k8s:optional + CurrentActorTemplateUid string `protobuf:"bytes,11,opt,name=current_actor_template_uid,json=currentActorTemplateUid,proto3" json:"current_actor_template_uid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ActorStatus) Reset() { @@ -1570,6 +1582,20 @@ func (x *ActorStatus) GetSourceSnapshot() *ActorSourceSnapshotStatus { return nil } +func (x *ActorStatus) GetCurrentActorTemplate() *ObjectRef { + if x != nil { + return x.CurrentActorTemplate + } + return nil +} + +func (x *ActorStatus) GetCurrentActorTemplateUid() string { + if x != nil { + return x.CurrentActorTemplateUid + } + return "" +} + // ActorSourceSnapshotStatus records what an Actor's source_snapshot_tag // resolved to at CreateActor time. Set once, immutable afterward. type ActorSourceSnapshotStatus struct { @@ -6593,7 +6619,7 @@ const file_ateapi_proto_rawDesc = "" + "\x19CredentialHeaderInjection\x12\x16\n" + "\x06header\x18\x01 \x01(\tR\x06header\x12\x16\n" + "\x06prefix\x18\x02 \x01(\tR\x06prefix\x12%\n" + - "\x0ecredential_uri\x18\x03 \x01(\tR\rcredentialUri\"\xe8\x04\n" + + "\x0ecredential_uri\x18\x03 \x01(\tR\rcredentialUri\"\xee\x05\n" + "\vActorStatus\x12(\n" + "\x05state\x18\x01 \x01(\x0e2\x12.ateapi.ActorStateR\x05state\x12E\n" + "\x11worker_assignment\x18\x02 \x01(\v2\x18.ateapi.WorkerAssignmentR\x10workerAssignment\x129\n" + @@ -6603,7 +6629,10 @@ const file_ateapi_proto_rawDesc = "" + ")in_progress_snapshot_source_actor_version\x18\x06 \x01(\x03R$inProgressSnapshotSourceActorVersion\x12;\n" + "\ractor_volumes\x18\a \x03(\v2\x16.ateapi.ExternalVolumeR\factorVolumes\x12D\n" + "\x1fin_progress_local_snapshot_name\x18\b \x01(\tR\x1binProgressLocalSnapshotName\x12J\n" + - "\x0fsource_snapshot\x18\t \x01(\v2!.ateapi.ActorSourceSnapshotStatusR\x0esourceSnapshot\"m\n" + + "\x0fsource_snapshot\x18\t \x01(\v2!.ateapi.ActorSourceSnapshotStatusR\x0esourceSnapshot\x12G\n" + + "\x16current_actor_template\x18\n" + + " \x01(\v2\x11.ateapi.ObjectRefR\x14currentActorTemplate\x12;\n" + + "\x1acurrent_actor_template_uid\x18\v \x01(\tR\x17currentActorTemplateUid\"m\n" + "\x19ActorSourceSnapshotStatus\x12-\n" + "\bsnapshot\x18\x01 \x01(\v2\x11.ateapi.ObjectRefR\bsnapshot\x12!\n" + "\fsnapshot_uid\x18\x02 \x01(\tR\vsnapshotUid\"\xf2\x01\n" + @@ -7101,171 +7130,172 @@ var file_ateapi_proto_depIdxs = []int32{ 9, // 21: ateapi.ActorStatus.local_snapshot_info:type_name -> ateapi.LocalSnapshotInfo 12, // 22: ateapi.ActorStatus.actor_volumes:type_name -> ateapi.ExternalVolume 21, // 23: ateapi.ActorStatus.source_snapshot:type_name -> ateapi.ActorSourceSnapshotStatus - 27, // 24: ateapi.ActorSourceSnapshotStatus.snapshot:type_name -> ateapi.ObjectRef - 27, // 25: ateapi.WorkerAssignment.worker:type_name -> ateapi.ObjectRef - 11, // 26: ateapi.ActorSnapshot.metadata:type_name -> ateapi.ResourceMetadata - 24, // 27: ateapi.ActorSnapshot.status:type_name -> ateapi.ActorSnapshotStatus - 27, // 28: ateapi.ActorSnapshotStatus.source_actor:type_name -> ateapi.ObjectRef - 0, // 29: ateapi.ActorSnapshotStatus.content_scope:type_name -> ateapi.SnapshotContentScope - 27, // 30: ateapi.ActorSnapshotStatus.actor_template:type_name -> ateapi.ObjectRef - 11, // 31: ateapi.ActorSnapshotTag.metadata:type_name -> ateapi.ResourceMetadata - 27, // 32: ateapi.ActorSnapshotTag.snapshot:type_name -> ateapi.ObjectRef - 1, // 33: ateapi.ActorSnapshotTag.scope:type_name -> ateapi.ActorSnapshotTagScope - 11, // 34: ateapi.Atespace.metadata:type_name -> ateapi.ResourceMetadata - 11, // 35: ateapi.ActorTemplate.metadata:type_name -> ateapi.ResourceMetadata - 10, // 36: ateapi.ActorTemplate.worker_selector:type_name -> ateapi.Selector - 36, // 37: ateapi.ActorTemplate.containers:type_name -> ateapi.Container - 42, // 38: ateapi.ActorTemplate.volumes:type_name -> ateapi.Volume - 34, // 39: ateapi.ActorTemplate.snapshots_config:type_name -> ateapi.SnapshotsConfig - 33, // 40: ateapi.ActorTemplate.sandbox_config:type_name -> ateapi.SandboxConfig - 29, // 41: ateapi.ActorTemplate.resources:type_name -> ateapi.Resources - 32, // 42: ateapi.ActorTemplate.status:type_name -> ateapi.ActorTemplateStatus - 30, // 43: ateapi.Resources.limits:type_name -> ateapi.Limits - 27, // 44: ateapi.GoldenSnapshotStatus.golden_snapshot:type_name -> ateapi.ObjectRef - 104, // 45: ateapi.GoldenSnapshotStatus.take_golden_snapshot_at:type_name -> google.protobuf.Timestamp - 31, // 46: ateapi.ActorTemplateStatus.golden_snapshot_status:type_name -> ateapi.GoldenSnapshotStatus - 3, // 47: ateapi.SandboxConfig.sandbox_class:type_name -> ateapi.SandboxClass - 0, // 48: ateapi.SnapshotsConfig.on_pause:type_name -> ateapi.SnapshotContentScope - 0, // 49: ateapi.SnapshotsConfig.on_commit:type_name -> ateapi.SnapshotContentScope - 35, // 50: ateapi.SnapshotsConfig.on_resume:type_name -> ateapi.OnResumeConfig - 4, // 51: ateapi.OnResumeConfig.from_data:type_name -> ateapi.ResumeSource - 39, // 52: ateapi.Container.env:type_name -> ateapi.EnvVar - 40, // 53: ateapi.Container.readyz:type_name -> ateapi.ContainerReadyz - 51, // 54: ateapi.Container.volume_mounts:type_name -> ateapi.VolumeMount - 37, // 55: ateapi.Container.security_context:type_name -> ateapi.SecurityContext - 29, // 56: ateapi.Container.resources:type_name -> ateapi.Resources - 38, // 57: ateapi.SecurityContext.capabilities:type_name -> ateapi.Capabilities - 41, // 58: ateapi.ContainerReadyz.http_get:type_name -> ateapi.HTTPGetAction - 44, // 59: ateapi.Volume.durable_dir:type_name -> ateapi.DurableDirVolumeSource - 45, // 60: ateapi.Volume.external_volume_template:type_name -> ateapi.ExternalVolumeTemplate - 46, // 61: ateapi.Volume.system_info:type_name -> ateapi.SystemInfoVolumeSource - 43, // 62: ateapi.Volume.image:type_name -> ateapi.ImageVolumeSource - 47, // 63: ateapi.SystemInfoVolumeSource.data_sources:type_name -> ateapi.SystemInfoDataSource - 48, // 64: ateapi.SystemInfoDataSource.actor_metadata:type_name -> ateapi.ActorMetadataDataSource - 50, // 65: ateapi.SystemInfoDataSource.trust_bundle:type_name -> ateapi.TrustBundleDataSource - 49, // 66: ateapi.ActorMetadataDataSource.items:type_name -> ateapi.ActorMetadataItem - 5, // 67: ateapi.ActorMetadataItem.field:type_name -> ateapi.ActorMetadataField - 26, // 68: ateapi.CreateAtespaceRequest.atespace:type_name -> ateapi.Atespace - 27, // 69: ateapi.GetAtespaceRequest.atespace:type_name -> ateapi.ObjectRef - 26, // 70: ateapi.ListAtespacesResponse.atespaces:type_name -> ateapi.Atespace - 27, // 71: ateapi.DeleteAtespaceRequest.atespace:type_name -> ateapi.ObjectRef - 28, // 72: ateapi.CreateActorTemplateRequest.actor_template:type_name -> ateapi.ActorTemplate - 27, // 73: ateapi.GetActorTemplateRequest.actor_template:type_name -> ateapi.ObjectRef - 28, // 74: ateapi.ListActorTemplatesResponse.actor_templates:type_name -> ateapi.ActorTemplate - 27, // 75: ateapi.DeleteActorTemplateRequest.actor_template:type_name -> ateapi.ObjectRef - 27, // 76: ateapi.GetActorRequest.actor:type_name -> ateapi.ObjectRef - 13, // 77: ateapi.CreateActorRequest.actor:type_name -> ateapi.Actor - 13, // 78: ateapi.UpdateActorRequest.actor:type_name -> ateapi.Actor - 27, // 79: ateapi.SuspendActorRequest.actor:type_name -> ateapi.ObjectRef - 13, // 80: ateapi.SuspendActorResponse.actor:type_name -> ateapi.Actor - 27, // 81: ateapi.PauseActorRequest.actor:type_name -> ateapi.ObjectRef - 13, // 82: ateapi.PauseActorResponse.actor:type_name -> ateapi.Actor - 27, // 83: ateapi.ResumeActorRequest.actor:type_name -> ateapi.ObjectRef - 13, // 84: ateapi.ResumeActorResponse.actor:type_name -> ateapi.Actor - 27, // 85: ateapi.DeleteActorRequest.actor:type_name -> ateapi.ObjectRef - 27, // 86: ateapi.GetActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef - 27, // 87: ateapi.CreateActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef - 14, // 88: ateapi.CreateActorEgressPolicyRequest.egress_policy:type_name -> ateapi.EgressPolicy - 27, // 89: ateapi.UpdateActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef - 14, // 90: ateapi.UpdateActorEgressPolicyRequest.egress_policy:type_name -> ateapi.EgressPolicy - 27, // 91: ateapi.DeleteActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef - 27, // 92: ateapi.GetActorSnapshotRequest.actor_snapshot:type_name -> ateapi.ObjectRef - 27, // 93: ateapi.GetActorSnapshotTagRequest.actor_snapshot_tag:type_name -> ateapi.ObjectRef - 23, // 94: ateapi.ListActorSnapshotsResponse.actor_snapshots:type_name -> ateapi.ActorSnapshot - 25, // 95: ateapi.CreateActorSnapshotTagRequest.actor_snapshot_tag:type_name -> ateapi.ActorSnapshotTag - 25, // 96: ateapi.UpdateActorSnapshotTagRequest.actor_snapshot_tag:type_name -> ateapi.ActorSnapshotTag - 27, // 97: ateapi.DeleteActorSnapshotTagRequest.actor_snapshot_tag:type_name -> ateapi.ObjectRef - 93, // 98: ateapi.ListWorkersResponse.workers:type_name -> ateapi.Worker - 27, // 99: ateapi.GetWorkerRequest.worker:type_name -> ateapi.ObjectRef - 93, // 100: ateapi.CreateWorkerRequest.worker:type_name -> ateapi.Worker - 93, // 101: ateapi.UpdateWorkerRequest.worker:type_name -> ateapi.Worker - 27, // 102: ateapi.DeleteWorkerRequest.worker:type_name -> ateapi.ObjectRef - 83, // 103: ateapi.DeleteWorkerRequest.options:type_name -> ateapi.DeleteOptions - 27, // 104: ateapi.DrainWorkerRequest.worker:type_name -> ateapi.ObjectRef - 13, // 105: ateapi.ListActorsResponse.actors:type_name -> ateapi.Actor - 11, // 106: ateapi.Worker.metadata:type_name -> ateapi.ResourceMetadata - 103, // 107: ateapi.Worker.labels:type_name -> ateapi.Worker.LabelsEntry - 95, // 108: ateapi.Worker.capacity:type_name -> ateapi.WorkerCapacity - 94, // 109: ateapi.Worker.status:type_name -> ateapi.WorkerStatus - 6, // 110: ateapi.WorkerStatus.state:type_name -> ateapi.WorkerState - 96, // 111: ateapi.WorkerStatus.assignment:type_name -> ateapi.ActorAssignment - 27, // 112: ateapi.ActorAssignment.actor:type_name -> ateapi.ObjectRef - 27, // 113: ateapi.ActorAssignment.actor_template_ref:type_name -> ateapi.ObjectRef - 27, // 114: ateapi.MintCertRequest.worker:type_name -> ateapi.ObjectRef - 7, // 115: ateapi.MintCertRequest.purpose:type_name -> ateapi.ActorCertificatePurpose - 62, // 116: ateapi.Control.GetActor:input_type -> ateapi.GetActorRequest - 63, // 117: ateapi.Control.CreateActor:input_type -> ateapi.CreateActorRequest - 64, // 118: ateapi.Control.UpdateActor:input_type -> ateapi.UpdateActorRequest - 65, // 119: ateapi.Control.SuspendActor:input_type -> ateapi.SuspendActorRequest - 67, // 120: ateapi.Control.PauseActor:input_type -> ateapi.PauseActorRequest - 69, // 121: ateapi.Control.ResumeActor:input_type -> ateapi.ResumeActorRequest - 71, // 122: ateapi.Control.DeleteActor:input_type -> ateapi.DeleteActorRequest - 72, // 123: ateapi.Control.GetActorEgressPolicy:input_type -> ateapi.GetActorEgressPolicyRequest - 73, // 124: ateapi.Control.CreateActorEgressPolicy:input_type -> ateapi.CreateActorEgressPolicyRequest - 74, // 125: ateapi.Control.UpdateActorEgressPolicy:input_type -> ateapi.UpdateActorEgressPolicyRequest - 75, // 126: ateapi.Control.DeleteActorEgressPolicy:input_type -> ateapi.DeleteActorEgressPolicyRequest - 76, // 127: ateapi.Control.GetActorSnapshot:input_type -> ateapi.GetActorSnapshotRequest - 77, // 128: ateapi.Control.GetActorSnapshotTag:input_type -> ateapi.GetActorSnapshotTagRequest - 78, // 129: ateapi.Control.ListActorSnapshots:input_type -> ateapi.ListActorSnapshotsRequest - 80, // 130: ateapi.Control.CreateActorSnapshotTag:input_type -> ateapi.CreateActorSnapshotTagRequest - 81, // 131: ateapi.Control.UpdateActorSnapshotTag:input_type -> ateapi.UpdateActorSnapshotTagRequest - 82, // 132: ateapi.Control.DeleteActorSnapshotTag:input_type -> ateapi.DeleteActorSnapshotTagRequest - 84, // 133: ateapi.Control.ListWorkers:input_type -> ateapi.ListWorkersRequest - 86, // 134: ateapi.Control.GetWorker:input_type -> ateapi.GetWorkerRequest - 87, // 135: ateapi.Control.CreateWorker:input_type -> ateapi.CreateWorkerRequest - 88, // 136: ateapi.Control.UpdateWorker:input_type -> ateapi.UpdateWorkerRequest - 89, // 137: ateapi.Control.DeleteWorker:input_type -> ateapi.DeleteWorkerRequest - 90, // 138: ateapi.Control.DrainWorker:input_type -> ateapi.DrainWorkerRequest - 91, // 139: ateapi.Control.ListActors:input_type -> ateapi.ListActorsRequest - 52, // 140: ateapi.Control.CreateAtespace:input_type -> ateapi.CreateAtespaceRequest - 53, // 141: ateapi.Control.GetAtespace:input_type -> ateapi.GetAtespaceRequest - 54, // 142: ateapi.Control.ListAtespaces:input_type -> ateapi.ListAtespacesRequest - 56, // 143: ateapi.Control.DeleteAtespace:input_type -> ateapi.DeleteAtespaceRequest - 57, // 144: ateapi.Control.CreateActorTemplate:input_type -> ateapi.CreateActorTemplateRequest - 58, // 145: ateapi.Control.GetActorTemplate:input_type -> ateapi.GetActorTemplateRequest - 59, // 146: ateapi.Control.ListActorTemplates:input_type -> ateapi.ListActorTemplatesRequest - 61, // 147: ateapi.Control.DeleteActorTemplate:input_type -> ateapi.DeleteActorTemplateRequest - 97, // 148: ateapi.ActorIdentity.MintJWT:input_type -> ateapi.MintJWTRequest - 99, // 149: ateapi.ActorIdentity.MintCert:input_type -> ateapi.MintCertRequest - 13, // 150: ateapi.Control.GetActor:output_type -> ateapi.Actor - 13, // 151: ateapi.Control.CreateActor:output_type -> ateapi.Actor - 13, // 152: ateapi.Control.UpdateActor:output_type -> ateapi.Actor - 66, // 153: ateapi.Control.SuspendActor:output_type -> ateapi.SuspendActorResponse - 68, // 154: ateapi.Control.PauseActor:output_type -> ateapi.PauseActorResponse - 70, // 155: ateapi.Control.ResumeActor:output_type -> ateapi.ResumeActorResponse - 13, // 156: ateapi.Control.DeleteActor:output_type -> ateapi.Actor - 14, // 157: ateapi.Control.GetActorEgressPolicy:output_type -> ateapi.EgressPolicy - 14, // 158: ateapi.Control.CreateActorEgressPolicy:output_type -> ateapi.EgressPolicy - 14, // 159: ateapi.Control.UpdateActorEgressPolicy:output_type -> ateapi.EgressPolicy - 14, // 160: ateapi.Control.DeleteActorEgressPolicy:output_type -> ateapi.EgressPolicy - 23, // 161: ateapi.Control.GetActorSnapshot:output_type -> ateapi.ActorSnapshot - 25, // 162: ateapi.Control.GetActorSnapshotTag:output_type -> ateapi.ActorSnapshotTag - 79, // 163: ateapi.Control.ListActorSnapshots:output_type -> ateapi.ListActorSnapshotsResponse - 25, // 164: ateapi.Control.CreateActorSnapshotTag:output_type -> ateapi.ActorSnapshotTag - 25, // 165: ateapi.Control.UpdateActorSnapshotTag:output_type -> ateapi.ActorSnapshotTag - 25, // 166: ateapi.Control.DeleteActorSnapshotTag:output_type -> ateapi.ActorSnapshotTag - 85, // 167: ateapi.Control.ListWorkers:output_type -> ateapi.ListWorkersResponse - 93, // 168: ateapi.Control.GetWorker:output_type -> ateapi.Worker - 93, // 169: ateapi.Control.CreateWorker:output_type -> ateapi.Worker - 93, // 170: ateapi.Control.UpdateWorker:output_type -> ateapi.Worker - 93, // 171: ateapi.Control.DeleteWorker:output_type -> ateapi.Worker - 93, // 172: ateapi.Control.DrainWorker:output_type -> ateapi.Worker - 92, // 173: ateapi.Control.ListActors:output_type -> ateapi.ListActorsResponse - 26, // 174: ateapi.Control.CreateAtespace:output_type -> ateapi.Atespace - 26, // 175: ateapi.Control.GetAtespace:output_type -> ateapi.Atespace - 55, // 176: ateapi.Control.ListAtespaces:output_type -> ateapi.ListAtespacesResponse - 26, // 177: ateapi.Control.DeleteAtespace:output_type -> ateapi.Atespace - 28, // 178: ateapi.Control.CreateActorTemplate:output_type -> ateapi.ActorTemplate - 28, // 179: ateapi.Control.GetActorTemplate:output_type -> ateapi.ActorTemplate - 60, // 180: ateapi.Control.ListActorTemplates:output_type -> ateapi.ListActorTemplatesResponse - 28, // 181: ateapi.Control.DeleteActorTemplate:output_type -> ateapi.ActorTemplate - 98, // 182: ateapi.ActorIdentity.MintJWT:output_type -> ateapi.MintJWTResponse - 100, // 183: ateapi.ActorIdentity.MintCert:output_type -> ateapi.MintCertResponse - 150, // [150:184] is the sub-list for method output_type - 116, // [116:150] is the sub-list for method input_type - 116, // [116:116] is the sub-list for extension type_name - 116, // [116:116] is the sub-list for extension extendee - 0, // [0:116] is the sub-list for field type_name + 27, // 24: ateapi.ActorStatus.current_actor_template:type_name -> ateapi.ObjectRef + 27, // 25: ateapi.ActorSourceSnapshotStatus.snapshot:type_name -> ateapi.ObjectRef + 27, // 26: ateapi.WorkerAssignment.worker:type_name -> ateapi.ObjectRef + 11, // 27: ateapi.ActorSnapshot.metadata:type_name -> ateapi.ResourceMetadata + 24, // 28: ateapi.ActorSnapshot.status:type_name -> ateapi.ActorSnapshotStatus + 27, // 29: ateapi.ActorSnapshotStatus.source_actor:type_name -> ateapi.ObjectRef + 0, // 30: ateapi.ActorSnapshotStatus.content_scope:type_name -> ateapi.SnapshotContentScope + 27, // 31: ateapi.ActorSnapshotStatus.actor_template:type_name -> ateapi.ObjectRef + 11, // 32: ateapi.ActorSnapshotTag.metadata:type_name -> ateapi.ResourceMetadata + 27, // 33: ateapi.ActorSnapshotTag.snapshot:type_name -> ateapi.ObjectRef + 1, // 34: ateapi.ActorSnapshotTag.scope:type_name -> ateapi.ActorSnapshotTagScope + 11, // 35: ateapi.Atespace.metadata:type_name -> ateapi.ResourceMetadata + 11, // 36: ateapi.ActorTemplate.metadata:type_name -> ateapi.ResourceMetadata + 10, // 37: ateapi.ActorTemplate.worker_selector:type_name -> ateapi.Selector + 36, // 38: ateapi.ActorTemplate.containers:type_name -> ateapi.Container + 42, // 39: ateapi.ActorTemplate.volumes:type_name -> ateapi.Volume + 34, // 40: ateapi.ActorTemplate.snapshots_config:type_name -> ateapi.SnapshotsConfig + 33, // 41: ateapi.ActorTemplate.sandbox_config:type_name -> ateapi.SandboxConfig + 29, // 42: ateapi.ActorTemplate.resources:type_name -> ateapi.Resources + 32, // 43: ateapi.ActorTemplate.status:type_name -> ateapi.ActorTemplateStatus + 30, // 44: ateapi.Resources.limits:type_name -> ateapi.Limits + 27, // 45: ateapi.GoldenSnapshotStatus.golden_snapshot:type_name -> ateapi.ObjectRef + 104, // 46: ateapi.GoldenSnapshotStatus.take_golden_snapshot_at:type_name -> google.protobuf.Timestamp + 31, // 47: ateapi.ActorTemplateStatus.golden_snapshot_status:type_name -> ateapi.GoldenSnapshotStatus + 3, // 48: ateapi.SandboxConfig.sandbox_class:type_name -> ateapi.SandboxClass + 0, // 49: ateapi.SnapshotsConfig.on_pause:type_name -> ateapi.SnapshotContentScope + 0, // 50: ateapi.SnapshotsConfig.on_commit:type_name -> ateapi.SnapshotContentScope + 35, // 51: ateapi.SnapshotsConfig.on_resume:type_name -> ateapi.OnResumeConfig + 4, // 52: ateapi.OnResumeConfig.from_data:type_name -> ateapi.ResumeSource + 39, // 53: ateapi.Container.env:type_name -> ateapi.EnvVar + 40, // 54: ateapi.Container.readyz:type_name -> ateapi.ContainerReadyz + 51, // 55: ateapi.Container.volume_mounts:type_name -> ateapi.VolumeMount + 37, // 56: ateapi.Container.security_context:type_name -> ateapi.SecurityContext + 29, // 57: ateapi.Container.resources:type_name -> ateapi.Resources + 38, // 58: ateapi.SecurityContext.capabilities:type_name -> ateapi.Capabilities + 41, // 59: ateapi.ContainerReadyz.http_get:type_name -> ateapi.HTTPGetAction + 44, // 60: ateapi.Volume.durable_dir:type_name -> ateapi.DurableDirVolumeSource + 45, // 61: ateapi.Volume.external_volume_template:type_name -> ateapi.ExternalVolumeTemplate + 46, // 62: ateapi.Volume.system_info:type_name -> ateapi.SystemInfoVolumeSource + 43, // 63: ateapi.Volume.image:type_name -> ateapi.ImageVolumeSource + 47, // 64: ateapi.SystemInfoVolumeSource.data_sources:type_name -> ateapi.SystemInfoDataSource + 48, // 65: ateapi.SystemInfoDataSource.actor_metadata:type_name -> ateapi.ActorMetadataDataSource + 50, // 66: ateapi.SystemInfoDataSource.trust_bundle:type_name -> ateapi.TrustBundleDataSource + 49, // 67: ateapi.ActorMetadataDataSource.items:type_name -> ateapi.ActorMetadataItem + 5, // 68: ateapi.ActorMetadataItem.field:type_name -> ateapi.ActorMetadataField + 26, // 69: ateapi.CreateAtespaceRequest.atespace:type_name -> ateapi.Atespace + 27, // 70: ateapi.GetAtespaceRequest.atespace:type_name -> ateapi.ObjectRef + 26, // 71: ateapi.ListAtespacesResponse.atespaces:type_name -> ateapi.Atespace + 27, // 72: ateapi.DeleteAtespaceRequest.atespace:type_name -> ateapi.ObjectRef + 28, // 73: ateapi.CreateActorTemplateRequest.actor_template:type_name -> ateapi.ActorTemplate + 27, // 74: ateapi.GetActorTemplateRequest.actor_template:type_name -> ateapi.ObjectRef + 28, // 75: ateapi.ListActorTemplatesResponse.actor_templates:type_name -> ateapi.ActorTemplate + 27, // 76: ateapi.DeleteActorTemplateRequest.actor_template:type_name -> ateapi.ObjectRef + 27, // 77: ateapi.GetActorRequest.actor:type_name -> ateapi.ObjectRef + 13, // 78: ateapi.CreateActorRequest.actor:type_name -> ateapi.Actor + 13, // 79: ateapi.UpdateActorRequest.actor:type_name -> ateapi.Actor + 27, // 80: ateapi.SuspendActorRequest.actor:type_name -> ateapi.ObjectRef + 13, // 81: ateapi.SuspendActorResponse.actor:type_name -> ateapi.Actor + 27, // 82: ateapi.PauseActorRequest.actor:type_name -> ateapi.ObjectRef + 13, // 83: ateapi.PauseActorResponse.actor:type_name -> ateapi.Actor + 27, // 84: ateapi.ResumeActorRequest.actor:type_name -> ateapi.ObjectRef + 13, // 85: ateapi.ResumeActorResponse.actor:type_name -> ateapi.Actor + 27, // 86: ateapi.DeleteActorRequest.actor:type_name -> ateapi.ObjectRef + 27, // 87: ateapi.GetActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef + 27, // 88: ateapi.CreateActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef + 14, // 89: ateapi.CreateActorEgressPolicyRequest.egress_policy:type_name -> ateapi.EgressPolicy + 27, // 90: ateapi.UpdateActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef + 14, // 91: ateapi.UpdateActorEgressPolicyRequest.egress_policy:type_name -> ateapi.EgressPolicy + 27, // 92: ateapi.DeleteActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef + 27, // 93: ateapi.GetActorSnapshotRequest.actor_snapshot:type_name -> ateapi.ObjectRef + 27, // 94: ateapi.GetActorSnapshotTagRequest.actor_snapshot_tag:type_name -> ateapi.ObjectRef + 23, // 95: ateapi.ListActorSnapshotsResponse.actor_snapshots:type_name -> ateapi.ActorSnapshot + 25, // 96: ateapi.CreateActorSnapshotTagRequest.actor_snapshot_tag:type_name -> ateapi.ActorSnapshotTag + 25, // 97: ateapi.UpdateActorSnapshotTagRequest.actor_snapshot_tag:type_name -> ateapi.ActorSnapshotTag + 27, // 98: ateapi.DeleteActorSnapshotTagRequest.actor_snapshot_tag:type_name -> ateapi.ObjectRef + 93, // 99: ateapi.ListWorkersResponse.workers:type_name -> ateapi.Worker + 27, // 100: ateapi.GetWorkerRequest.worker:type_name -> ateapi.ObjectRef + 93, // 101: ateapi.CreateWorkerRequest.worker:type_name -> ateapi.Worker + 93, // 102: ateapi.UpdateWorkerRequest.worker:type_name -> ateapi.Worker + 27, // 103: ateapi.DeleteWorkerRequest.worker:type_name -> ateapi.ObjectRef + 83, // 104: ateapi.DeleteWorkerRequest.options:type_name -> ateapi.DeleteOptions + 27, // 105: ateapi.DrainWorkerRequest.worker:type_name -> ateapi.ObjectRef + 13, // 106: ateapi.ListActorsResponse.actors:type_name -> ateapi.Actor + 11, // 107: ateapi.Worker.metadata:type_name -> ateapi.ResourceMetadata + 103, // 108: ateapi.Worker.labels:type_name -> ateapi.Worker.LabelsEntry + 95, // 109: ateapi.Worker.capacity:type_name -> ateapi.WorkerCapacity + 94, // 110: ateapi.Worker.status:type_name -> ateapi.WorkerStatus + 6, // 111: ateapi.WorkerStatus.state:type_name -> ateapi.WorkerState + 96, // 112: ateapi.WorkerStatus.assignment:type_name -> ateapi.ActorAssignment + 27, // 113: ateapi.ActorAssignment.actor:type_name -> ateapi.ObjectRef + 27, // 114: ateapi.ActorAssignment.actor_template_ref:type_name -> ateapi.ObjectRef + 27, // 115: ateapi.MintCertRequest.worker:type_name -> ateapi.ObjectRef + 7, // 116: ateapi.MintCertRequest.purpose:type_name -> ateapi.ActorCertificatePurpose + 62, // 117: ateapi.Control.GetActor:input_type -> ateapi.GetActorRequest + 63, // 118: ateapi.Control.CreateActor:input_type -> ateapi.CreateActorRequest + 64, // 119: ateapi.Control.UpdateActor:input_type -> ateapi.UpdateActorRequest + 65, // 120: ateapi.Control.SuspendActor:input_type -> ateapi.SuspendActorRequest + 67, // 121: ateapi.Control.PauseActor:input_type -> ateapi.PauseActorRequest + 69, // 122: ateapi.Control.ResumeActor:input_type -> ateapi.ResumeActorRequest + 71, // 123: ateapi.Control.DeleteActor:input_type -> ateapi.DeleteActorRequest + 72, // 124: ateapi.Control.GetActorEgressPolicy:input_type -> ateapi.GetActorEgressPolicyRequest + 73, // 125: ateapi.Control.CreateActorEgressPolicy:input_type -> ateapi.CreateActorEgressPolicyRequest + 74, // 126: ateapi.Control.UpdateActorEgressPolicy:input_type -> ateapi.UpdateActorEgressPolicyRequest + 75, // 127: ateapi.Control.DeleteActorEgressPolicy:input_type -> ateapi.DeleteActorEgressPolicyRequest + 76, // 128: ateapi.Control.GetActorSnapshot:input_type -> ateapi.GetActorSnapshotRequest + 77, // 129: ateapi.Control.GetActorSnapshotTag:input_type -> ateapi.GetActorSnapshotTagRequest + 78, // 130: ateapi.Control.ListActorSnapshots:input_type -> ateapi.ListActorSnapshotsRequest + 80, // 131: ateapi.Control.CreateActorSnapshotTag:input_type -> ateapi.CreateActorSnapshotTagRequest + 81, // 132: ateapi.Control.UpdateActorSnapshotTag:input_type -> ateapi.UpdateActorSnapshotTagRequest + 82, // 133: ateapi.Control.DeleteActorSnapshotTag:input_type -> ateapi.DeleteActorSnapshotTagRequest + 84, // 134: ateapi.Control.ListWorkers:input_type -> ateapi.ListWorkersRequest + 86, // 135: ateapi.Control.GetWorker:input_type -> ateapi.GetWorkerRequest + 87, // 136: ateapi.Control.CreateWorker:input_type -> ateapi.CreateWorkerRequest + 88, // 137: ateapi.Control.UpdateWorker:input_type -> ateapi.UpdateWorkerRequest + 89, // 138: ateapi.Control.DeleteWorker:input_type -> ateapi.DeleteWorkerRequest + 90, // 139: ateapi.Control.DrainWorker:input_type -> ateapi.DrainWorkerRequest + 91, // 140: ateapi.Control.ListActors:input_type -> ateapi.ListActorsRequest + 52, // 141: ateapi.Control.CreateAtespace:input_type -> ateapi.CreateAtespaceRequest + 53, // 142: ateapi.Control.GetAtespace:input_type -> ateapi.GetAtespaceRequest + 54, // 143: ateapi.Control.ListAtespaces:input_type -> ateapi.ListAtespacesRequest + 56, // 144: ateapi.Control.DeleteAtespace:input_type -> ateapi.DeleteAtespaceRequest + 57, // 145: ateapi.Control.CreateActorTemplate:input_type -> ateapi.CreateActorTemplateRequest + 58, // 146: ateapi.Control.GetActorTemplate:input_type -> ateapi.GetActorTemplateRequest + 59, // 147: ateapi.Control.ListActorTemplates:input_type -> ateapi.ListActorTemplatesRequest + 61, // 148: ateapi.Control.DeleteActorTemplate:input_type -> ateapi.DeleteActorTemplateRequest + 97, // 149: ateapi.ActorIdentity.MintJWT:input_type -> ateapi.MintJWTRequest + 99, // 150: ateapi.ActorIdentity.MintCert:input_type -> ateapi.MintCertRequest + 13, // 151: ateapi.Control.GetActor:output_type -> ateapi.Actor + 13, // 152: ateapi.Control.CreateActor:output_type -> ateapi.Actor + 13, // 153: ateapi.Control.UpdateActor:output_type -> ateapi.Actor + 66, // 154: ateapi.Control.SuspendActor:output_type -> ateapi.SuspendActorResponse + 68, // 155: ateapi.Control.PauseActor:output_type -> ateapi.PauseActorResponse + 70, // 156: ateapi.Control.ResumeActor:output_type -> ateapi.ResumeActorResponse + 13, // 157: ateapi.Control.DeleteActor:output_type -> ateapi.Actor + 14, // 158: ateapi.Control.GetActorEgressPolicy:output_type -> ateapi.EgressPolicy + 14, // 159: ateapi.Control.CreateActorEgressPolicy:output_type -> ateapi.EgressPolicy + 14, // 160: ateapi.Control.UpdateActorEgressPolicy:output_type -> ateapi.EgressPolicy + 14, // 161: ateapi.Control.DeleteActorEgressPolicy:output_type -> ateapi.EgressPolicy + 23, // 162: ateapi.Control.GetActorSnapshot:output_type -> ateapi.ActorSnapshot + 25, // 163: ateapi.Control.GetActorSnapshotTag:output_type -> ateapi.ActorSnapshotTag + 79, // 164: ateapi.Control.ListActorSnapshots:output_type -> ateapi.ListActorSnapshotsResponse + 25, // 165: ateapi.Control.CreateActorSnapshotTag:output_type -> ateapi.ActorSnapshotTag + 25, // 166: ateapi.Control.UpdateActorSnapshotTag:output_type -> ateapi.ActorSnapshotTag + 25, // 167: ateapi.Control.DeleteActorSnapshotTag:output_type -> ateapi.ActorSnapshotTag + 85, // 168: ateapi.Control.ListWorkers:output_type -> ateapi.ListWorkersResponse + 93, // 169: ateapi.Control.GetWorker:output_type -> ateapi.Worker + 93, // 170: ateapi.Control.CreateWorker:output_type -> ateapi.Worker + 93, // 171: ateapi.Control.UpdateWorker:output_type -> ateapi.Worker + 93, // 172: ateapi.Control.DeleteWorker:output_type -> ateapi.Worker + 93, // 173: ateapi.Control.DrainWorker:output_type -> ateapi.Worker + 92, // 174: ateapi.Control.ListActors:output_type -> ateapi.ListActorsResponse + 26, // 175: ateapi.Control.CreateAtespace:output_type -> ateapi.Atespace + 26, // 176: ateapi.Control.GetAtespace:output_type -> ateapi.Atespace + 55, // 177: ateapi.Control.ListAtespaces:output_type -> ateapi.ListAtespacesResponse + 26, // 178: ateapi.Control.DeleteAtespace:output_type -> ateapi.Atespace + 28, // 179: ateapi.Control.CreateActorTemplate:output_type -> ateapi.ActorTemplate + 28, // 180: ateapi.Control.GetActorTemplate:output_type -> ateapi.ActorTemplate + 60, // 181: ateapi.Control.ListActorTemplates:output_type -> ateapi.ListActorTemplatesResponse + 28, // 182: ateapi.Control.DeleteActorTemplate:output_type -> ateapi.ActorTemplate + 98, // 183: ateapi.ActorIdentity.MintJWT:output_type -> ateapi.MintJWTResponse + 100, // 184: ateapi.ActorIdentity.MintCert:output_type -> ateapi.MintCertResponse + 151, // [151:185] is the sub-list for method output_type + 117, // [117:151] is the sub-list for method input_type + 117, // [117:117] is the sub-list for extension type_name + 117, // [117:117] is the sub-list for extension extendee + 0, // [0:117] is the sub-list for field type_name } func init() { file_ateapi_proto_init() } diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index d5ac215bf7..ed94c0aac1 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -564,6 +564,20 @@ message ActorStatus { // Not immutable: create validates the final object against the request // with op=Update, and immutable rejects that nil->set. ActorSourceSnapshotStatus source_snapshot = 9; + + // The actor template the Actor's guest state is currently built on: the + // one its latest sprint booted with, recorded when the resume (or first + // boot) commits RUNNING. Diverges from the Actor's actor_template ref when + // a suspended Actor is repointed at a replacement template. + // + // +k8s:optional + ObjectRef current_actor_template = 10; + + // The UID of the actor template the Actor's guest state is currently + // built on, recorded when the resume (or first boot) commits RUNNING. + // + // +k8s:optional + string current_actor_template_uid = 11; } // ActorSourceSnapshotStatus records what an Actor's source_snapshot_tag From 7b49bccd5d42e9bed381e3c3b7800e33180cda7c Mon Sep 17 00:00:00 2001 From: zoezhao Date: Tue, 1 Sep 2026 15:44:04 -0700 Subject: [PATCH 2/9] ateapi: update TestResumeActor for the current-template status fields ResumeActor now records the template it restored from in status.current_actor_template and status.current_actor_template_uid, so the resume functional test must expect them in the GetActor response. --- cmd/ateapi/internal/controlapi/functionaltest/actor_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go index d8a07846a1..28567d0e52 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go @@ -1688,7 +1688,7 @@ func TestResumeActor(t *testing.T) { tc := setupTest(t, ns) defer tc.cleanup() - createTemplate(t, tc, ns) + tmpl := createTemplate(t, tc, ns) podUID := createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") @@ -1722,7 +1722,9 @@ func TestResumeActor(t *testing.T) { Metadata: &ateapipb.ResourceMetadata{Name: name, Atespace: testAtespace}, ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"}, Status: &ateapipb.ActorStatus{ - State: ateapipb.ActorState_ACTOR_STATE_RUNNING, + State: ateapipb.ActorState_ACTOR_STATE_RUNNING, + CurrentActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"}, + CurrentActorTemplateUid: tmpl.GetMetadata().GetUid(), WorkerAssignment: &ateapipb.WorkerAssignment{ Worker: &ateapipb.ObjectRef{Name: podUID}, WorkerNamespace: ns, From 9ba154c4656328c9cee9a6e2030ecd3ac82df9e1 Mon Sep 17 00:00:00 2001 From: zoezhao Date: Tue, 1 Sep 2026 16:43:54 -0700 Subject: [PATCH 3/9] Add validation to compare new and old actor templates --- cmd/ateapi/internal/controlapi/actor.go | 57 +++++- cmd/ateapi/internal/controlapi/actor_test.go | 183 +++++++++++++++++- .../controlapi/functionaltest/actor_test.go | 119 +++++++----- 3 files changed, 303 insertions(+), 56 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/actor.go b/cmd/ateapi/internal/controlapi/actor.go index 31db3bfe8b..517432fc6c 100644 --- a/cmd/ateapi/internal/controlapi/actor.go +++ b/cmd/ateapi/internal/controlapi/actor.go @@ -18,6 +18,7 @@ import ( "context" "errors" "fmt" + "slices" "time" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" @@ -277,11 +278,25 @@ func (s *ServiceImpl) UpdateActor(ctx context.Context, actorRef resources.ActorR // Do any further work on the resource. - // A repointed template ref must resolve, mirroring CreateActor's check - // (same non-atomicity caveat; resume re-resolves and fails cleanly). + // Update actor template is only allowed while the actor is suspended. + // The repointed ref must also resolve, mirroring CreateActor's + // check (same non-atomicity caveat; resume re-resolves and fails + // cleanly), and the replacement's volumes and volume mounts must match + // the old template's. if !proto.Equal(oldVal.GetActorTemplate(), newVal.GetActorTemplate()) { - // TODO: Add validation that compares the new and previous ActorTemplate. - if _, err := resolveActorTemplate(ctx, s.store, newVal); err != nil { + if state := oldVal.GetStatus().GetState(); state != ateapipb.ActorState_ACTOR_STATE_SUSPENDED { + return status.Errorf(codes.FailedPrecondition, + "actor must be %s to change its actor template (got: %s)", ateapipb.ActorState_ACTOR_STATE_SUSPENDED, state) + } + newTemplate, err := resolveActorTemplate(ctx, s.store, newVal) + if err != nil { + return err + } + oldTemplate, err := resolveActorTemplate(ctx, s.store, oldVal) + if err != nil { + return err + } + if err := validateTemplateVolumesUnchanged(oldTemplate, newTemplate); err != nil { return err } } @@ -311,6 +326,40 @@ func (s *ServiceImpl) UpdateActor(ctx context.Context, actorRef resources.ActorR return storedActor, nil } +// validateTemplateVolumesUnchanged rejects a template repoint that changes +// the template's volumes or any container's volume mounts: an actor's +// snapshot data is laid out per the volumes and mount paths it was captured +// with, so a different layout would restore it to the wrong places. The +// volumes list must be identical, and containers present in both templates +// must keep identical mounts, order included; containers added or removed by +// the new template are unconstrained. +func validateTemplateVolumesUnchanged(oldTemplate, newTemplate *ateapipb.ActorTemplate) error { + if !slices.EqualFunc(oldTemplate.GetVolumes(), newTemplate.GetVolumes(), func(a, b *ateapipb.Volume) bool { + return proto.Equal(a, b) + }) { + return status.Error(codes.FailedPrecondition, + "volumes differ between the current and the new actor template; volumes must be identical to repoint an actor") + } + + newContainers := make(map[string]*ateapipb.Container, len(newTemplate.GetContainers())) + for _, c := range newTemplate.GetContainers() { + newContainers[c.GetName()] = c + } + for _, oldC := range oldTemplate.GetContainers() { + newC, ok := newContainers[oldC.GetName()] + if !ok { + continue + } + if !slices.EqualFunc(oldC.GetVolumeMounts(), newC.GetVolumeMounts(), func(a, b *ateapipb.VolumeMount) bool { + return proto.Equal(a, b) + }) { + return status.Errorf(codes.FailedPrecondition, + "volume mounts of container %q differ between the current and the new actor template; volume mounts must be identical to repoint an actor", oldC.GetName()) + } + } + return nil +} + func validateUpdateActorRequest(ctx context.Context, req *ateapipb.UpdateActorRequest) field.ErrorList { // Call the generated validation. // We model this as a create rather than an update because updates assume diff --git a/cmd/ateapi/internal/controlapi/actor_test.go b/cmd/ateapi/internal/controlapi/actor_test.go index af8b055505..233b3047bd 100644 --- a/cmd/ateapi/internal/controlapi/actor_test.go +++ b/cmd/ateapi/internal/controlapi/actor_test.go @@ -868,18 +868,38 @@ func TestUpdateActor(t *testing.T) { } // TestUpdateActor_RepointTemplate covers the mutable actor_template ref: an -// update may point the actor at a different template (it takes effect on the -// next ResumeActor), but the new ref must resolve. +// update may point a suspended actor at a different template (it takes effect +// on the next ResumeActor), but the actor must be suspended, the new ref must +// resolve, and the replacement's volumes and volume mounts must match the old +// template's. func TestUpdateActor_RepointTemplate(t *testing.T) { ctx := context.Background() persistence, cleanup := storetest.SetupTestStore(t) t.Cleanup(cleanup) storetest.MustCreateAtespace(t, ctx, persistence, testAtespace) - for _, name := range []string{"tmpl-a", "tmpl-b"} { + // tmpl-a and tmpl-b are volume-compatible; tmpl-c mounts the data volume + // elsewhere and tmpl-d declares an extra volume. + dataVolume := &ateapipb.Volume{Name: "data", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} + scratchVolume := &ateapipb.Volume{Name: "scratch", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} + templates := map[string]struct { + mountPath string + volumes []*ateapipb.Volume + }{ + "tmpl-a": {"/data", []*ateapipb.Volume{dataVolume}}, + "tmpl-b": {"/data", []*ateapipb.Volume{dataVolume}}, + "tmpl-c": {"/mnt/data", []*ateapipb.Volume{dataVolume}}, + "tmpl-d": {"/data", []*ateapipb.Volume{dataVolume, scratchVolume}}, + } + for name, tmpl := range templates { if _, err := persistence.CreateActorTemplate(ctx, &ateapipb.ActorTemplate{ - Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: name}, - Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1"}}, + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: name}, + Containers: []*ateapipb.Container{{ + Name: "main", + Image: "example.com/app:v1", + VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: tmpl.mountPath}}, + }}, + Volumes: tmpl.volumes, SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"}, }); err != nil { t.Fatalf("creating template %s: %v", name, err) @@ -902,7 +922,26 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { t.Fatalf("UpdateActor to an absent template = %v, want FailedPrecondition (err: %v)", got, err) } - // Repointing at an existing template succeeds. + // Repointing at a template with different volume mounts is rejected. + _, err = svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ + Metadata: created.GetMetadata(), + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-c"}, + }}) + if got := status.Code(err); got != codes.FailedPrecondition { + t.Fatalf("UpdateActor to a template with different mounts = %v, want FailedPrecondition (err: %v)", got, err) + } + + // Repointing at a template with different volumes is rejected. + _, err = svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ + Metadata: created.GetMetadata(), + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-d"}, + }}) + if got := status.Code(err); got != codes.FailedPrecondition { + t.Fatalf("UpdateActor to a template with different volumes = %v, want FailedPrecondition (err: %v)", got, err) + } + + // Repointing at an existing template with identical volumes and mounts + // succeeds. updated, err := svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ Metadata: created.GetMetadata(), ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-b"}, @@ -913,6 +952,138 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { if got, want := updated.GetActorTemplate().GetName(), "tmpl-b"; got != want { t.Errorf("updated actor_template.name = %q, want %q", got, want) } + + // Repointing an actor that is not suspended is rejected, even at a + // compatible template. + running := storetest.MustCreateActor(t, ctx, persistence, &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "running-actor"}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-a"}, + Status: &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_RUNNING}, + }) + _, err = svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ + Metadata: running.GetMetadata(), + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-b"}, + }}) + if got := status.Code(err); got != codes.FailedPrecondition { + t.Fatalf("UpdateActor repointing a running actor = %v, want FailedPrecondition (err: %v)", got, err) + } + + // An update that keeps the template ref is still allowed while running: + // the suspended-state gate applies only to repoints. + kept, err := svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ + Metadata: running.GetMetadata(), + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-a"}, + WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"tier": "paid"}}, + }}) + if err != nil { + t.Fatalf("UpdateActor keeping the template on a running actor failed: %v", err) + } + if got, want := kept.GetActorTemplate().GetName(), "tmpl-a"; got != want { + t.Errorf("updated actor_template.name = %q, want %q", got, want) + } +} + +// TestValidateTemplateVolumesUnchanged exercises the volumes and +// per-container mount comparison applied when an actor is repointed at a +// replacement template. +func TestValidateTemplateVolumesUnchanged(t *testing.T) { + dataVolume := &ateapipb.Volume{Name: "data", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} + scratchVolume := &ateapipb.Volume{Name: "scratch", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} + template := func(volumes []*ateapipb.Volume, containers ...*ateapipb.Container) *ateapipb.ActorTemplate { + return &ateapipb.ActorTemplate{Volumes: volumes, Containers: containers} + } + container := func(name string, mounts ...*ateapipb.VolumeMount) *ateapipb.Container { + return &ateapipb.Container{Name: name, Image: "example.com/app:v1", VolumeMounts: mounts} + } + dataMount := &ateapipb.VolumeMount{Name: "data", MountPath: "/data"} + scratchMount := &ateapipb.VolumeMount{Name: "scratch", MountPath: "/scratch"} + + oneVolume := []*ateapipb.Volume{dataVolume} + twoVolumes := []*ateapipb.Volume{dataVolume, scratchVolume} + + tests := []struct { + name string + oldTmpl, newTmpl *ateapipb.ActorTemplate + wantErr bool + }{{ + name: "identical volumes and mounts", + oldTmpl: template(oneVolume, container("main", dataMount)), + newTmpl: template(oneVolume, container("main", dataMount)), + }, { + name: "no volumes or mounts on either side", + oldTmpl: template(nil, container("main")), + newTmpl: template(nil, container("other")), + }, { + name: "volume added", + oldTmpl: template(oneVolume, container("main", dataMount)), + newTmpl: template(twoVolumes, container("main", dataMount)), + wantErr: true, + }, { + name: "volume removed", + oldTmpl: template(twoVolumes, container("main", dataMount)), + newTmpl: template(oneVolume, container("main", dataMount)), + wantErr: true, + }, { + name: "volume renamed", + oldTmpl: template(oneVolume, container("main", dataMount)), + newTmpl: template([]*ateapipb.Volume{{Name: "data2", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}}}, container("main", dataMount)), + wantErr: true, + }, { + name: "volume source changed", + oldTmpl: template(oneVolume, container("main", dataMount)), + newTmpl: template([]*ateapipb.Volume{{Name: "data", Type: "Image", Image: &ateapipb.ImageVolumeSource{Reference: "example.com/data@sha256:0f9c04b7387d13ba9d15ec50355f9ad533fee2e5ad25378753a30671f8f9b938"}}}, container("main", dataMount)), + wantErr: true, + }, { + name: "volume order changed", + oldTmpl: template(twoVolumes, container("main", dataMount)), + newTmpl: template([]*ateapipb.Volume{scratchVolume, dataVolume}, container("main", dataMount)), + wantErr: true, + }, { + name: "mount path changed", + oldTmpl: template(oneVolume, container("main", dataMount)), + newTmpl: template(oneVolume, container("main", &ateapipb.VolumeMount{Name: "data", MountPath: "/mnt/data"})), + wantErr: true, + }, { + name: "mount added", + oldTmpl: template(twoVolumes, container("main", dataMount)), + newTmpl: template(twoVolumes, container("main", dataMount, scratchMount)), + wantErr: true, + }, { + name: "mount removed", + oldTmpl: template(oneVolume, container("main", dataMount)), + newTmpl: template(oneVolume, container("main")), + wantErr: true, + }, { + name: "mounted container renamed", + oldTmpl: template(oneVolume, container("main", dataMount)), + newTmpl: template(oneVolume, container("renamed", dataMount)), + }, { + name: "container added with mounts", + oldTmpl: template(twoVolumes, container("main", dataMount)), + newTmpl: template(twoVolumes, container("main", dataMount), container("sidecar", scratchMount)), + }, { + name: "mount order changed", + oldTmpl: template(twoVolumes, container("main", dataMount, scratchMount)), + newTmpl: template(twoVolumes, container("main", scratchMount, dataMount)), + wantErr: true, + }, { + name: "mountless container renamed", + oldTmpl: template(oneVolume, container("main", dataMount), container("sidecar")), + newTmpl: template(oneVolume, container("main", dataMount), container("helper")), + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateTemplateVolumesUnchanged(tt.oldTmpl, tt.newTmpl) + if gotErr := err != nil; gotErr != tt.wantErr { + t.Fatalf("validateVolumeMountsUnchanged() error = %v, wantErr %v", err, tt.wantErr) + } + if err != nil { + if got := status.Code(err); got != codes.FailedPrecondition { + t.Errorf("status code = %v, want FailedPrecondition", got) + } + } + }) + } } // TestUpdateActor_DeleteRecreateRace checks that an update is not applied diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go index 28567d0e52..3bdc2d7391 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go @@ -667,58 +667,85 @@ func TestUpdateActor_Success(t *testing.T) { // TestUpdateActor_RepointTemplate verifies UpdateActor can point an actor at // a different substrate ActorTemplate (effective on the next ResumeActor), -// and that a ref to an absent template is rejected. +// and that a ref to an absent template, or to one with different volumes or +// volume mounts, is rejected. func TestUpdateActor_RepointTemplate(t *testing.T) { - ns := namespaceForTest("ns-update-repoint") - tc := setupTest(t, ns) - defer tc.cleanup() - - ctx := context.Background() - for _, name := range []string{"tmpl-a", "tmpl-b"} { - if _, err := tc.client.CreateActorTemplate(ctx, &ateapipb.CreateActorTemplateRequest{ - ActorTemplate: &ateapipb.ActorTemplate{ - Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: name}, - Containers: []*ateapipb.Container{{Name: "main", Image: "example.com/app:v1"}}, - SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"}, - SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"}, - }, - }); err != nil { - t.Fatalf("CreateActorTemplate %s failed: %v", name, err) - } + tests := []struct { + name string + template string + wantCode codes.Code + }{ + {name: "absent-template", template: "absent", wantCode: codes.FailedPrecondition}, + {name: "different-mounts", template: "tmpl-c", wantCode: codes.FailedPrecondition}, + {name: "different-volumes", template: "tmpl-d", wantCode: codes.FailedPrecondition}, + {name: "same-volumes", template: "tmpl-b", wantCode: codes.OK}, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ns := namespaceForTest("ns-update-repoint-" + tt.name) + tc := setupTest(t, ns) + defer tc.cleanup() - created, err := tc.client.CreateActor(ctx, &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ - Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "repoint-actor"}, - ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-a"}, - }}) - if err != nil { - t.Fatalf("CreateActor failed: %v", err) - } + ctx := context.Background() + // tmpl-a and tmpl-b are volume-compatible; tmpl-c mounts the data + // volume elsewhere and tmpl-d declares an extra volume. + dataVolume := &ateapipb.Volume{Name: "data", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} + scratchVolume := &ateapipb.Volume{Name: "scratch", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} + templates := map[string]struct { + mountPath string + volumes []*ateapipb.Volume + }{ + "tmpl-a": {"/data", []*ateapipb.Volume{dataVolume}}, + "tmpl-b": {"/data", []*ateapipb.Volume{dataVolume}}, + "tmpl-c": {"/mnt/data", []*ateapipb.Volume{dataVolume}}, + "tmpl-d": {"/data", []*ateapipb.Volume{dataVolume, scratchVolume}}, + } + for name, tmpl := range templates { + if _, err := tc.client.CreateActorTemplate(ctx, &ateapipb.CreateActorTemplateRequest{ + ActorTemplate: &ateapipb.ActorTemplate{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: name}, + Containers: []*ateapipb.Container{{ + Name: "main", + Image: "example.com/app:v1", + VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: tmpl.mountPath}}, + }}, + Volumes: tmpl.volumes, + SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"}, + SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"}, + }, + }); err != nil { + t.Fatalf("CreateActorTemplate %s failed: %v", name, err) + } + } - // A ref to a template that does not exist is rejected. - _, err = tc.client.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ - Metadata: created.GetMetadata(), - ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "absent"}, - }}) - if got := status.Code(err); got != codes.FailedPrecondition { - t.Fatalf("UpdateActor with an absent template ref = %v, want FailedPrecondition (err: %v)", got, err) - } + created, err := tc.client.CreateActor(ctx, &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "repoint-actor"}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-a"}, + }}) + if err != nil { + t.Fatalf("CreateActor failed: %v", err) + } - updated, err := tc.client.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ - Metadata: created.GetMetadata(), - ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-b"}, - }}) - if err != nil { - t.Fatalf("UpdateActor failed: %v", err) - } + updated, err := tc.client.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ + Metadata: created.GetMetadata(), + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: tt.template}, + }}) + if got := status.Code(err); got != tt.wantCode { + t.Fatalf("UpdateActor repointing at %s = %v, want %v (err: %v)", tt.template, got, tt.wantCode, err) + } + if tt.wantCode != codes.OK { + return + } - want := &ateapipb.Actor{ - Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "repoint-actor", Version: 2}, - ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-b"}, - Status: &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED}, - } - if diff := cmp.Diff(want, updated, protocmp.Transform(), ignoreUID, ignoreTimestamps); diff != "" { - t.Errorf("UpdateActor response mismatch (-want +got):\n%s", diff) + want := &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "repoint-actor", Version: 2}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: tt.template}, + Status: &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED}, + } + if diff := cmp.Diff(want, updated, protocmp.Transform(), ignoreUID, ignoreTimestamps); diff != "" { + t.Errorf("UpdateActor response mismatch (-want +got):\n%s", diff) + } + }) } } From db5ec38e69e2e6cbac86e37552ed7c8f7fa3bd3c Mon Sep 17 00:00:00 2001 From: zoezhao Date: Wed, 2 Sep 2026 08:40:07 -0700 Subject: [PATCH 4/9] Add validation to refuse updating gVisor actortemplates with FULL snapshot scope --- cmd/ateapi/internal/controlapi/actor.go | 32 ++++++- cmd/ateapi/internal/controlapi/actor_test.go | 94 +++++++++++++++++-- .../controlapi/functionaltest/actor_test.go | 43 +++++++-- .../e2e/suites/demo/updatetemplate_test.go | 15 ++- 4 files changed, 164 insertions(+), 20 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/actor.go b/cmd/ateapi/internal/controlapi/actor.go index 517432fc6c..4d91c44a7a 100644 --- a/cmd/ateapi/internal/controlapi/actor.go +++ b/cmd/ateapi/internal/controlapi/actor.go @@ -281,8 +281,9 @@ func (s *ServiceImpl) UpdateActor(ctx context.Context, actorRef resources.ActorR // Update actor template is only allowed while the actor is suspended. // The repointed ref must also resolve, mirroring CreateActor's // check (same non-atomicity caveat; resume re-resolves and fails - // cleanly), and the replacement's volumes and volume mounts must match - // the old template's. + // cleanly), the replacement's volumes and volume mounts must match + // the old template's, and both templates' snapshot scopes must + // support the data-only restore a repoint forces. if !proto.Equal(oldVal.GetActorTemplate(), newVal.GetActorTemplate()) { if state := oldVal.GetStatus().GetState(); state != ateapipb.ActorState_ACTOR_STATE_SUSPENDED { return status.Errorf(codes.FailedPrecondition, @@ -299,6 +300,9 @@ func (s *ServiceImpl) UpdateActor(ctx context.Context, actorRef resources.ActorR if err := validateTemplateVolumesUnchanged(oldTemplate, newTemplate); err != nil { return err } + if err := validateTemplateSnapshotScopes(oldTemplate, newTemplate); err != nil { + return err + } } // Validate the final value before storing it. @@ -360,6 +364,30 @@ func validateTemplateVolumesUnchanged(oldTemplate, newTemplate *ateapipb.ActorTe return nil } +// validateTemplateSnapshotScopes rejects an actor template update unless +// both gVisor templates commit DATA-scope snapshots: the update forces a +// data-only resume, which gVisor cannot yet do from a FULL snapshot. +func validateTemplateSnapshotScopes(oldTemplate, newTemplate *ateapipb.ActorTemplate) error { + if err := validateGvisorCommitScope(oldTemplate); err != nil { + return err + } + return validateGvisorCommitScope(newTemplate) +} + +// validateGvisorCommitScope rejects a gVisor template that commits FULL-scope +// snapshots. +func validateGvisorCommitScope(template *ateapipb.ActorTemplate) error { + if template.GetSandboxConfig().GetSandboxClass() != ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR { + return nil + } + if scope := effectiveContentScope(template.GetSnapshotsConfig().GetOnCommit()); scope != ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA { + return status.Errorf(codes.FailedPrecondition, + "actor template %q commits %s snapshots on gVisor, which does not support the data-only restore a repoint forces; both templates must set on_commit to %s", + template.GetMetadata().GetName(), scope, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA) + } + return nil +} + func validateUpdateActorRequest(ctx context.Context, req *ateapipb.UpdateActorRequest) field.ErrorList { // Call the generated validation. // We model this as a create rather than an update because updates assume diff --git a/cmd/ateapi/internal/controlapi/actor_test.go b/cmd/ateapi/internal/controlapi/actor_test.go index 233b3047bd..6c6b21c3a8 100644 --- a/cmd/ateapi/internal/controlapi/actor_test.go +++ b/cmd/ateapi/internal/controlapi/actor_test.go @@ -870,8 +870,8 @@ func TestUpdateActor(t *testing.T) { // TestUpdateActor_RepointTemplate covers the mutable actor_template ref: an // update may point a suspended actor at a different template (it takes effect // on the next ResumeActor), but the actor must be suspended, the new ref must -// resolve, and the replacement's volumes and volume mounts must match the old -// template's. +// resolve, the replacement's volumes and volume mounts must match the old +// template's, and gVisor templates must commit DATA-scope snapshots. func TestUpdateActor_RepointTemplate(t *testing.T) { ctx := context.Background() persistence, cleanup := storetest.SetupTestStore(t) @@ -879,17 +879,20 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { storetest.MustCreateAtespace(t, ctx, persistence, testAtespace) // tmpl-a and tmpl-b are volume-compatible; tmpl-c mounts the data volume - // elsewhere and tmpl-d declares an extra volume. + // elsewhere, tmpl-d declares an extra volume, and tmpl-e commits FULL + // snapshots, which gVisor cannot restore data-only. dataVolume := &ateapipb.Volume{Name: "data", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} scratchVolume := &ateapipb.Volume{Name: "scratch", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} templates := map[string]struct { mountPath string volumes []*ateapipb.Volume + onCommit ateapipb.SnapshotContentScope }{ - "tmpl-a": {"/data", []*ateapipb.Volume{dataVolume}}, - "tmpl-b": {"/data", []*ateapipb.Volume{dataVolume}}, - "tmpl-c": {"/mnt/data", []*ateapipb.Volume{dataVolume}}, - "tmpl-d": {"/data", []*ateapipb.Volume{dataVolume, scratchVolume}}, + "tmpl-a": {"/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, + "tmpl-b": {"/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, + "tmpl-c": {"/mnt/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, + "tmpl-d": {"/data", []*ateapipb.Volume{dataVolume, scratchVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, + "tmpl-e": {"/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_FULL}, } for name, tmpl := range templates { if _, err := persistence.CreateActorTemplate(ctx, &ateapipb.ActorTemplate{ @@ -900,7 +903,8 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: tmpl.mountPath}}, }}, Volumes: tmpl.volumes, - SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"}, + SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots", OnCommit: tmpl.onCommit}, + SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"}, }); err != nil { t.Fatalf("creating template %s: %v", name, err) } @@ -940,6 +944,15 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { t.Fatalf("UpdateActor to a template with different volumes = %v, want FailedPrecondition (err: %v)", got, err) } + // Repointing at a gVisor template that commits FULL snapshots is rejected. + _, err = svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ + Metadata: created.GetMetadata(), + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-e"}, + }}) + if got := status.Code(err); got != codes.FailedPrecondition { + t.Fatalf("UpdateActor to a FULL-commit gVisor template = %v, want FailedPrecondition (err: %v)", got, err) + } + // Repointing at an existing template with identical volumes and mounts // succeeds. updated, err := svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ @@ -1086,6 +1099,71 @@ func TestValidateTemplateVolumesUnchanged(t *testing.T) { } } +// TestValidateTemplateSnapshotScopes exercises the snapshot-scope comparison +// applied when an actor is repointed: a gVisor template on either side must +// commit DATA-scope snapshots, while other sandbox classes are unconstrained. +func TestValidateTemplateSnapshotScopes(t *testing.T) { + template := func(class ateapipb.SandboxClass, onCommit ateapipb.SnapshotContentScope) *ateapipb.ActorTemplate { + tmpl := &ateapipb.ActorTemplate{ + SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots", OnCommit: onCommit}, + } + switch class { + case ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR: + tmpl.SandboxConfig = &ateapipb.SandboxConfig{SandboxClass: class, ConfigName: "gvisor-default"} + case ateapipb.SandboxClass_SANDBOX_CLASS_MICROVM: + tmpl.SandboxConfig = &ateapipb.SandboxConfig{SandboxClass: class, ConfigName: "microvm-default"} + } + return tmpl + } + dataScope := ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA + fullScope := ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_FULL + unspecifiedScope := ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_UNSPECIFIED + gvisor := ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR + microvm := ateapipb.SandboxClass_SANDBOX_CLASS_MICROVM + + tests := []struct { + name string + oldTmpl, newTmpl *ateapipb.ActorTemplate + wantErr bool + }{{ + name: "gvisor with DATA commits on both sides", + oldTmpl: template(gvisor, dataScope), + newTmpl: template(gvisor, dataScope), + }, { + name: "gvisor with FULL commits on the current template", + oldTmpl: template(gvisor, fullScope), + newTmpl: template(gvisor, dataScope), + wantErr: true, + }, { + name: "gvisor with FULL commits on the new template", + oldTmpl: template(gvisor, dataScope), + newTmpl: template(gvisor, fullScope), + wantErr: true, + }, { + name: "gvisor with an unset on_commit reads as FULL", + oldTmpl: template(gvisor, dataScope), + newTmpl: template(gvisor, unspecifiedScope), + wantErr: true, + }, { + name: "microvm with FULL commits", + oldTmpl: template(microvm, fullScope), + newTmpl: template(microvm, fullScope), + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateTemplateSnapshotScopes(tt.oldTmpl, tt.newTmpl) + if gotErr := err != nil; gotErr != tt.wantErr { + t.Fatalf("validateTemplateSnapshotScopes() error = %v, wantErr %v", err, tt.wantErr) + } + if err != nil { + if got := status.Code(err); got != codes.FailedPrecondition { + t.Errorf("status code = %v, want FailedPrecondition", got) + } + } + }) + } +} + // TestUpdateActor_DeleteRecreateRace checks that an update is not applied // if an actor was deleted and recreated during the update operation. func TestUpdateActor_DeleteRecreateRace(t *testing.T) { diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go index 3bdc2d7391..8c6b6d6bae 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go @@ -667,8 +667,8 @@ func TestUpdateActor_Success(t *testing.T) { // TestUpdateActor_RepointTemplate verifies UpdateActor can point an actor at // a different substrate ActorTemplate (effective on the next ResumeActor), -// and that a ref to an absent template, or to one with different volumes or -// volume mounts, is rejected. +// and that a ref to an absent template, to one with different volumes or +// volume mounts, or to a FULL-commit gVisor template, is rejected. func TestUpdateActor_RepointTemplate(t *testing.T) { tests := []struct { name string @@ -678,6 +678,7 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { {name: "absent-template", template: "absent", wantCode: codes.FailedPrecondition}, {name: "different-mounts", template: "tmpl-c", wantCode: codes.FailedPrecondition}, {name: "different-volumes", template: "tmpl-d", wantCode: codes.FailedPrecondition}, + {name: "gvisor-full-commit", template: "tmpl-e", wantCode: codes.FailedPrecondition}, {name: "same-volumes", template: "tmpl-b", wantCode: codes.OK}, } for _, tt := range tests { @@ -688,17 +689,20 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { ctx := context.Background() // tmpl-a and tmpl-b are volume-compatible; tmpl-c mounts the data - // volume elsewhere and tmpl-d declares an extra volume. + // volume elsewhere, tmpl-d declares an extra volume, and tmpl-e + // commits FULL snapshots, which gVisor cannot restore data-only. dataVolume := &ateapipb.Volume{Name: "data", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} scratchVolume := &ateapipb.Volume{Name: "scratch", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} templates := map[string]struct { mountPath string volumes []*ateapipb.Volume + onCommit ateapipb.SnapshotContentScope }{ - "tmpl-a": {"/data", []*ateapipb.Volume{dataVolume}}, - "tmpl-b": {"/data", []*ateapipb.Volume{dataVolume}}, - "tmpl-c": {"/mnt/data", []*ateapipb.Volume{dataVolume}}, - "tmpl-d": {"/data", []*ateapipb.Volume{dataVolume, scratchVolume}}, + "tmpl-a": {"/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, + "tmpl-b": {"/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, + "tmpl-c": {"/mnt/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, + "tmpl-d": {"/data", []*ateapipb.Volume{dataVolume, scratchVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, + "tmpl-e": {"/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_FULL}, } for name, tmpl := range templates { if _, err := tc.client.CreateActorTemplate(ctx, &ateapipb.CreateActorTemplateRequest{ @@ -710,7 +714,7 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: tmpl.mountPath}}, }}, Volumes: tmpl.volumes, - SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"}, + SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots", OnCommit: tmpl.onCommit}, SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"}, }, }); err != nil { @@ -758,7 +762,28 @@ func TestUpdateActor(t *testing.T) { tc := setupTest(t, ns) defer tc.cleanup() - tmpl := createTemplate(t, tc, ns) + // Created directly rather than via createTemplate: the repoint below runs + // under gVisor, which refuses to repoint actors whose templates commit + // FULL snapshots, so this template needs a DATA commit scope. Nothing + // resumes here, so no pool or golden snapshot is required. + tmpl, err := tc.client.CreateActorTemplate(context.Background(), &ateapipb.CreateActorTemplateRequest{ + ActorTemplate: &ateapipb.ActorTemplate{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "tmpl1"}, + Containers: []*ateapipb.Container{{ + Name: "main", + Image: "main@sha256:abc", + Command: []string{"/main"}, + }}, + SnapshotsConfig: &ateapipb.SnapshotsConfig{ + StorageLocation: "gs://fake-fake-fake", + OnCommit: ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA, + }, + SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"}, + }, + }) + if err != nil { + t.Fatalf("CreateActorTemplate failed: %v", err) + } created, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "id1"}, diff --git a/internal/e2e/suites/demo/updatetemplate_test.go b/internal/e2e/suites/demo/updatetemplate_test.go index 52d346ff1a..560e116232 100644 --- a/internal/e2e/suites/demo/updatetemplate_test.go +++ b/internal/e2e/suites/demo/updatetemplate_test.go @@ -23,6 +23,8 @@ import ( "github.com/agent-substrate/substrate/internal/e2e" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // TestUpdateTemplateLifecycle covers repointing a suspended actor at a @@ -31,7 +33,9 @@ import ( // UpdateActor, and resumes. The resume must detect the template change (the // recorded current_actor_template_uid no longer matches) and restore // data-only: the durable dir survives while the guest cold-boots from -// template B. +// template B. On gVisor the onCommit:Full case instead expects UpdateActor +// to refuse the repoint: gVisor cannot restore data-only from a FULL +// snapshot yet. func TestUpdateTemplateLifecycle(t *testing.T) { tests := []struct { name string @@ -158,6 +162,15 @@ func runUpdateTemplateTestCase(t *testing.T, onCommit ateapipb.SnapshotContentSc Metadata: suspended.GetMetadata(), ActorTemplate: refB, }}) + // gVisor cannot restore data-only from a FULL snapshot yet, so the API + // refuses to repoint an actor whose gVisor templates commit FULL scope. + if onCommit == ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_FULL && + createdA.GetSandboxConfig().GetSandboxClass() == ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR { + if got := status.Code(err); got != codes.FailedPrecondition { + t.Fatalf("UpdateActor repointing a FULL-commit gVisor actor = %v, want FailedPrecondition (err: %v)", got, err) + } + return + } if err != nil { t.Fatalf("failed to update Actor's template: %v", err) } From d61e10e28bc5af92c6abdffb151da13bcd0d69b3 Mon Sep 17 00:00:00 2001 From: zoezhao Date: Wed, 2 Sep 2026 10:36:01 -0700 Subject: [PATCH 5/9] compare against the previous template's template instead --- .../internal/controlapi/workflow_resume.go | 18 +++-- .../controlapi/workflow_resume_test.go | 73 +++++++++++++++++++ 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/workflow_resume.go b/cmd/ateapi/internal/controlapi/workflow_resume.go index 07f69f4380..0e8c62258e 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume.go @@ -49,6 +49,9 @@ type resumeSnapshotSource struct { // selects the golden snapshot as the boot source for the pending restore: // restore then combines the golden snapshot with the actor's data. GoldenSnapshotURI resources.SnapshotURI + // TemplateReplaced is true when the snapshot's recorded template UID + // differs from the actor's current template. + TemplateReplaced bool } // restoreTelemetry labels the restore operation for the resume lifecycle @@ -188,6 +191,11 @@ func (w *ActorWorkflow) loadActorForResume(ctx context.Context, actorRef resourc return nil, nil, src, status.Errorf(codes.DataLoss, "ActorSnapshot %s/%s: %v", ref.GetAtespace(), ref.GetName(), err) } src.Scope = snapshot.GetStatus().GetContentScope() + // The snapshot records the template it was captured under; a + // different UID on the actor's current template means the actor was + // repointed since the capture. + snapshotTemplateUID := snapshot.GetStatus().GetActorTemplateUid() + src.TemplateReplaced = snapshotTemplateUID != "" && snapshotTemplateUID != actorTemplate.GetMetadata().GetUid() } else if goldenRef := actorTemplate.GetStatus().GetGoldenSnapshotStatus().GetGoldenSnapshot(); goldenRef != nil && !boot { snapshot, err := w.store.GetActorSnapshot(ctx, resources.ActorSnapshotRefFromObjectRef(goldenRef)) if errors.Is(err, store.ErrNotFound) { @@ -671,12 +679,6 @@ func (w *ActorWorkflow) ensureAteletRestored(ctx context.Context, actorRef resou return tele, err } - // A snapshot taken under a since-replaced template holds guest state that - // cannot be layered onto the new template's golden snapshot, so the - // restore is forced to data-only. - templateReplaced := actor.GetStatus().GetCurrentActorTemplate() != nil && - actor.GetStatus().GetCurrentActorTemplateUid() != actorTemplate.GetMetadata().GetUid() - if local := actor.GetStatus().GetLocalSnapshotInfo(); local != nil { slog.InfoContext(ctx, "Actor has snapshot; Restoring from snapshot") tele.SnapshotKind = ateattr.SnapshotKindLocal @@ -701,7 +703,7 @@ func (w *ActorWorkflow) ensureAteletRestored(ctx context.Context, actorRef resou // loadActorForResume resolved a golden URI per the template's onResume // configuration, else what the pause captured. switch { - case templateReplaced: + case src.TemplateReplaced: req.Scope = ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA case !src.GoldenSnapshotURI.IsZero(): req.Scope = ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA_ON_GOLDEN @@ -724,7 +726,7 @@ func (w *ActorWorkflow) ensureAteletRestored(ctx context.Context, actorRef resou var scope ateletpb.SnapshotScope var goldenSnapshotURI string switch { - case templateReplaced: + case src.TemplateReplaced: scope = ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA case !src.GoldenSnapshotURI.IsZero(): scope = ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA_ON_GOLDEN diff --git a/cmd/ateapi/internal/controlapi/workflow_resume_test.go b/cmd/ateapi/internal/controlapi/workflow_resume_test.go index 98dee2bc7d..bd54e4bd8f 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume_test.go @@ -1160,6 +1160,79 @@ func TestLoadActorForResume_GoldenFallbackRejectsNonFullGolden(t *testing.T) { } } +// TestLoadActorForResume_TemplateReplaced covers the snapshot-side detection +// of a repointed actor: the durable snapshot records the template UID it was +// captured under, and a mismatch with the actor's current template marks the +// source TemplateReplaced, forcing the restore to data-only. The detection +// must not depend on the actor's boot-time template stamp, which is absent +// on actors created from a snapshot clone. +func TestLoadActorForResume_TemplateReplaced(t *testing.T) { + actorRef := resources.ActorRef{Atespace: "team-a", Name: "id1"} + + tests := []struct { + name string + // snapshotTemplateUID seeds the snapshot's recorded template UID; + // "current" stands for the created template's own UID, "" leaves + // the field unset (a snapshot from before the field was recorded). + snapshotTemplateUID string + noSnapshot bool + want bool + }{ + {name: "snapshot taken under the current template", snapshotTemplateUID: "current", want: false}, + {name: "snapshot taken under a replaced template", snapshotTemplateUID: "some-other-uid", want: true}, + {name: "snapshot without a recorded template UID", snapshotTemplateUID: "", want: false}, + {name: "no durable snapshot", noSnapshot: true, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + persistence := newTestPersistence(t) + + storetest.MustCreateAtespace(t, ctx, persistence, "ns") + tmpl, err := persistence.CreateActorTemplate(ctx, &ateapipb.ActorTemplate{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "ns", Name: "tmpl1"}, + }) + if err != nil { + t.Fatalf("create template: %v", err) + } + if tmpl.GetMetadata().GetUid() == "" { + t.Fatal("created template has no UID; the matching case would be vacuous") + } + + var seedOpts []func(*ateapipb.Actor) + if !tt.noSnapshot { + uid := tt.snapshotTemplateUID + if uid == "current" { + uid = tmpl.GetMetadata().GetUid() + } + snap := storetest.MustCreateActorSnapshot(t, ctx, persistence, &ateapipb.ActorSnapshot{ + Metadata: &ateapipb.ResourceMetadata{Atespace: actorRef.Atespace, Name: "snap-1"}, + Status: &ateapipb.ActorSnapshotStatus{ + SourceActor: &ateapipb.ObjectRef{Atespace: actorRef.Atespace, Name: actorRef.Name}, + ActorTemplateUid: uid, + ContentScope: ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_FULL, + SnapshotUri: "gs://bucket/root/snapshots/" + actorRef.Atespace + "/snap-1", + }, + }) + seedOpts = append(seedOpts, func(a *ateapipb.Actor) { + a.Status.LatestSnapshot = &ateapipb.ObjectRef{Atespace: actorRef.Atespace, Name: snap.GetMetadata().GetName()} + }) + } + seedWorkflowActor(t, ctx, persistence, actorRef, "ns", "tmpl1", ateapipb.ActorState_ACTOR_STATE_SUSPENDED, seedOpts...) + + w := &ActorWorkflow{store: persistence} + _, _, src, err := w.loadActorForResume(ctx, actorRef, false) + if err != nil { + t.Fatalf("loadActorForResume: %v", err) + } + if src.TemplateReplaced != tt.want { + t.Errorf("src.TemplateReplaced = %v, want %v", src.TemplateReplaced, tt.want) + } + }) + } +} + func TestLoadActorForResume_RunningActorShortCircuits(t *testing.T) { ctx := context.Background() persistence := newTestPersistence(t) From c9ec7733f64f6145f2eabb5fff23eb1414f35da8 Mon Sep 17 00:00:00 2001 From: zoezhao Date: Wed, 2 Sep 2026 10:50:20 -0700 Subject: [PATCH 6/9] test: drop Volume.Type from the repoint-template fixtures The union discriminator was removed from ActorTemplate.volumes, but the repoint-template test fixtures added on this branch still set it, so the controlapi test binary no longer compiled. --- cmd/ateapi/internal/controlapi/actor_test.go | 12 ++++++------ .../internal/controlapi/functionaltest/actor_test.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/actor_test.go b/cmd/ateapi/internal/controlapi/actor_test.go index 6c6b21c3a8..b6794120d0 100644 --- a/cmd/ateapi/internal/controlapi/actor_test.go +++ b/cmd/ateapi/internal/controlapi/actor_test.go @@ -881,8 +881,8 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { // tmpl-a and tmpl-b are volume-compatible; tmpl-c mounts the data volume // elsewhere, tmpl-d declares an extra volume, and tmpl-e commits FULL // snapshots, which gVisor cannot restore data-only. - dataVolume := &ateapipb.Volume{Name: "data", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} - scratchVolume := &ateapipb.Volume{Name: "scratch", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} + dataVolume := &ateapipb.Volume{Name: "data", DurableDir: &ateapipb.DurableDirVolumeSource{}} + scratchVolume := &ateapipb.Volume{Name: "scratch", DurableDir: &ateapipb.DurableDirVolumeSource{}} templates := map[string]struct { mountPath string volumes []*ateapipb.Volume @@ -1000,8 +1000,8 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { // per-container mount comparison applied when an actor is repointed at a // replacement template. func TestValidateTemplateVolumesUnchanged(t *testing.T) { - dataVolume := &ateapipb.Volume{Name: "data", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} - scratchVolume := &ateapipb.Volume{Name: "scratch", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} + dataVolume := &ateapipb.Volume{Name: "data", DurableDir: &ateapipb.DurableDirVolumeSource{}} + scratchVolume := &ateapipb.Volume{Name: "scratch", DurableDir: &ateapipb.DurableDirVolumeSource{}} template := func(volumes []*ateapipb.Volume, containers ...*ateapipb.Container) *ateapipb.ActorTemplate { return &ateapipb.ActorTemplate{Volumes: volumes, Containers: containers} } @@ -1039,12 +1039,12 @@ func TestValidateTemplateVolumesUnchanged(t *testing.T) { }, { name: "volume renamed", oldTmpl: template(oneVolume, container("main", dataMount)), - newTmpl: template([]*ateapipb.Volume{{Name: "data2", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}}}, container("main", dataMount)), + newTmpl: template([]*ateapipb.Volume{{Name: "data2", DurableDir: &ateapipb.DurableDirVolumeSource{}}}, container("main", dataMount)), wantErr: true, }, { name: "volume source changed", oldTmpl: template(oneVolume, container("main", dataMount)), - newTmpl: template([]*ateapipb.Volume{{Name: "data", Type: "Image", Image: &ateapipb.ImageVolumeSource{Reference: "example.com/data@sha256:0f9c04b7387d13ba9d15ec50355f9ad533fee2e5ad25378753a30671f8f9b938"}}}, container("main", dataMount)), + newTmpl: template([]*ateapipb.Volume{{Name: "data", Image: &ateapipb.ImageVolumeSource{Reference: "example.com/data@sha256:0f9c04b7387d13ba9d15ec50355f9ad533fee2e5ad25378753a30671f8f9b938"}}}, container("main", dataMount)), wantErr: true, }, { name: "volume order changed", diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go index 8c6b6d6bae..9b42c15878 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go @@ -691,8 +691,8 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { // tmpl-a and tmpl-b are volume-compatible; tmpl-c mounts the data // volume elsewhere, tmpl-d declares an extra volume, and tmpl-e // commits FULL snapshots, which gVisor cannot restore data-only. - dataVolume := &ateapipb.Volume{Name: "data", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} - scratchVolume := &ateapipb.Volume{Name: "scratch", Type: "DurableDir", DurableDir: &ateapipb.DurableDirVolumeSource{}} + dataVolume := &ateapipb.Volume{Name: "data", DurableDir: &ateapipb.DurableDirVolumeSource{}} + scratchVolume := &ateapipb.Volume{Name: "scratch", DurableDir: &ateapipb.DurableDirVolumeSource{}} templates := map[string]struct { mountPath string volumes []*ateapipb.Volume From 3800aabd56b2ce1ac7b189b3418c9dcb00b012e1 Mon Sep 17 00:00:00 2001 From: zoezhao Date: Wed, 2 Sep 2026 13:21:09 -0700 Subject: [PATCH 7/9] Allow repointing gVisor actors that commit FULL-scope snapshots gVisor full captures now archive durable-dir bind mounts into durable-dir.tar and atelet can narrow them to a data snapshot, so the data-only restore a repoint forces works from a FULL snapshot. Drop the UpdateActor validation that refused FULL-commit gVisor templates, and let the e2e onCommit:Full case exercise the full repoint lifecycle on gVisor instead of expecting FailedPrecondition. --- cmd/ateapi/internal/controlapi/actor.go | 32 +------ cmd/ateapi/internal/controlapi/actor_test.go | 94 ++----------------- .../controlapi/functionaltest/actor_test.go | 43 ++------- .../e2e/suites/demo/updatetemplate_test.go | 15 +-- 4 files changed, 20 insertions(+), 164 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/actor.go b/cmd/ateapi/internal/controlapi/actor.go index 4d91c44a7a..517432fc6c 100644 --- a/cmd/ateapi/internal/controlapi/actor.go +++ b/cmd/ateapi/internal/controlapi/actor.go @@ -281,9 +281,8 @@ func (s *ServiceImpl) UpdateActor(ctx context.Context, actorRef resources.ActorR // Update actor template is only allowed while the actor is suspended. // The repointed ref must also resolve, mirroring CreateActor's // check (same non-atomicity caveat; resume re-resolves and fails - // cleanly), the replacement's volumes and volume mounts must match - // the old template's, and both templates' snapshot scopes must - // support the data-only restore a repoint forces. + // cleanly), and the replacement's volumes and volume mounts must match + // the old template's. if !proto.Equal(oldVal.GetActorTemplate(), newVal.GetActorTemplate()) { if state := oldVal.GetStatus().GetState(); state != ateapipb.ActorState_ACTOR_STATE_SUSPENDED { return status.Errorf(codes.FailedPrecondition, @@ -300,9 +299,6 @@ func (s *ServiceImpl) UpdateActor(ctx context.Context, actorRef resources.ActorR if err := validateTemplateVolumesUnchanged(oldTemplate, newTemplate); err != nil { return err } - if err := validateTemplateSnapshotScopes(oldTemplate, newTemplate); err != nil { - return err - } } // Validate the final value before storing it. @@ -364,30 +360,6 @@ func validateTemplateVolumesUnchanged(oldTemplate, newTemplate *ateapipb.ActorTe return nil } -// validateTemplateSnapshotScopes rejects an actor template update unless -// both gVisor templates commit DATA-scope snapshots: the update forces a -// data-only resume, which gVisor cannot yet do from a FULL snapshot. -func validateTemplateSnapshotScopes(oldTemplate, newTemplate *ateapipb.ActorTemplate) error { - if err := validateGvisorCommitScope(oldTemplate); err != nil { - return err - } - return validateGvisorCommitScope(newTemplate) -} - -// validateGvisorCommitScope rejects a gVisor template that commits FULL-scope -// snapshots. -func validateGvisorCommitScope(template *ateapipb.ActorTemplate) error { - if template.GetSandboxConfig().GetSandboxClass() != ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR { - return nil - } - if scope := effectiveContentScope(template.GetSnapshotsConfig().GetOnCommit()); scope != ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA { - return status.Errorf(codes.FailedPrecondition, - "actor template %q commits %s snapshots on gVisor, which does not support the data-only restore a repoint forces; both templates must set on_commit to %s", - template.GetMetadata().GetName(), scope, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA) - } - return nil -} - func validateUpdateActorRequest(ctx context.Context, req *ateapipb.UpdateActorRequest) field.ErrorList { // Call the generated validation. // We model this as a create rather than an update because updates assume diff --git a/cmd/ateapi/internal/controlapi/actor_test.go b/cmd/ateapi/internal/controlapi/actor_test.go index b6794120d0..da08345df0 100644 --- a/cmd/ateapi/internal/controlapi/actor_test.go +++ b/cmd/ateapi/internal/controlapi/actor_test.go @@ -870,8 +870,8 @@ func TestUpdateActor(t *testing.T) { // TestUpdateActor_RepointTemplate covers the mutable actor_template ref: an // update may point a suspended actor at a different template (it takes effect // on the next ResumeActor), but the actor must be suspended, the new ref must -// resolve, the replacement's volumes and volume mounts must match the old -// template's, and gVisor templates must commit DATA-scope snapshots. +// resolve, and the replacement's volumes and volume mounts must match the old +// template's. func TestUpdateActor_RepointTemplate(t *testing.T) { ctx := context.Background() persistence, cleanup := storetest.SetupTestStore(t) @@ -879,20 +879,17 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { storetest.MustCreateAtespace(t, ctx, persistence, testAtespace) // tmpl-a and tmpl-b are volume-compatible; tmpl-c mounts the data volume - // elsewhere, tmpl-d declares an extra volume, and tmpl-e commits FULL - // snapshots, which gVisor cannot restore data-only. + // elsewhere and tmpl-d declares an extra volume. dataVolume := &ateapipb.Volume{Name: "data", DurableDir: &ateapipb.DurableDirVolumeSource{}} scratchVolume := &ateapipb.Volume{Name: "scratch", DurableDir: &ateapipb.DurableDirVolumeSource{}} templates := map[string]struct { mountPath string volumes []*ateapipb.Volume - onCommit ateapipb.SnapshotContentScope }{ - "tmpl-a": {"/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, - "tmpl-b": {"/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, - "tmpl-c": {"/mnt/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, - "tmpl-d": {"/data", []*ateapipb.Volume{dataVolume, scratchVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, - "tmpl-e": {"/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_FULL}, + "tmpl-a": {"/data", []*ateapipb.Volume{dataVolume}}, + "tmpl-b": {"/data", []*ateapipb.Volume{dataVolume}}, + "tmpl-c": {"/mnt/data", []*ateapipb.Volume{dataVolume}}, + "tmpl-d": {"/data", []*ateapipb.Volume{dataVolume, scratchVolume}}, } for name, tmpl := range templates { if _, err := persistence.CreateActorTemplate(ctx, &ateapipb.ActorTemplate{ @@ -903,8 +900,7 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: tmpl.mountPath}}, }}, Volumes: tmpl.volumes, - SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots", OnCommit: tmpl.onCommit}, - SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"}, + SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"}, }); err != nil { t.Fatalf("creating template %s: %v", name, err) } @@ -944,15 +940,6 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { t.Fatalf("UpdateActor to a template with different volumes = %v, want FailedPrecondition (err: %v)", got, err) } - // Repointing at a gVisor template that commits FULL snapshots is rejected. - _, err = svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ - Metadata: created.GetMetadata(), - ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-e"}, - }}) - if got := status.Code(err); got != codes.FailedPrecondition { - t.Fatalf("UpdateActor to a FULL-commit gVisor template = %v, want FailedPrecondition (err: %v)", got, err) - } - // Repointing at an existing template with identical volumes and mounts // succeeds. updated, err := svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ @@ -1099,71 +1086,6 @@ func TestValidateTemplateVolumesUnchanged(t *testing.T) { } } -// TestValidateTemplateSnapshotScopes exercises the snapshot-scope comparison -// applied when an actor is repointed: a gVisor template on either side must -// commit DATA-scope snapshots, while other sandbox classes are unconstrained. -func TestValidateTemplateSnapshotScopes(t *testing.T) { - template := func(class ateapipb.SandboxClass, onCommit ateapipb.SnapshotContentScope) *ateapipb.ActorTemplate { - tmpl := &ateapipb.ActorTemplate{ - SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots", OnCommit: onCommit}, - } - switch class { - case ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR: - tmpl.SandboxConfig = &ateapipb.SandboxConfig{SandboxClass: class, ConfigName: "gvisor-default"} - case ateapipb.SandboxClass_SANDBOX_CLASS_MICROVM: - tmpl.SandboxConfig = &ateapipb.SandboxConfig{SandboxClass: class, ConfigName: "microvm-default"} - } - return tmpl - } - dataScope := ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA - fullScope := ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_FULL - unspecifiedScope := ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_UNSPECIFIED - gvisor := ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR - microvm := ateapipb.SandboxClass_SANDBOX_CLASS_MICROVM - - tests := []struct { - name string - oldTmpl, newTmpl *ateapipb.ActorTemplate - wantErr bool - }{{ - name: "gvisor with DATA commits on both sides", - oldTmpl: template(gvisor, dataScope), - newTmpl: template(gvisor, dataScope), - }, { - name: "gvisor with FULL commits on the current template", - oldTmpl: template(gvisor, fullScope), - newTmpl: template(gvisor, dataScope), - wantErr: true, - }, { - name: "gvisor with FULL commits on the new template", - oldTmpl: template(gvisor, dataScope), - newTmpl: template(gvisor, fullScope), - wantErr: true, - }, { - name: "gvisor with an unset on_commit reads as FULL", - oldTmpl: template(gvisor, dataScope), - newTmpl: template(gvisor, unspecifiedScope), - wantErr: true, - }, { - name: "microvm with FULL commits", - oldTmpl: template(microvm, fullScope), - newTmpl: template(microvm, fullScope), - }} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := validateTemplateSnapshotScopes(tt.oldTmpl, tt.newTmpl) - if gotErr := err != nil; gotErr != tt.wantErr { - t.Fatalf("validateTemplateSnapshotScopes() error = %v, wantErr %v", err, tt.wantErr) - } - if err != nil { - if got := status.Code(err); got != codes.FailedPrecondition { - t.Errorf("status code = %v, want FailedPrecondition", got) - } - } - }) - } -} - // TestUpdateActor_DeleteRecreateRace checks that an update is not applied // if an actor was deleted and recreated during the update operation. func TestUpdateActor_DeleteRecreateRace(t *testing.T) { diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go index 9b42c15878..bf04053056 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go @@ -667,8 +667,8 @@ func TestUpdateActor_Success(t *testing.T) { // TestUpdateActor_RepointTemplate verifies UpdateActor can point an actor at // a different substrate ActorTemplate (effective on the next ResumeActor), -// and that a ref to an absent template, to one with different volumes or -// volume mounts, or to a FULL-commit gVisor template, is rejected. +// and that a ref to an absent template, or to one with different volumes or +// volume mounts, is rejected. func TestUpdateActor_RepointTemplate(t *testing.T) { tests := []struct { name string @@ -678,7 +678,6 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { {name: "absent-template", template: "absent", wantCode: codes.FailedPrecondition}, {name: "different-mounts", template: "tmpl-c", wantCode: codes.FailedPrecondition}, {name: "different-volumes", template: "tmpl-d", wantCode: codes.FailedPrecondition}, - {name: "gvisor-full-commit", template: "tmpl-e", wantCode: codes.FailedPrecondition}, {name: "same-volumes", template: "tmpl-b", wantCode: codes.OK}, } for _, tt := range tests { @@ -689,20 +688,17 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { ctx := context.Background() // tmpl-a and tmpl-b are volume-compatible; tmpl-c mounts the data - // volume elsewhere, tmpl-d declares an extra volume, and tmpl-e - // commits FULL snapshots, which gVisor cannot restore data-only. + // volume elsewhere and tmpl-d declares an extra volume. dataVolume := &ateapipb.Volume{Name: "data", DurableDir: &ateapipb.DurableDirVolumeSource{}} scratchVolume := &ateapipb.Volume{Name: "scratch", DurableDir: &ateapipb.DurableDirVolumeSource{}} templates := map[string]struct { mountPath string volumes []*ateapipb.Volume - onCommit ateapipb.SnapshotContentScope }{ - "tmpl-a": {"/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, - "tmpl-b": {"/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, - "tmpl-c": {"/mnt/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, - "tmpl-d": {"/data", []*ateapipb.Volume{dataVolume, scratchVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA}, - "tmpl-e": {"/data", []*ateapipb.Volume{dataVolume}, ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_FULL}, + "tmpl-a": {"/data", []*ateapipb.Volume{dataVolume}}, + "tmpl-b": {"/data", []*ateapipb.Volume{dataVolume}}, + "tmpl-c": {"/mnt/data", []*ateapipb.Volume{dataVolume}}, + "tmpl-d": {"/data", []*ateapipb.Volume{dataVolume, scratchVolume}}, } for name, tmpl := range templates { if _, err := tc.client.CreateActorTemplate(ctx, &ateapipb.CreateActorTemplateRequest{ @@ -714,7 +710,7 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: tmpl.mountPath}}, }}, Volumes: tmpl.volumes, - SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots", OnCommit: tmpl.onCommit}, + SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"}, SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"}, }, }); err != nil { @@ -762,28 +758,7 @@ func TestUpdateActor(t *testing.T) { tc := setupTest(t, ns) defer tc.cleanup() - // Created directly rather than via createTemplate: the repoint below runs - // under gVisor, which refuses to repoint actors whose templates commit - // FULL snapshots, so this template needs a DATA commit scope. Nothing - // resumes here, so no pool or golden snapshot is required. - tmpl, err := tc.client.CreateActorTemplate(context.Background(), &ateapipb.CreateActorTemplateRequest{ - ActorTemplate: &ateapipb.ActorTemplate{ - Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "tmpl1"}, - Containers: []*ateapipb.Container{{ - Name: "main", - Image: "main@sha256:abc", - Command: []string{"/main"}, - }}, - SnapshotsConfig: &ateapipb.SnapshotsConfig{ - StorageLocation: "gs://fake-fake-fake", - OnCommit: ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA, - }, - SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"}, - }, - }) - if err != nil { - t.Fatalf("CreateActorTemplate failed: %v", err) - } + tmpl := createTemplate(t, tc, ns) created, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "id1"}, diff --git a/internal/e2e/suites/demo/updatetemplate_test.go b/internal/e2e/suites/demo/updatetemplate_test.go index 560e116232..52d346ff1a 100644 --- a/internal/e2e/suites/demo/updatetemplate_test.go +++ b/internal/e2e/suites/demo/updatetemplate_test.go @@ -23,8 +23,6 @@ import ( "github.com/agent-substrate/substrate/internal/e2e" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) // TestUpdateTemplateLifecycle covers repointing a suspended actor at a @@ -33,9 +31,7 @@ import ( // UpdateActor, and resumes. The resume must detect the template change (the // recorded current_actor_template_uid no longer matches) and restore // data-only: the durable dir survives while the guest cold-boots from -// template B. On gVisor the onCommit:Full case instead expects UpdateActor -// to refuse the repoint: gVisor cannot restore data-only from a FULL -// snapshot yet. +// template B. func TestUpdateTemplateLifecycle(t *testing.T) { tests := []struct { name string @@ -162,15 +158,6 @@ func runUpdateTemplateTestCase(t *testing.T, onCommit ateapipb.SnapshotContentSc Metadata: suspended.GetMetadata(), ActorTemplate: refB, }}) - // gVisor cannot restore data-only from a FULL snapshot yet, so the API - // refuses to repoint an actor whose gVisor templates commit FULL scope. - if onCommit == ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_FULL && - createdA.GetSandboxConfig().GetSandboxClass() == ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR { - if got := status.Code(err); got != codes.FailedPrecondition { - t.Fatalf("UpdateActor repointing a FULL-commit gVisor actor = %v, want FailedPrecondition (err: %v)", got, err) - } - return - } if err != nil { t.Fatalf("failed to update Actor's template: %v", err) } From 07977c878612837e2befb13ba3f1a9df8ca49c14 Mon Sep 17 00:00:00 2001 From: zoezhao Date: Wed, 2 Sep 2026 16:10:41 -0700 Subject: [PATCH 8/9] Skip repoint compatibility checks when the old template is deleted An actor can outlive its ActorTemplate. When the stored template ref no longer resolves there is nothing to compare the replacement against, so repointing away from a deleted template was impossible. Treat a not-found old template as skipping the comparison; any other lookup error still fails the update. --- cmd/ateapi/internal/controlapi/actor.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/actor.go b/cmd/ateapi/internal/controlapi/actor.go index 517432fc6c..62acbb416d 100644 --- a/cmd/ateapi/internal/controlapi/actor.go +++ b/cmd/ateapi/internal/controlapi/actor.go @@ -293,10 +293,12 @@ func (s *ServiceImpl) UpdateActor(ctx context.Context, actorRef resources.ActorR return err } oldTemplate, err := resolveActorTemplate(ctx, s.store, oldVal) - if err != nil { - return err - } - if err := validateTemplateVolumesUnchanged(oldTemplate, newTemplate); err != nil { + if err == nil { + if err := validateTemplateVolumesUnchanged(oldTemplate, newTemplate); err != nil { + return err + } + } else if !errors.Is(err, errActorTemplateNotFound) { + // Skip the validation if old template is not found return err } } From 5428e3895ddfe6e1eaf68fc673fceff8a3d038ef Mon Sep 17 00:00:00 2001 From: zoezhao Date: Wed, 2 Sep 2026 16:10:46 -0700 Subject: [PATCH 9/9] Refuse repointing an actor at a template with a different sandbox class Snapshots are not portable across sandbox runtime families, so a repointed actor's saved state could not be restored under a template of another class. Compare the old and new template's sandbox_config.sandbox_class during UpdateActor and reject mismatches with FailedPrecondition. Only the class must match; a different config_name within the same class is still allowed. --- cmd/ateapi/internal/controlapi/actor.go | 21 +++++- cmd/ateapi/internal/controlapi/actor_test.go | 77 ++++++++++++++++++-- 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/actor.go b/cmd/ateapi/internal/controlapi/actor.go index 62acbb416d..2a87788274 100644 --- a/cmd/ateapi/internal/controlapi/actor.go +++ b/cmd/ateapi/internal/controlapi/actor.go @@ -281,8 +281,8 @@ func (s *ServiceImpl) UpdateActor(ctx context.Context, actorRef resources.ActorR // Update actor template is only allowed while the actor is suspended. // The repointed ref must also resolve, mirroring CreateActor's // check (same non-atomicity caveat; resume re-resolves and fails - // cleanly), and the replacement's volumes and volume mounts must match - // the old template's. + // cleanly), and the replacement's sandbox class, volumes, and volume + // mounts must match the old template's. if !proto.Equal(oldVal.GetActorTemplate(), newVal.GetActorTemplate()) { if state := oldVal.GetStatus().GetState(); state != ateapipb.ActorState_ACTOR_STATE_SUSPENDED { return status.Errorf(codes.FailedPrecondition, @@ -294,6 +294,9 @@ func (s *ServiceImpl) UpdateActor(ctx context.Context, actorRef resources.ActorR } oldTemplate, err := resolveActorTemplate(ctx, s.store, oldVal) if err == nil { + if err := validateTemplateSandboxClassUnchanged(oldTemplate, newTemplate); err != nil { + return err + } if err := validateTemplateVolumesUnchanged(oldTemplate, newTemplate); err != nil { return err } @@ -328,6 +331,20 @@ func (s *ServiceImpl) UpdateActor(ctx context.Context, actorRef resources.ActorR return storedActor, nil } +// validateTemplateSandboxClassUnchanged rejects a template repoint that +// changes the sandbox class: snapshots are not portable across sandbox +// runtime families, so the actor's saved state could not be restored under +// the new template. +func validateTemplateSandboxClassUnchanged(oldTemplate, newTemplate *ateapipb.ActorTemplate) error { + oldClass := oldTemplate.GetSandboxConfig().GetSandboxClass() + newClass := newTemplate.GetSandboxConfig().GetSandboxClass() + if oldClass != newClass { + return status.Errorf(codes.FailedPrecondition, + "sandbox class differs between the current (%s) and the new (%s) actor template; the sandbox class must be identical to repoint an actor", oldClass, newClass) + } + return nil +} + // validateTemplateVolumesUnchanged rejects a template repoint that changes // the template's volumes or any container's volume mounts: an actor's // snapshot data is laid out per the volumes and mount paths it was captured diff --git a/cmd/ateapi/internal/controlapi/actor_test.go b/cmd/ateapi/internal/controlapi/actor_test.go index da08345df0..e6f04a8524 100644 --- a/cmd/ateapi/internal/controlapi/actor_test.go +++ b/cmd/ateapi/internal/controlapi/actor_test.go @@ -879,17 +879,22 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { storetest.MustCreateAtespace(t, ctx, persistence, testAtespace) // tmpl-a and tmpl-b are volume-compatible; tmpl-c mounts the data volume - // elsewhere and tmpl-d declares an extra volume. + // elsewhere, tmpl-d declares an extra volume, and tmpl-e runs on a + // different sandbox class. dataVolume := &ateapipb.Volume{Name: "data", DurableDir: &ateapipb.DurableDirVolumeSource{}} scratchVolume := &ateapipb.Volume{Name: "scratch", DurableDir: &ateapipb.DurableDirVolumeSource{}} + gvisorConfig := &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"} + microvmConfig := &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_MICROVM, ConfigName: "microvm"} templates := map[string]struct { - mountPath string - volumes []*ateapipb.Volume + mountPath string + volumes []*ateapipb.Volume + sandboxConfig *ateapipb.SandboxConfig }{ - "tmpl-a": {"/data", []*ateapipb.Volume{dataVolume}}, - "tmpl-b": {"/data", []*ateapipb.Volume{dataVolume}}, - "tmpl-c": {"/mnt/data", []*ateapipb.Volume{dataVolume}}, - "tmpl-d": {"/data", []*ateapipb.Volume{dataVolume, scratchVolume}}, + "tmpl-a": {"/data", []*ateapipb.Volume{dataVolume}, gvisorConfig}, + "tmpl-b": {"/data", []*ateapipb.Volume{dataVolume}, gvisorConfig}, + "tmpl-c": {"/mnt/data", []*ateapipb.Volume{dataVolume}, gvisorConfig}, + "tmpl-d": {"/data", []*ateapipb.Volume{dataVolume, scratchVolume}, gvisorConfig}, + "tmpl-e": {"/data", []*ateapipb.Volume{dataVolume}, microvmConfig}, } for name, tmpl := range templates { if _, err := persistence.CreateActorTemplate(ctx, &ateapipb.ActorTemplate{ @@ -901,6 +906,7 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { }}, Volumes: tmpl.volumes, SnapshotsConfig: &ateapipb.SnapshotsConfig{StorageLocation: "gs://my-bucket/snapshots"}, + SandboxConfig: tmpl.sandboxConfig, }); err != nil { t.Fatalf("creating template %s: %v", name, err) } @@ -940,6 +946,15 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { t.Fatalf("UpdateActor to a template with different volumes = %v, want FailedPrecondition (err: %v)", got, err) } + // Repointing at a template with a different sandbox class is rejected. + _, err = svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ + Metadata: created.GetMetadata(), + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl-e"}, + }}) + if got := status.Code(err); got != codes.FailedPrecondition { + t.Fatalf("UpdateActor to a template with a different sandbox class = %v, want FailedPrecondition (err: %v)", got, err) + } + // Repointing at an existing template with identical volumes and mounts // succeeds. updated, err := svc.UpdateActor(ctx, &ateapipb.UpdateActorRequest{Actor: &ateapipb.Actor{ @@ -983,6 +998,54 @@ func TestUpdateActor_RepointTemplate(t *testing.T) { } } +// TestValidateTemplateSandboxClassUnchanged exercises the sandbox class +// comparison applied when an actor is repointed at a replacement template. +func TestValidateTemplateSandboxClassUnchanged(t *testing.T) { + template := func(config *ateapipb.SandboxConfig) *ateapipb.ActorTemplate { + return &ateapipb.ActorTemplate{SandboxConfig: config} + } + gvisorDefault := &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default"} + gvisorNightly := &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-nightly"} + microvm := &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_MICROVM, ConfigName: "microvm"} + + tests := []struct { + name string + oldTmpl, newTmpl *ateapipb.ActorTemplate + wantErr bool + }{{ + name: "same sandbox class", + oldTmpl: template(gvisorDefault), + newTmpl: template(gvisorDefault), + }, { + name: "same class with a different config name", + oldTmpl: template(gvisorDefault), + newTmpl: template(gvisorNightly), + }, { + name: "class changed", + oldTmpl: template(gvisorDefault), + newTmpl: template(microvm), + wantErr: true, + }, { + name: "class set on the new template only", + oldTmpl: template(nil), + newTmpl: template(microvm), + wantErr: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateTemplateSandboxClassUnchanged(tt.oldTmpl, tt.newTmpl) + if gotErr := err != nil; gotErr != tt.wantErr { + t.Fatalf("validateTemplateSandboxClassUnchanged() error = %v, wantErr %v", err, tt.wantErr) + } + if err != nil { + if got := status.Code(err); got != codes.FailedPrecondition { + t.Errorf("status code = %v, want FailedPrecondition", got) + } + } + }) + } +} + // TestValidateTemplateVolumesUnchanged exercises the volumes and // per-container mount comparison applied when an actor is repointed at a // replacement template.