From 000d9810d78530c1a1556a10986cfaaf283b2d01 Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 13:49:06 -0700 Subject: [PATCH 1/6] Add atespace fields to ateapi proto --- pkg/proto/ateapipb/ateapi.pb.go | 464 +++++++++++++++++++++----------- pkg/proto/ateapipb/ateapi.proto | 28 ++ 2 files changed, 333 insertions(+), 159 deletions(-) diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index f9040f2d..dcba4173 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -405,8 +405,11 @@ type Actor struct { // suspend/pause since eligibility is no longer a single fixed pool // reference on the ActorTemplate. WorkerPoolName string `protobuf:"bytes,14,opt,name=worker_pool_name,json=workerPoolName,proto3" json:"worker_pool_name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The atespace (tenant boundary) this actor belongs to. Part of the actor's + // resource identity; folded into the Redis key as actor::. + Atespace string `protobuf:"bytes,15,opt,name=atespace,proto3" json:"atespace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Actor) Reset() { @@ -530,16 +533,70 @@ func (x *Actor) GetWorkerPoolName() string { return "" } +func (x *Actor) GetAtespace() string { + if x != nil { + return x.Atespace + } + return "" +} + +// Atespace is the tenant boundary an Actor is created into. Placeholder for now +// (name only). +type Atespace struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Atespace) Reset() { + *x = Atespace{} + mi := &file_ateapi_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Atespace) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Atespace) ProtoMessage() {} + +func (x *Atespace) ProtoReflect() protoreflect.Message { + mi := &file_ateapi_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Atespace.ProtoReflect.Descriptor instead. +func (*Atespace) Descriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{5} +} + +func (x *Atespace) GetName() string { + if x != nil { + return x.Name + } + return "" +} + type GetActorRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ActorId string `protobuf:"bytes,1,opt,name=actor_id,json=actorId,proto3" json:"actor_id,omitempty"` + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetActorRequest) Reset() { *x = GetActorRequest{} - mi := &file_ateapi_proto_msgTypes[5] + mi := &file_ateapi_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -551,7 +608,7 @@ func (x *GetActorRequest) String() string { func (*GetActorRequest) ProtoMessage() {} func (x *GetActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[5] + mi := &file_ateapi_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -564,7 +621,7 @@ func (x *GetActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActorRequest.ProtoReflect.Descriptor instead. func (*GetActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{5} + return file_ateapi_proto_rawDescGZIP(), []int{6} } func (x *GetActorRequest) GetActorId() string { @@ -574,6 +631,13 @@ func (x *GetActorRequest) GetActorId() string { return "" } +func (x *GetActorRequest) GetAtespace() string { + if x != nil { + return x.Atespace + } + return "" +} + type GetActorResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Actor *Actor `protobuf:"bytes,1,opt,name=actor,proto3" json:"actor,omitempty"` @@ -583,7 +647,7 @@ type GetActorResponse struct { func (x *GetActorResponse) Reset() { *x = GetActorResponse{} - mi := &file_ateapi_proto_msgTypes[6] + mi := &file_ateapi_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -595,7 +659,7 @@ func (x *GetActorResponse) String() string { func (*GetActorResponse) ProtoMessage() {} func (x *GetActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[6] + mi := &file_ateapi_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -608,7 +672,7 @@ func (x *GetActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActorResponse.ProtoReflect.Descriptor instead. func (*GetActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{6} + return file_ateapi_proto_rawDescGZIP(), []int{7} } func (x *GetActorResponse) GetActor() *Actor { @@ -630,13 +694,15 @@ type CreateActorRequest struct { // worker_selector sets the actor's placement constraint at creation time. // If empty, the actor matches any pool admitted by the template's selector. WorkerSelector *Selector `protobuf:"bytes,4,opt,name=worker_selector,json=workerSelector,proto3" json:"worker_selector,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The atespace to create the actor into. + Atespace string `protobuf:"bytes,5,opt,name=atespace,proto3" json:"atespace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateActorRequest) Reset() { *x = CreateActorRequest{} - mi := &file_ateapi_proto_msgTypes[7] + mi := &file_ateapi_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -648,7 +714,7 @@ func (x *CreateActorRequest) String() string { func (*CreateActorRequest) ProtoMessage() {} func (x *CreateActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[7] + mi := &file_ateapi_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -661,7 +727,7 @@ func (x *CreateActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateActorRequest.ProtoReflect.Descriptor instead. func (*CreateActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{7} + return file_ateapi_proto_rawDescGZIP(), []int{8} } func (x *CreateActorRequest) GetActorId() string { @@ -692,6 +758,13 @@ func (x *CreateActorRequest) GetWorkerSelector() *Selector { return nil } +func (x *CreateActorRequest) GetAtespace() string { + if x != nil { + return x.Atespace + } + return "" +} + type CreateActorResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Actor *Actor `protobuf:"bytes,1,opt,name=actor,proto3" json:"actor,omitempty"` @@ -701,7 +774,7 @@ type CreateActorResponse struct { func (x *CreateActorResponse) Reset() { *x = CreateActorResponse{} - mi := &file_ateapi_proto_msgTypes[8] + mi := &file_ateapi_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -713,7 +786,7 @@ func (x *CreateActorResponse) String() string { func (*CreateActorResponse) ProtoMessage() {} func (x *CreateActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[8] + mi := &file_ateapi_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -726,7 +799,7 @@ func (x *CreateActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateActorResponse.ProtoReflect.Descriptor instead. func (*CreateActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{8} + return file_ateapi_proto_rawDescGZIP(), []int{9} } func (x *CreateActorResponse) GetActor() *Actor { @@ -745,13 +818,15 @@ type UpdateActorRequest struct { // worker_selector replaces the actor's current placement constraint. // Takes effect on the next ResumeActor call. WorkerSelector *Selector `protobuf:"bytes,2,opt,name=worker_selector,json=workerSelector,proto3" json:"worker_selector,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The atespace the actor lives in. + Atespace string `protobuf:"bytes,3,opt,name=atespace,proto3" json:"atespace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdateActorRequest) Reset() { *x = UpdateActorRequest{} - mi := &file_ateapi_proto_msgTypes[9] + mi := &file_ateapi_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -763,7 +838,7 @@ func (x *UpdateActorRequest) String() string { func (*UpdateActorRequest) ProtoMessage() {} func (x *UpdateActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[9] + mi := &file_ateapi_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -776,7 +851,7 @@ func (x *UpdateActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateActorRequest.ProtoReflect.Descriptor instead. func (*UpdateActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{9} + return file_ateapi_proto_rawDescGZIP(), []int{10} } func (x *UpdateActorRequest) GetActorId() string { @@ -793,6 +868,13 @@ func (x *UpdateActorRequest) GetWorkerSelector() *Selector { return nil } +func (x *UpdateActorRequest) GetAtespace() string { + if x != nil { + return x.Atespace + } + return "" +} + type UpdateActorResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Actor *Actor `protobuf:"bytes,1,opt,name=actor,proto3" json:"actor,omitempty"` @@ -802,7 +884,7 @@ type UpdateActorResponse struct { func (x *UpdateActorResponse) Reset() { *x = UpdateActorResponse{} - mi := &file_ateapi_proto_msgTypes[10] + mi := &file_ateapi_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -814,7 +896,7 @@ func (x *UpdateActorResponse) String() string { func (*UpdateActorResponse) ProtoMessage() {} func (x *UpdateActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[10] + mi := &file_ateapi_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -827,7 +909,7 @@ func (x *UpdateActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateActorResponse.ProtoReflect.Descriptor instead. func (*UpdateActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{10} + return file_ateapi_proto_rawDescGZIP(), []int{11} } func (x *UpdateActorResponse) GetActor() *Actor { @@ -840,13 +922,14 @@ func (x *UpdateActorResponse) GetActor() *Actor { type SuspendActorRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ActorId string `protobuf:"bytes,1,opt,name=actor_id,json=actorId,proto3" json:"actor_id,omitempty"` + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SuspendActorRequest) Reset() { *x = SuspendActorRequest{} - mi := &file_ateapi_proto_msgTypes[11] + mi := &file_ateapi_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -858,7 +941,7 @@ func (x *SuspendActorRequest) String() string { func (*SuspendActorRequest) ProtoMessage() {} func (x *SuspendActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[11] + mi := &file_ateapi_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -871,7 +954,7 @@ func (x *SuspendActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SuspendActorRequest.ProtoReflect.Descriptor instead. func (*SuspendActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{11} + return file_ateapi_proto_rawDescGZIP(), []int{12} } func (x *SuspendActorRequest) GetActorId() string { @@ -881,6 +964,13 @@ func (x *SuspendActorRequest) GetActorId() string { return "" } +func (x *SuspendActorRequest) GetAtespace() string { + if x != nil { + return x.Atespace + } + return "" +} + type SuspendActorResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Actor *Actor `protobuf:"bytes,1,opt,name=actor,proto3" json:"actor,omitempty"` @@ -890,7 +980,7 @@ type SuspendActorResponse struct { func (x *SuspendActorResponse) Reset() { *x = SuspendActorResponse{} - mi := &file_ateapi_proto_msgTypes[12] + mi := &file_ateapi_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -902,7 +992,7 @@ func (x *SuspendActorResponse) String() string { func (*SuspendActorResponse) ProtoMessage() {} func (x *SuspendActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[12] + mi := &file_ateapi_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -915,7 +1005,7 @@ func (x *SuspendActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SuspendActorResponse.ProtoReflect.Descriptor instead. func (*SuspendActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{12} + return file_ateapi_proto_rawDescGZIP(), []int{13} } func (x *SuspendActorResponse) GetActor() *Actor { @@ -928,13 +1018,14 @@ func (x *SuspendActorResponse) GetActor() *Actor { type PauseActorRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ActorId string `protobuf:"bytes,1,opt,name=actor_id,json=actorId,proto3" json:"actor_id,omitempty"` + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PauseActorRequest) Reset() { *x = PauseActorRequest{} - mi := &file_ateapi_proto_msgTypes[13] + mi := &file_ateapi_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -946,7 +1037,7 @@ func (x *PauseActorRequest) String() string { func (*PauseActorRequest) ProtoMessage() {} func (x *PauseActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[13] + mi := &file_ateapi_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -959,7 +1050,7 @@ func (x *PauseActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PauseActorRequest.ProtoReflect.Descriptor instead. func (*PauseActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{13} + return file_ateapi_proto_rawDescGZIP(), []int{14} } func (x *PauseActorRequest) GetActorId() string { @@ -969,6 +1060,13 @@ func (x *PauseActorRequest) GetActorId() string { return "" } +func (x *PauseActorRequest) GetAtespace() string { + if x != nil { + return x.Atespace + } + return "" +} + type PauseActorResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Actor *Actor `protobuf:"bytes,1,opt,name=actor,proto3" json:"actor,omitempty"` @@ -978,7 +1076,7 @@ type PauseActorResponse struct { func (x *PauseActorResponse) Reset() { *x = PauseActorResponse{} - mi := &file_ateapi_proto_msgTypes[14] + mi := &file_ateapi_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -990,7 +1088,7 @@ func (x *PauseActorResponse) String() string { func (*PauseActorResponse) ProtoMessage() {} func (x *PauseActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[14] + mi := &file_ateapi_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1003,7 +1101,7 @@ func (x *PauseActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PauseActorResponse.ProtoReflect.Descriptor instead. func (*PauseActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{14} + return file_ateapi_proto_rawDescGZIP(), []int{15} } func (x *PauseActorResponse) GetActor() *Actor { @@ -1017,14 +1115,15 @@ type ResumeActorRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ActorId string `protobuf:"bytes,1,opt,name=actor_id,json=actorId,proto3" json:"actor_id,omitempty"` // If true, skip golden snapshot and boot the workload from scratch. - Boot bool `protobuf:"varint,2,opt,name=boot,proto3" json:"boot,omitempty"` + Boot bool `protobuf:"varint,2,opt,name=boot,proto3" json:"boot,omitempty"` + Atespace string `protobuf:"bytes,3,opt,name=atespace,proto3" json:"atespace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ResumeActorRequest) Reset() { *x = ResumeActorRequest{} - mi := &file_ateapi_proto_msgTypes[15] + mi := &file_ateapi_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1036,7 +1135,7 @@ func (x *ResumeActorRequest) String() string { func (*ResumeActorRequest) ProtoMessage() {} func (x *ResumeActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[15] + mi := &file_ateapi_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1049,7 +1148,7 @@ func (x *ResumeActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResumeActorRequest.ProtoReflect.Descriptor instead. func (*ResumeActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{15} + return file_ateapi_proto_rawDescGZIP(), []int{16} } func (x *ResumeActorRequest) GetActorId() string { @@ -1066,6 +1165,13 @@ func (x *ResumeActorRequest) GetBoot() bool { return false } +func (x *ResumeActorRequest) GetAtespace() string { + if x != nil { + return x.Atespace + } + return "" +} + type ResumeActorResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Actor *Actor `protobuf:"bytes,1,opt,name=actor,proto3" json:"actor,omitempty"` @@ -1075,7 +1181,7 @@ type ResumeActorResponse struct { func (x *ResumeActorResponse) Reset() { *x = ResumeActorResponse{} - mi := &file_ateapi_proto_msgTypes[16] + mi := &file_ateapi_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1087,7 +1193,7 @@ func (x *ResumeActorResponse) String() string { func (*ResumeActorResponse) ProtoMessage() {} func (x *ResumeActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[16] + mi := &file_ateapi_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1100,7 +1206,7 @@ func (x *ResumeActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResumeActorResponse.ProtoReflect.Descriptor instead. func (*ResumeActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{16} + return file_ateapi_proto_rawDescGZIP(), []int{17} } func (x *ResumeActorResponse) GetActor() *Actor { @@ -1113,13 +1219,14 @@ func (x *ResumeActorResponse) GetActor() *Actor { type DeleteActorRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ActorId string `protobuf:"bytes,1,opt,name=actor_id,json=actorId,proto3" json:"actor_id,omitempty"` + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DeleteActorRequest) Reset() { *x = DeleteActorRequest{} - mi := &file_ateapi_proto_msgTypes[17] + mi := &file_ateapi_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1131,7 +1238,7 @@ func (x *DeleteActorRequest) String() string { func (*DeleteActorRequest) ProtoMessage() {} func (x *DeleteActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[17] + mi := &file_ateapi_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1144,7 +1251,7 @@ func (x *DeleteActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteActorRequest.ProtoReflect.Descriptor instead. func (*DeleteActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{17} + return file_ateapi_proto_rawDescGZIP(), []int{18} } func (x *DeleteActorRequest) GetActorId() string { @@ -1154,6 +1261,13 @@ func (x *DeleteActorRequest) GetActorId() string { return "" } +func (x *DeleteActorRequest) GetAtespace() string { + if x != nil { + return x.Atespace + } + return "" +} + type DeleteActorResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -1162,7 +1276,7 @@ type DeleteActorResponse struct { func (x *DeleteActorResponse) Reset() { *x = DeleteActorResponse{} - mi := &file_ateapi_proto_msgTypes[18] + mi := &file_ateapi_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1174,7 +1288,7 @@ func (x *DeleteActorResponse) String() string { func (*DeleteActorResponse) ProtoMessage() {} func (x *DeleteActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[18] + mi := &file_ateapi_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1187,7 +1301,7 @@ func (x *DeleteActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteActorResponse.ProtoReflect.Descriptor instead. func (*DeleteActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{18} + return file_ateapi_proto_rawDescGZIP(), []int{19} } type ListWorkersRequest struct { @@ -1198,7 +1312,7 @@ type ListWorkersRequest struct { func (x *ListWorkersRequest) Reset() { *x = ListWorkersRequest{} - mi := &file_ateapi_proto_msgTypes[19] + mi := &file_ateapi_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1210,7 +1324,7 @@ func (x *ListWorkersRequest) String() string { func (*ListWorkersRequest) ProtoMessage() {} func (x *ListWorkersRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[19] + mi := &file_ateapi_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1223,7 +1337,7 @@ func (x *ListWorkersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkersRequest.ProtoReflect.Descriptor instead. func (*ListWorkersRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{19} + return file_ateapi_proto_rawDescGZIP(), []int{20} } type ListWorkersResponse struct { @@ -1235,7 +1349,7 @@ type ListWorkersResponse struct { func (x *ListWorkersResponse) Reset() { *x = ListWorkersResponse{} - mi := &file_ateapi_proto_msgTypes[20] + mi := &file_ateapi_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1247,7 +1361,7 @@ func (x *ListWorkersResponse) String() string { func (*ListWorkersResponse) ProtoMessage() {} func (x *ListWorkersResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[20] + mi := &file_ateapi_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1260,7 +1374,7 @@ func (x *ListWorkersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkersResponse.ProtoReflect.Descriptor instead. func (*ListWorkersResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{20} + return file_ateapi_proto_rawDescGZIP(), []int{21} } func (x *ListWorkersResponse) GetWorkers() []*Worker { @@ -1280,14 +1394,16 @@ type ListActorsRequest struct { PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` // An opaque pagination token obtained from a previous ListActorsResponse. // Empty for the first request. - PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // If set, list only actors in this atespace (scoped SCAN actor::*). + Atespace string `protobuf:"bytes,3,opt,name=atespace,proto3" json:"atespace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListActorsRequest) Reset() { *x = ListActorsRequest{} - mi := &file_ateapi_proto_msgTypes[21] + mi := &file_ateapi_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1299,7 +1415,7 @@ func (x *ListActorsRequest) String() string { func (*ListActorsRequest) ProtoMessage() {} func (x *ListActorsRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[21] + mi := &file_ateapi_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1312,7 +1428,7 @@ func (x *ListActorsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListActorsRequest.ProtoReflect.Descriptor instead. func (*ListActorsRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{21} + return file_ateapi_proto_rawDescGZIP(), []int{22} } func (x *ListActorsRequest) GetPageSize() int32 { @@ -1329,6 +1445,13 @@ func (x *ListActorsRequest) GetPageToken() string { return "" } +func (x *ListActorsRequest) GetAtespace() string { + if x != nil { + return x.Atespace + } + return "" +} + // Response for a ListActors operation. type ListActorsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1343,7 +1466,7 @@ type ListActorsResponse struct { func (x *ListActorsResponse) Reset() { *x = ListActorsResponse{} - mi := &file_ateapi_proto_msgTypes[22] + mi := &file_ateapi_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1355,7 +1478,7 @@ func (x *ListActorsResponse) String() string { func (*ListActorsResponse) ProtoMessage() {} func (x *ListActorsResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[22] + mi := &file_ateapi_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1368,7 +1491,7 @@ func (x *ListActorsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListActorsResponse.ProtoReflect.Descriptor instead. func (*ListActorsResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{22} + return file_ateapi_proto_rawDescGZIP(), []int{23} } func (x *ListActorsResponse) GetActors() []*Actor { @@ -1397,13 +1520,16 @@ type Worker struct { Version int64 `protobuf:"varint,8,opt,name=version,proto3" json:"version,omitempty"` WorkerPodUid string `protobuf:"bytes,9,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` NodeName string `protobuf:"bytes,10,opt,name=node_name,json=nodeName,proto3" json:"node_name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The atespace of the actor currently running on this worker. Set when an actor + // is assigned, cleared when freed. It is NOT the worker's own atespace. + ActorAtespace string `protobuf:"bytes,11,opt,name=actor_atespace,json=actorAtespace,proto3" json:"actor_atespace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Worker) Reset() { *x = Worker{} - mi := &file_ateapi_proto_msgTypes[23] + mi := &file_ateapi_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1415,7 +1541,7 @@ func (x *Worker) String() string { func (*Worker) ProtoMessage() {} func (x *Worker) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[23] + mi := &file_ateapi_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1428,7 +1554,7 @@ func (x *Worker) ProtoReflect() protoreflect.Message { // Deprecated: Use Worker.ProtoReflect.Descriptor instead. func (*Worker) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{23} + return file_ateapi_proto_rawDescGZIP(), []int{24} } func (x *Worker) GetWorkerNamespace() string { @@ -1501,6 +1627,13 @@ func (x *Worker) GetNodeName() string { return "" } +func (x *Worker) GetActorAtespace() string { + if x != nil { + return x.ActorAtespace + } + return "" +} + type DebugClearRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -1509,7 +1642,7 @@ type DebugClearRequest struct { func (x *DebugClearRequest) Reset() { *x = DebugClearRequest{} - mi := &file_ateapi_proto_msgTypes[24] + mi := &file_ateapi_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1521,7 +1654,7 @@ func (x *DebugClearRequest) String() string { func (*DebugClearRequest) ProtoMessage() {} func (x *DebugClearRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[24] + mi := &file_ateapi_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1534,7 +1667,7 @@ func (x *DebugClearRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DebugClearRequest.ProtoReflect.Descriptor instead. func (*DebugClearRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{24} + return file_ateapi_proto_rawDescGZIP(), []int{25} } type DebugClearResponse struct { @@ -1545,7 +1678,7 @@ type DebugClearResponse struct { func (x *DebugClearResponse) Reset() { *x = DebugClearResponse{} - mi := &file_ateapi_proto_msgTypes[25] + mi := &file_ateapi_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1557,7 +1690,7 @@ func (x *DebugClearResponse) String() string { func (*DebugClearResponse) ProtoMessage() {} func (x *DebugClearResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[25] + mi := &file_ateapi_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1570,7 +1703,7 @@ func (x *DebugClearResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DebugClearResponse.ProtoReflect.Descriptor instead. func (*DebugClearResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{25} + return file_ateapi_proto_rawDescGZIP(), []int{26} } type MintJWTRequest struct { @@ -1585,7 +1718,7 @@ type MintJWTRequest struct { func (x *MintJWTRequest) Reset() { *x = MintJWTRequest{} - mi := &file_ateapi_proto_msgTypes[26] + mi := &file_ateapi_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1597,7 +1730,7 @@ func (x *MintJWTRequest) String() string { func (*MintJWTRequest) ProtoMessage() {} func (x *MintJWTRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[26] + mi := &file_ateapi_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1610,7 +1743,7 @@ func (x *MintJWTRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MintJWTRequest.ProtoReflect.Descriptor instead. func (*MintJWTRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{26} + return file_ateapi_proto_rawDescGZIP(), []int{27} } func (x *MintJWTRequest) GetAudience() []string { @@ -1670,7 +1803,7 @@ type MintJWTResponse struct { func (x *MintJWTResponse) Reset() { *x = MintJWTResponse{} - mi := &file_ateapi_proto_msgTypes[27] + mi := &file_ateapi_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1682,7 +1815,7 @@ func (x *MintJWTResponse) String() string { func (*MintJWTResponse) ProtoMessage() {} func (x *MintJWTResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[27] + mi := &file_ateapi_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1695,7 +1828,7 @@ func (x *MintJWTResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MintJWTResponse.ProtoReflect.Descriptor instead. func (*MintJWTResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{27} + return file_ateapi_proto_rawDescGZIP(), []int{28} } func (x *MintJWTResponse) GetSessionJwt() string { @@ -1720,7 +1853,7 @@ type MintCertRequest struct { func (x *MintCertRequest) Reset() { *x = MintCertRequest{} - mi := &file_ateapi_proto_msgTypes[28] + mi := &file_ateapi_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1732,7 +1865,7 @@ func (x *MintCertRequest) String() string { func (*MintCertRequest) ProtoMessage() {} func (x *MintCertRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[28] + mi := &file_ateapi_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1745,7 +1878,7 @@ func (x *MintCertRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MintCertRequest.ProtoReflect.Descriptor instead. func (*MintCertRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{28} + return file_ateapi_proto_rawDescGZIP(), []int{29} } func (x *MintCertRequest) GetAppId() string { @@ -1788,7 +1921,7 @@ type MintCertResponse struct { func (x *MintCertResponse) Reset() { *x = MintCertResponse{} - mi := &file_ateapi_proto_msgTypes[29] + mi := &file_ateapi_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1800,7 +1933,7 @@ func (x *MintCertResponse) String() string { func (*MintCertResponse) ProtoMessage() {} func (x *MintCertResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[29] + mi := &file_ateapi_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1813,7 +1946,7 @@ func (x *MintCertResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MintCertResponse.ProtoReflect.Descriptor instead. func (*MintCertResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{29} + return file_ateapi_proto_rawDescGZIP(), []int{30} } func (x *MintCertResponse) GetSessionCertificates() [][]byte { @@ -1842,7 +1975,7 @@ const file_ateapi_proto_rawDesc = "" + "\fmatch_labels\x18\x01 \x03(\v2!.ateapi.Selector.MatchLabelsEntryR\vmatchLabels\x1a>\n" + "\x10MatchLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xf5\x05\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x91\x06\n" + "\x05Actor\x12\x19\n" + "\bactor_id\x18\x01 \x01(\tR\aactorId\x12\x18\n" + "\aversion\x18\x02 \x01(\x03R\aversion\x128\n" + @@ -1858,7 +1991,8 @@ const file_ateapi_proto_rawDesc = "" + "\rateom_pod_uid\x18\v \x01(\tR\vateomPodUid\x12F\n" + "\x14latest_snapshot_info\x18\f \x01(\v2\x14.ateapi.SnapshotInfoR\x12latestSnapshotInfo\x129\n" + "\x0fworker_selector\x18\r \x01(\v2\x10.ateapi.SelectorR\x0eworkerSelector\x12(\n" + - "\x10worker_pool_name\x18\x0e \x01(\tR\x0eworkerPoolName\"\x9d\x01\n" + + "\x10worker_pool_name\x18\x0e \x01(\tR\x0eworkerPoolName\x12\x1a\n" + + "\batespace\x18\x0f \x01(\tR\batespace\"\x9d\x01\n" + "\x06Status\x12\x16\n" + "\x12STATUS_UNSPECIFIED\x10\x00\x12\x13\n" + "\x0fSTATUS_RESUMING\x10\x01\x12\x12\n" + @@ -1867,49 +2001,59 @@ const file_ateapi_proto_rawDesc = "" + "\x10STATUS_SUSPENDED\x10\x04\x12\x12\n" + "\x0eSTATUS_PAUSING\x10\x05\x12\x11\n" + "\rSTATUS_PAUSED\x10\x06J\x04\b\t\x10\n" + - "\",\n" + + "\"\x1e\n" + + "\bAtespace\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"H\n" + "\x0fGetActorRequest\x12\x19\n" + - "\bactor_id\x18\x01 \x01(\tR\aactorId\"7\n" + + "\bactor_id\x18\x01 \x01(\tR\aactorId\x12\x1a\n" + + "\batespace\x18\x02 \x01(\tR\batespace\"7\n" + "\x10GetActorResponse\x12#\n" + - "\x05actor\x18\x01 \x01(\v2\r.ateapi.ActorR\x05actor\"\xd4\x01\n" + + "\x05actor\x18\x01 \x01(\v2\r.ateapi.ActorR\x05actor\"\xf0\x01\n" + "\x12CreateActorRequest\x12\x19\n" + "\bactor_id\x18\x01 \x01(\tR\aactorId\x128\n" + "\x18actor_template_namespace\x18\x02 \x01(\tR\x16actorTemplateNamespace\x12.\n" + "\x13actor_template_name\x18\x03 \x01(\tR\x11actorTemplateName\x129\n" + - "\x0fworker_selector\x18\x04 \x01(\v2\x10.ateapi.SelectorR\x0eworkerSelector\":\n" + + "\x0fworker_selector\x18\x04 \x01(\v2\x10.ateapi.SelectorR\x0eworkerSelector\x12\x1a\n" + + "\batespace\x18\x05 \x01(\tR\batespace\":\n" + "\x13CreateActorResponse\x12#\n" + - "\x05actor\x18\x01 \x01(\v2\r.ateapi.ActorR\x05actor\"j\n" + + "\x05actor\x18\x01 \x01(\v2\r.ateapi.ActorR\x05actor\"\x86\x01\n" + "\x12UpdateActorRequest\x12\x19\n" + "\bactor_id\x18\x01 \x01(\tR\aactorId\x129\n" + - "\x0fworker_selector\x18\x02 \x01(\v2\x10.ateapi.SelectorR\x0eworkerSelector\":\n" + + "\x0fworker_selector\x18\x02 \x01(\v2\x10.ateapi.SelectorR\x0eworkerSelector\x12\x1a\n" + + "\batespace\x18\x03 \x01(\tR\batespace\":\n" + "\x13UpdateActorResponse\x12#\n" + - "\x05actor\x18\x01 \x01(\v2\r.ateapi.ActorR\x05actor\"0\n" + + "\x05actor\x18\x01 \x01(\v2\r.ateapi.ActorR\x05actor\"L\n" + "\x13SuspendActorRequest\x12\x19\n" + - "\bactor_id\x18\x01 \x01(\tR\aactorId\";\n" + + "\bactor_id\x18\x01 \x01(\tR\aactorId\x12\x1a\n" + + "\batespace\x18\x02 \x01(\tR\batespace\";\n" + "\x14SuspendActorResponse\x12#\n" + - "\x05actor\x18\x01 \x01(\v2\r.ateapi.ActorR\x05actor\".\n" + + "\x05actor\x18\x01 \x01(\v2\r.ateapi.ActorR\x05actor\"J\n" + "\x11PauseActorRequest\x12\x19\n" + - "\bactor_id\x18\x01 \x01(\tR\aactorId\"9\n" + + "\bactor_id\x18\x01 \x01(\tR\aactorId\x12\x1a\n" + + "\batespace\x18\x02 \x01(\tR\batespace\"9\n" + "\x12PauseActorResponse\x12#\n" + - "\x05actor\x18\x01 \x01(\v2\r.ateapi.ActorR\x05actor\"C\n" + + "\x05actor\x18\x01 \x01(\v2\r.ateapi.ActorR\x05actor\"_\n" + "\x12ResumeActorRequest\x12\x19\n" + "\bactor_id\x18\x01 \x01(\tR\aactorId\x12\x12\n" + - "\x04boot\x18\x02 \x01(\bR\x04boot\":\n" + + "\x04boot\x18\x02 \x01(\bR\x04boot\x12\x1a\n" + + "\batespace\x18\x03 \x01(\tR\batespace\":\n" + "\x13ResumeActorResponse\x12#\n" + - "\x05actor\x18\x01 \x01(\v2\r.ateapi.ActorR\x05actor\"/\n" + + "\x05actor\x18\x01 \x01(\v2\r.ateapi.ActorR\x05actor\"K\n" + "\x12DeleteActorRequest\x12\x19\n" + - "\bactor_id\x18\x01 \x01(\tR\aactorId\"\x15\n" + + "\bactor_id\x18\x01 \x01(\tR\aactorId\x12\x1a\n" + + "\batespace\x18\x02 \x01(\tR\batespace\"\x15\n" + "\x13DeleteActorResponse\"\x14\n" + "\x12ListWorkersRequest\"?\n" + "\x13ListWorkersResponse\x12(\n" + - "\aworkers\x18\x01 \x03(\v2\x0e.ateapi.WorkerR\aworkers\"O\n" + + "\aworkers\x18\x01 \x03(\v2\x0e.ateapi.WorkerR\aworkers\"k\n" + "\x11ListActorsRequest\x12\x1b\n" + "\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" + "\n" + - "page_token\x18\x02 \x01(\tR\tpageToken\"c\n" + + "page_token\x18\x02 \x01(\tR\tpageToken\x12\x1a\n" + + "\batespace\x18\x03 \x01(\tR\batespace\"c\n" + "\x12ListActorsResponse\x12%\n" + "\x06actors\x18\x01 \x03(\v2\r.ateapi.ActorR\x06actors\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xcb\x02\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xf2\x02\n" + "\x06Worker\x12)\n" + "\x10worker_namespace\x18\x01 \x01(\tR\x0fworkerNamespace\x12\x1f\n" + "\vworker_pool\x18\x02 \x01(\tR\n" + @@ -1923,7 +2067,8 @@ const file_ateapi_proto_rawDesc = "" + "\aversion\x18\b \x01(\x03R\aversion\x12$\n" + "\x0eworker_pod_uid\x18\t \x01(\tR\fworkerPodUid\x12\x1b\n" + "\tnode_name\x18\n" + - " \x01(\tR\bnodeName\"\x13\n" + + " \x01(\tR\bnodeName\x12%\n" + + "\x0eactor_atespace\x18\v \x01(\tR\ractorAtespace\"\x13\n" + "\x11DebugClearRequest\"\x14\n" + "\x12DebugClearResponse\"{\n" + "\x0eMintJWTRequest\x12\x1a\n" + @@ -1978,7 +2123,7 @@ func file_ateapi_proto_rawDescGZIP() []byte { } var file_ateapi_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_ateapi_proto_msgTypes = make([]protoimpl.MessageInfo, 31) +var file_ateapi_proto_msgTypes = make([]protoimpl.MessageInfo, 32) var file_ateapi_proto_goTypes = []any{ (SnapshotType)(0), // 0: ateapi.SnapshotType (Actor_Status)(0), // 1: ateapi.Actor.Status @@ -1987,38 +2132,39 @@ var file_ateapi_proto_goTypes = []any{ (*SnapshotInfo)(nil), // 4: ateapi.SnapshotInfo (*Selector)(nil), // 5: ateapi.Selector (*Actor)(nil), // 6: ateapi.Actor - (*GetActorRequest)(nil), // 7: ateapi.GetActorRequest - (*GetActorResponse)(nil), // 8: ateapi.GetActorResponse - (*CreateActorRequest)(nil), // 9: ateapi.CreateActorRequest - (*CreateActorResponse)(nil), // 10: ateapi.CreateActorResponse - (*UpdateActorRequest)(nil), // 11: ateapi.UpdateActorRequest - (*UpdateActorResponse)(nil), // 12: ateapi.UpdateActorResponse - (*SuspendActorRequest)(nil), // 13: ateapi.SuspendActorRequest - (*SuspendActorResponse)(nil), // 14: ateapi.SuspendActorResponse - (*PauseActorRequest)(nil), // 15: ateapi.PauseActorRequest - (*PauseActorResponse)(nil), // 16: ateapi.PauseActorResponse - (*ResumeActorRequest)(nil), // 17: ateapi.ResumeActorRequest - (*ResumeActorResponse)(nil), // 18: ateapi.ResumeActorResponse - (*DeleteActorRequest)(nil), // 19: ateapi.DeleteActorRequest - (*DeleteActorResponse)(nil), // 20: ateapi.DeleteActorResponse - (*ListWorkersRequest)(nil), // 21: ateapi.ListWorkersRequest - (*ListWorkersResponse)(nil), // 22: ateapi.ListWorkersResponse - (*ListActorsRequest)(nil), // 23: ateapi.ListActorsRequest - (*ListActorsResponse)(nil), // 24: ateapi.ListActorsResponse - (*Worker)(nil), // 25: ateapi.Worker - (*DebugClearRequest)(nil), // 26: ateapi.DebugClearRequest - (*DebugClearResponse)(nil), // 27: ateapi.DebugClearResponse - (*MintJWTRequest)(nil), // 28: ateapi.MintJWTRequest - (*MintJWTResponse)(nil), // 29: ateapi.MintJWTResponse - (*MintCertRequest)(nil), // 30: ateapi.MintCertRequest - (*MintCertResponse)(nil), // 31: ateapi.MintCertResponse - nil, // 32: ateapi.Selector.MatchLabelsEntry + (*Atespace)(nil), // 7: ateapi.Atespace + (*GetActorRequest)(nil), // 8: ateapi.GetActorRequest + (*GetActorResponse)(nil), // 9: ateapi.GetActorResponse + (*CreateActorRequest)(nil), // 10: ateapi.CreateActorRequest + (*CreateActorResponse)(nil), // 11: ateapi.CreateActorResponse + (*UpdateActorRequest)(nil), // 12: ateapi.UpdateActorRequest + (*UpdateActorResponse)(nil), // 13: ateapi.UpdateActorResponse + (*SuspendActorRequest)(nil), // 14: ateapi.SuspendActorRequest + (*SuspendActorResponse)(nil), // 15: ateapi.SuspendActorResponse + (*PauseActorRequest)(nil), // 16: ateapi.PauseActorRequest + (*PauseActorResponse)(nil), // 17: ateapi.PauseActorResponse + (*ResumeActorRequest)(nil), // 18: ateapi.ResumeActorRequest + (*ResumeActorResponse)(nil), // 19: ateapi.ResumeActorResponse + (*DeleteActorRequest)(nil), // 20: ateapi.DeleteActorRequest + (*DeleteActorResponse)(nil), // 21: ateapi.DeleteActorResponse + (*ListWorkersRequest)(nil), // 22: ateapi.ListWorkersRequest + (*ListWorkersResponse)(nil), // 23: ateapi.ListWorkersResponse + (*ListActorsRequest)(nil), // 24: ateapi.ListActorsRequest + (*ListActorsResponse)(nil), // 25: ateapi.ListActorsResponse + (*Worker)(nil), // 26: ateapi.Worker + (*DebugClearRequest)(nil), // 27: ateapi.DebugClearRequest + (*DebugClearResponse)(nil), // 28: ateapi.DebugClearResponse + (*MintJWTRequest)(nil), // 29: ateapi.MintJWTRequest + (*MintJWTResponse)(nil), // 30: ateapi.MintJWTResponse + (*MintCertRequest)(nil), // 31: ateapi.MintCertRequest + (*MintCertResponse)(nil), // 32: ateapi.MintCertResponse + nil, // 33: ateapi.Selector.MatchLabelsEntry } var file_ateapi_proto_depIdxs = []int32{ 0, // 0: ateapi.SnapshotInfo.type:type_name -> ateapi.SnapshotType 2, // 1: ateapi.SnapshotInfo.external:type_name -> ateapi.ExternalSnapshotInfo 3, // 2: ateapi.SnapshotInfo.local:type_name -> ateapi.LocalSnapshotInfo - 32, // 3: ateapi.Selector.match_labels:type_name -> ateapi.Selector.MatchLabelsEntry + 33, // 3: ateapi.Selector.match_labels:type_name -> ateapi.Selector.MatchLabelsEntry 1, // 4: ateapi.Actor.status:type_name -> ateapi.Actor.Status 4, // 5: ateapi.Actor.latest_snapshot_info:type_name -> ateapi.SnapshotInfo 5, // 6: ateapi.Actor.worker_selector:type_name -> ateapi.Selector @@ -2030,32 +2176,32 @@ var file_ateapi_proto_depIdxs = []int32{ 6, // 12: ateapi.SuspendActorResponse.actor:type_name -> ateapi.Actor 6, // 13: ateapi.PauseActorResponse.actor:type_name -> ateapi.Actor 6, // 14: ateapi.ResumeActorResponse.actor:type_name -> ateapi.Actor - 25, // 15: ateapi.ListWorkersResponse.workers:type_name -> ateapi.Worker + 26, // 15: ateapi.ListWorkersResponse.workers:type_name -> ateapi.Worker 6, // 16: ateapi.ListActorsResponse.actors:type_name -> ateapi.Actor - 7, // 17: ateapi.Control.GetActor:input_type -> ateapi.GetActorRequest - 9, // 18: ateapi.Control.CreateActor:input_type -> ateapi.CreateActorRequest - 11, // 19: ateapi.Control.UpdateActor:input_type -> ateapi.UpdateActorRequest - 13, // 20: ateapi.Control.SuspendActor:input_type -> ateapi.SuspendActorRequest - 15, // 21: ateapi.Control.PauseActor:input_type -> ateapi.PauseActorRequest - 17, // 22: ateapi.Control.ResumeActor:input_type -> ateapi.ResumeActorRequest - 19, // 23: ateapi.Control.DeleteActor:input_type -> ateapi.DeleteActorRequest - 21, // 24: ateapi.Control.ListWorkers:input_type -> ateapi.ListWorkersRequest - 23, // 25: ateapi.Control.ListActors:input_type -> ateapi.ListActorsRequest - 26, // 26: ateapi.Control.DebugClear:input_type -> ateapi.DebugClearRequest - 28, // 27: ateapi.SessionIdentity.MintJWT:input_type -> ateapi.MintJWTRequest - 30, // 28: ateapi.SessionIdentity.MintCert:input_type -> ateapi.MintCertRequest - 8, // 29: ateapi.Control.GetActor:output_type -> ateapi.GetActorResponse - 10, // 30: ateapi.Control.CreateActor:output_type -> ateapi.CreateActorResponse - 12, // 31: ateapi.Control.UpdateActor:output_type -> ateapi.UpdateActorResponse - 14, // 32: ateapi.Control.SuspendActor:output_type -> ateapi.SuspendActorResponse - 16, // 33: ateapi.Control.PauseActor:output_type -> ateapi.PauseActorResponse - 18, // 34: ateapi.Control.ResumeActor:output_type -> ateapi.ResumeActorResponse - 20, // 35: ateapi.Control.DeleteActor:output_type -> ateapi.DeleteActorResponse - 22, // 36: ateapi.Control.ListWorkers:output_type -> ateapi.ListWorkersResponse - 24, // 37: ateapi.Control.ListActors:output_type -> ateapi.ListActorsResponse - 27, // 38: ateapi.Control.DebugClear:output_type -> ateapi.DebugClearResponse - 29, // 39: ateapi.SessionIdentity.MintJWT:output_type -> ateapi.MintJWTResponse - 31, // 40: ateapi.SessionIdentity.MintCert:output_type -> ateapi.MintCertResponse + 8, // 17: ateapi.Control.GetActor:input_type -> ateapi.GetActorRequest + 10, // 18: ateapi.Control.CreateActor:input_type -> ateapi.CreateActorRequest + 12, // 19: ateapi.Control.UpdateActor:input_type -> ateapi.UpdateActorRequest + 14, // 20: ateapi.Control.SuspendActor:input_type -> ateapi.SuspendActorRequest + 16, // 21: ateapi.Control.PauseActor:input_type -> ateapi.PauseActorRequest + 18, // 22: ateapi.Control.ResumeActor:input_type -> ateapi.ResumeActorRequest + 20, // 23: ateapi.Control.DeleteActor:input_type -> ateapi.DeleteActorRequest + 22, // 24: ateapi.Control.ListWorkers:input_type -> ateapi.ListWorkersRequest + 24, // 25: ateapi.Control.ListActors:input_type -> ateapi.ListActorsRequest + 27, // 26: ateapi.Control.DebugClear:input_type -> ateapi.DebugClearRequest + 29, // 27: ateapi.SessionIdentity.MintJWT:input_type -> ateapi.MintJWTRequest + 31, // 28: ateapi.SessionIdentity.MintCert:input_type -> ateapi.MintCertRequest + 9, // 29: ateapi.Control.GetActor:output_type -> ateapi.GetActorResponse + 11, // 30: ateapi.Control.CreateActor:output_type -> ateapi.CreateActorResponse + 13, // 31: ateapi.Control.UpdateActor:output_type -> ateapi.UpdateActorResponse + 15, // 32: ateapi.Control.SuspendActor:output_type -> ateapi.SuspendActorResponse + 17, // 33: ateapi.Control.PauseActor:output_type -> ateapi.PauseActorResponse + 19, // 34: ateapi.Control.ResumeActor:output_type -> ateapi.ResumeActorResponse + 21, // 35: ateapi.Control.DeleteActor:output_type -> ateapi.DeleteActorResponse + 23, // 36: ateapi.Control.ListWorkers:output_type -> ateapi.ListWorkersResponse + 25, // 37: ateapi.Control.ListActors:output_type -> ateapi.ListActorsResponse + 28, // 38: ateapi.Control.DebugClear:output_type -> ateapi.DebugClearResponse + 30, // 39: ateapi.SessionIdentity.MintJWT:output_type -> ateapi.MintJWTResponse + 32, // 40: ateapi.SessionIdentity.MintCert:output_type -> ateapi.MintCertResponse 29, // [29:41] is the sub-list for method output_type 17, // [17:29] is the sub-list for method input_type 17, // [17:17] is the sub-list for extension type_name @@ -2078,7 +2224,7 @@ func file_ateapi_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ateapi_proto_rawDesc), len(file_ateapi_proto_rawDesc)), NumEnums: 2, - NumMessages: 31, + NumMessages: 32, NumExtensions: 0, NumServices: 2, }, diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 20e80e5c..774c4583 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -125,10 +125,21 @@ message Actor { // suspend/pause since eligibility is no longer a single fixed pool // reference on the ActorTemplate. string worker_pool_name = 14; + + // The atespace (tenant boundary) this actor belongs to. Part of the actor's + // resource identity; folded into the Redis key as actor::. + string atespace = 15; +} + +// Atespace is the tenant boundary an Actor is created into. Placeholder for now +// (name only). +message Atespace { + string name = 1; } message GetActorRequest { string actor_id = 1; + string atespace = 2; } message GetActorResponse { @@ -149,6 +160,9 @@ message CreateActorRequest { // worker_selector sets the actor's placement constraint at creation time. // If empty, the actor matches any pool admitted by the template's selector. Selector worker_selector = 4; + + // The atespace to create the actor into. + string atespace = 5; } message CreateActorResponse { @@ -164,6 +178,9 @@ message UpdateActorRequest { // worker_selector replaces the actor's current placement constraint. // Takes effect on the next ResumeActor call. Selector worker_selector = 2; + + // The atespace the actor lives in. + string atespace = 3; } message UpdateActorResponse { @@ -172,6 +189,7 @@ message UpdateActorResponse { message SuspendActorRequest { string actor_id = 1; + string atespace = 2; } message SuspendActorResponse { @@ -180,6 +198,7 @@ message SuspendActorResponse { message PauseActorRequest { string actor_id = 1; + string atespace = 2; } message PauseActorResponse { @@ -191,6 +210,7 @@ message ResumeActorRequest { // If true, skip golden snapshot and boot the workload from scratch. bool boot = 2; + string atespace = 3; } message ResumeActorResponse { @@ -199,6 +219,7 @@ message ResumeActorResponse { message DeleteActorRequest { string actor_id = 1; + string atespace = 2; } message DeleteActorResponse {} @@ -220,6 +241,9 @@ message ListActorsRequest { // An opaque pagination token obtained from a previous ListActorsResponse. // Empty for the first request. string page_token = 2; + + // If set, list only actors in this atespace (scoped SCAN actor::*). + string atespace = 3; } // Response for a ListActors operation. @@ -244,6 +268,10 @@ message Worker { int64 version = 8; string worker_pod_uid = 9; string node_name = 10; + + // The atespace of the actor currently running on this worker. Set when an actor + // is assigned, cleared when freed. It is NOT the worker's own atespace. + string actor_atespace = 11; } message DebugClearRequest {} From b5f74746cc6dd0c8fdbb5c4cdd03dc0d0c35d275 Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 13:49:06 -0700 Subject: [PATCH 2/6] Scope actor store and handlers by atespace --- .../internal/controlapi/create_actor.go | 9 +- .../internal/controlapi/delete_actor.go | 7 +- .../internal/controlapi/functional_test.go | 262 +++++++++++++----- cmd/ateapi/internal/controlapi/get_actor.go | 5 +- cmd/ateapi/internal/controlapi/list_actors.go | 9 +- cmd/ateapi/internal/controlapi/pause_actor.go | 5 +- .../internal/controlapi/resume_actor.go | 5 +- .../internal/controlapi/suspend_actor.go | 5 +- cmd/ateapi/internal/controlapi/syncer.go | 2 +- cmd/ateapi/internal/controlapi/syncer_test.go | 6 +- .../internal/controlapi/update_actor.go | 9 +- cmd/ateapi/internal/controlapi/workflow.go | 17 +- .../internal/controlapi/workflow_pause.go | 10 +- .../internal/controlapi/workflow_resume.go | 10 +- .../internal/controlapi/workflow_suspend.go | 10 +- .../internal/store/ateredis/ateredis.go | 29 +- .../internal/store/ateredis/ateredis_test.go | 93 ++++++- cmd/ateapi/internal/store/store.go | 10 +- internal/e2e/suites/demo/demo_test.go | 49 +++- internal/e2e/suites/identity/identity_test.go | 7 +- internal/resources/actor.go | 12 + 21 files changed, 417 insertions(+), 154 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/create_actor.go b/cmd/ateapi/internal/controlapi/create_actor.go index 54e36521..cb59b318 100644 --- a/cmd/ateapi/internal/controlapi/create_actor.go +++ b/cmd/ateapi/internal/controlapi/create_actor.go @@ -49,6 +49,7 @@ func (s *Service) CreateActor(ctx context.Context, req *ateapipb.CreateActorRequ ActorTemplateNamespace: req.GetActorTemplateNamespace(), ActorTemplateName: req.GetActorTemplateName(), WorkerSelector: req.GetWorkerSelector(), + Atespace: req.GetAtespace(), } err = s.persistence.CreateActor(ctx, actor) if err != nil { @@ -58,7 +59,7 @@ func (s *Service) CreateActor(ctx context.Context, req *ateapipb.CreateActorRequ return nil, fmt.Errorf("while recording actor: %w", err) } - storedActor, err := s.persistence.GetActor(ctx, id) + storedActor, err := s.persistence.GetActor(ctx, req.GetAtespace(), id) if err != nil { return nil, fmt.Errorf("while fetching recorded actor from DB: %w", err) } @@ -81,6 +82,12 @@ func validateCreateActorRequest(req *ateapipb.CreateActorRequest) error { if err := resources.ValidateActorID(req.GetActorId()); err != nil { return status.Error(codes.InvalidArgument, err.Error()) } + if req.GetAtespace() == "" { + return status.Error(codes.InvalidArgument, "atespace is required") + } + if err := resources.ValidateAtespace(req.GetAtespace()); err != nil { + return status.Error(codes.InvalidArgument, err.Error()) + } if err := validateSelector(req.GetWorkerSelector()); err != nil { return status.Error(codes.InvalidArgument, err.Error()) } diff --git a/cmd/ateapi/internal/controlapi/delete_actor.go b/cmd/ateapi/internal/controlapi/delete_actor.go index db70a32f..45a6ee0a 100644 --- a/cmd/ateapi/internal/controlapi/delete_actor.go +++ b/cmd/ateapi/internal/controlapi/delete_actor.go @@ -31,12 +31,12 @@ func (s *Service) DeleteActor(ctx context.Context, req *ateapipb.DeleteActorRequ return nil, err } - if err := s.persistence.DeleteActor(ctx, req.GetActorId()); err != nil { + if err := s.persistence.DeleteActor(ctx, req.GetAtespace(), req.GetActorId()); err != nil { if errors.Is(err, store.ErrNotFound) { return nil, status.Errorf(codes.NotFound, "Actor %s not found", req.GetActorId()) } if errors.Is(err, store.ErrFailedPrecondition) { - actor, getErr := s.persistence.GetActor(ctx, req.GetActorId()) + actor, getErr := s.persistence.GetActor(ctx, req.GetAtespace(), req.GetActorId()) if getErr == nil { return nil, status.Errorf(codes.FailedPrecondition, "Actor %s is not suspended (status: %v)", req.GetActorId(), actor.GetStatus()) } @@ -58,5 +58,8 @@ func validateDeleteActorRequest(req *ateapipb.DeleteActorRequest) error { if err := resources.ValidateActorID(req.GetActorId()); err != nil { return status.Error(codes.InvalidArgument, err.Error()) } + if req.GetAtespace() == "" { + return status.Error(codes.InvalidArgument, "atespace is required") + } return nil } diff --git a/cmd/ateapi/internal/controlapi/functional_test.go b/cmd/ateapi/internal/controlapi/functional_test.go index 6ad8c83d..e49548f6 100644 --- a/cmd/ateapi/internal/controlapi/functional_test.go +++ b/cmd/ateapi/internal/controlapi/functional_test.go @@ -60,6 +60,8 @@ var ( fakeAtelet = &FakeAteletServer{} ) +const testAtespace = "test-atespace" + func TestMain(m *testing.M) { binaryAssetsDirectory, err := envtestbins.BinaryAssetsDir() if err != nil { @@ -611,6 +613,7 @@ func TestCreateActor_Success(t *testing.T) { createTemplate(t, tc, ns) createResp, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -628,6 +631,7 @@ func TestCreateActor_Success(t *testing.T) { ActorTemplateName: "tmpl1", Status: ateapipb.Actor_STATUS_SUSPENDED, WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"tier": "free"}}, + Atespace: testAtespace, }, } @@ -643,6 +647,7 @@ func TestCreateActor_TemplateNotFound(t *testing.T) { defer tc.cleanup() _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "non-existent", ActorId: "id1", @@ -659,6 +664,7 @@ func TestCreateActor_Duplicate(t *testing.T) { createTemplate(t, tc, ns) _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -668,6 +674,7 @@ func TestCreateActor_Duplicate(t *testing.T) { } _, err = tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -684,6 +691,7 @@ func TestGetActor_Found(t *testing.T) { createTemplate(t, tc, ns) createResp, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -695,7 +703,8 @@ func TestGetActor_Found(t *testing.T) { id := createResp.GetActor().GetActorId() getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("GetActor failed: %v", err) @@ -720,7 +729,8 @@ func TestGetActor_NotFound(t *testing.T) { defer tc.cleanup() _, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ - ActorId: "non-existent", + Atespace: testAtespace, + ActorId: "non-existent", }) assertGrpcError(t, err, codes.NotFound, "Actor non-existent not found") } @@ -739,6 +749,7 @@ func TestListActors(t *testing.T) { createTemplate(t, tc, ns) resp1, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -747,6 +758,7 @@ func TestListActors(t *testing.T) { t.Fatalf("CreateActor 1 failed: %v", err) } resp2, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id2", @@ -755,7 +767,7 @@ func TestListActors(t *testing.T) { t.Fatalf("CreateActor 2 failed: %v", err) } - listResp, err := tc.client.ListActors(context.Background(), &ateapipb.ListActorsRequest{}) + listResp, err := tc.client.ListActors(context.Background(), &ateapipb.ListActorsRequest{Atespace: testAtespace}) if err != nil { t.Fatalf("ListActors failed: %v", err) } @@ -781,6 +793,63 @@ func TestListActors(t *testing.T) { } } +// TestListActors_ByAtespace verifies create + list are scoped by atespace end to +// end through the RPC surface: an actor created with a given atespace is only +// returned by ListActors(atespace=X) and only fetched by GetActor(atespace=X). +func TestListActors_ByAtespace(t *testing.T) { + ns := namespaceForTest("ns-list-by-atespace") + tc := setupTest(t, ns) + defer tc.cleanup() + + createTemplate(t, tc, ns) + + create := func(id, atespace string) *ateapipb.Actor { + resp, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + ActorTemplateNamespace: ns, + ActorTemplateName: "tmpl1", + ActorId: id, + Atespace: atespace, + }) + if err != nil { + t.Fatalf("CreateActor(%s, atespace=%q) failed: %v", id, atespace, err) + } + return resp.GetActor() + } + a1 := create("id1", "team-a") + a2 := create("id2", "team-a") + b1 := create("id3", "team-b") + + sortByID := []cmp.Option{ + protocmp.Transform(), + cmpopts.SortSlices(func(a, b *ateapipb.Actor) bool { return a.ActorId < b.ActorId }), + } + + // List scoped to team-a returns only its actors. + listA, err := tc.client.ListActors(context.Background(), &ateapipb.ListActorsRequest{Atespace: "team-a"}) + if err != nil { + t.Fatalf("ListActors(team-a) failed: %v", err) + } + if diff := cmp.Diff([]*ateapipb.Actor{a1, a2}, listA.GetActors(), sortByID...); diff != "" { + t.Errorf("ListActors(team-a) mismatch (-want +got):\n%s", diff) + } + + // List scoped to team-b returns only its actor. + listB, err := tc.client.ListActors(context.Background(), &ateapipb.ListActorsRequest{Atespace: "team-b"}) + if err != nil { + t.Fatalf("ListActors(team-b) failed: %v", err) + } + if diff := cmp.Diff([]*ateapipb.Actor{b1}, listB.GetActors(), sortByID...); diff != "" { + t.Errorf("ListActors(team-b) mismatch (-want +got):\n%s", diff) + } + + // Get is scoped: the right atespace hits, the empty atespace misses (deny-across by key). + if _, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ActorId: "id1", Atespace: "team-a"}); err != nil { + t.Errorf("GetActor(id1, team-a) failed: %v", err) + } + _, err = tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{Atespace: testAtespace, ActorId: "id1"}) + assertGrpcError(t, err, codes.NotFound, "Actor id1 not found") +} + // TestListActors_Pagination tests that ListActors correctly paginates results. func TestListActors_Pagination(t *testing.T) { ns := namespaceForTest("ns-list-actors-pagination") @@ -792,6 +861,7 @@ func TestListActors_Pagination(t *testing.T) { var want []*ateapipb.Actor for i := 0; i < 5; i++ { resp, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: fmt.Sprintf("id%d", i), @@ -807,6 +877,7 @@ func TestListActors_Pagination(t *testing.T) { for { listResp, err := tc.client.ListActors(context.Background(), &ateapipb.ListActorsRequest{ + Atespace: testAtespace, PageSize: 2, PageToken: pageToken, }) @@ -844,6 +915,7 @@ func TestListActors_PageSizeValidation(t *testing.T) { // 1. Negative page size _, err := tc.client.ListActors(context.Background(), &ateapipb.ListActorsRequest{ + Atespace: testAtespace, PageSize: -1, }) if status.Code(err) != codes.InvalidArgument { @@ -852,6 +924,7 @@ func TestListActors_PageSizeValidation(t *testing.T) { // 2. Page size exceeding maxPageSize (1000) _, err = tc.client.ListActors(context.Background(), &ateapipb.ListActorsRequest{ + Atespace: testAtespace, PageSize: 1001, }) if status.Code(err) != codes.InvalidArgument { @@ -919,6 +992,7 @@ func TestResumeActor(t *testing.T) { createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -929,7 +1003,8 @@ func TestResumeActor(t *testing.T) { id := "id1" _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("ResumeActor failed: %v", err) @@ -940,7 +1015,8 @@ func TestResumeActor(t *testing.T) { } getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("GetActor failed: %v", err) @@ -948,6 +1024,7 @@ func TestResumeActor(t *testing.T) { want := &ateapipb.GetActorResponse{ Actor: &ateapipb.Actor{ ActorId: id, + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", Status: ateapipb.Actor_STATUS_RUNNING, @@ -984,6 +1061,7 @@ func TestResumeActor(t *testing.T) { ActorNamespace: ns, ActorTemplate: "tmpl1", ActorId: id, + ActorAtespace: testAtespace, Ip: "127.0.0.1", NodeName: "node1", } @@ -1036,6 +1114,7 @@ func TestResumeActorResolvesValueFromEnv(t *testing.T) { createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") _, err = tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -1044,7 +1123,8 @@ func TestResumeActorResolvesValueFromEnv(t *testing.T) { t.Fatalf("CreateActor failed: %v", err) } _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: "id1", + Atespace: testAtespace, + ActorId: "id1", }) if err != nil { t.Fatalf("ResumeActor failed: %v", err) @@ -1090,6 +1170,7 @@ func TestResumeActor_NoWorkers(t *testing.T) { createTemplate(t, tc, ns) createResp, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -1101,7 +1182,8 @@ func TestResumeActor_NoWorkers(t *testing.T) { id := createResp.GetActor().GetActorId() _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) assertGrpcError(t, err, codes.FailedPrecondition, "no free workers available") } @@ -1119,7 +1201,7 @@ func TestResumeActor_NoEligiblePool(t *testing.T) { MatchLabels: map[string]string{"nonexistent": ns}, }) - createResp, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + createResp, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -1128,7 +1210,7 @@ func TestResumeActor_NoEligiblePool(t *testing.T) { t.Fatalf("CreateActor failed: %v", err) } - _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ + _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{Atespace: testAtespace, ActorId: createResp.GetActor().GetActorId(), }) assertGrpcError(t, err, codes.FailedPrecondition, "no worker pool matches the template and actor selectors") @@ -1151,7 +1233,7 @@ func TestResumeActor_MultiPoolSelector(t *testing.T) { createWorkerPod(t, tc, ns, "worker-a", "node1", "pool-a") createWorkerPod(t, tc, ns, "worker-b", "node1", "pool-b") - _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -1163,12 +1245,12 @@ func TestResumeActor_MultiPoolSelector(t *testing.T) { t.Fatalf("CreateActor failed: %v", err) } - _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ActorId: "id1"}) + _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{Atespace: testAtespace, ActorId: "id1"}) if err != nil { t.Fatalf("ResumeActor failed: %v", err) } - getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ActorId: "id1"}) + getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{Atespace: testAtespace, ActorId: "id1"}) if err != nil { t.Fatalf("GetActor failed: %v", err) } @@ -1202,7 +1284,7 @@ func TestResumeActor_RequiresBothSelectorsToMatch(t *testing.T) { createWorkerPod(t, tc, ns, "worker-template-only", "node1", "pool-template-only") createWorkerPod(t, tc, ns, "worker-actor-only", "node1", "pool-actor-only") - _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -1214,11 +1296,11 @@ func TestResumeActor_RequiresBothSelectorsToMatch(t *testing.T) { t.Fatalf("CreateActor failed: %v", err) } - if _, err := tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ActorId: "id1"}); err != nil { + if _, err := tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{Atespace: testAtespace, ActorId: "id1"}); err != nil { t.Fatalf("ResumeActor failed: %v", err) } - getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ActorId: "id1"}) + getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{Atespace: testAtespace, ActorId: "id1"}) if err != nil { t.Fatalf("GetActor failed: %v", err) } @@ -1248,6 +1330,7 @@ func TestResumeActor_Reentrancy(t *testing.T) { createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -1261,14 +1344,15 @@ func TestResumeActor_Reentrancy(t *testing.T) { tc.fakeAtelet.FailRestore = fmt.Errorf("mock atelet failure") _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err == nil { t.Fatalf("expected ResumeActor to fail due to atelet error") } // Verify actor state is RESUMING in Redis! - actor, err := tc.persistence.GetActor(context.Background(), id) + actor, err := tc.persistence.GetActor(context.Background(), testAtespace, id) if err != nil { t.Fatalf("failed to get actor from store: %v", err) } @@ -1281,7 +1365,8 @@ func TestResumeActor_Reentrancy(t *testing.T) { tc.fakeAtelet.RestoreCalled = false // reset for verification _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("ResumeActor failed on retry: %v", err) @@ -1292,7 +1377,7 @@ func TestResumeActor_Reentrancy(t *testing.T) { } // Verify actor state is RUNNING! - actor, err = tc.persistence.GetActor(context.Background(), id) + actor, err = tc.persistence.GetActor(context.Background(), testAtespace, id) if err != nil { t.Fatalf("failed to get actor from store: %v", err) } @@ -1321,6 +1406,7 @@ func TestSuspendActor(t *testing.T) { createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -1332,7 +1418,8 @@ func TestSuspendActor(t *testing.T) { // Resume first to make it running _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("ResumeActor failed: %v", err) @@ -1340,7 +1427,8 @@ func TestSuspendActor(t *testing.T) { // Suspend _, err = tc.client.SuspendActor(context.Background(), &ateapipb.SuspendActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("SuspendActor failed: %v", err) @@ -1351,7 +1439,8 @@ func TestSuspendActor(t *testing.T) { } getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("GetActor failed: %v", err) @@ -1359,6 +1448,7 @@ func TestSuspendActor(t *testing.T) { want := &ateapipb.GetActorResponse{ Actor: &ateapipb.Actor{ ActorId: id, + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", Status: ateapipb.Actor_STATUS_SUSPENDED, @@ -1405,6 +1495,7 @@ func TestPauseActor(t *testing.T) { createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -1416,7 +1507,8 @@ func TestPauseActor(t *testing.T) { // Resume first to make it running _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("ResumeActor failed: %v", err) @@ -1424,7 +1516,8 @@ func TestPauseActor(t *testing.T) { // Pause _, err = tc.client.PauseActor(context.Background(), &ateapipb.PauseActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("PauseActor failed: %v", err) @@ -1435,7 +1528,8 @@ func TestPauseActor(t *testing.T) { } getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("GetActor failed: %v", err) @@ -1443,6 +1537,7 @@ func TestPauseActor(t *testing.T) { want := &ateapipb.GetActorResponse{ Actor: &ateapipb.Actor{ ActorId: id, + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", Status: ateapipb.Actor_STATUS_PAUSED, @@ -1479,7 +1574,7 @@ func TestUpdateActor_Success(t *testing.T) { createTemplate(t, tc, ns) - _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -1491,7 +1586,7 @@ func TestUpdateActor_Success(t *testing.T) { t.Fatalf("CreateActor failed: %v", err) } - updateResp, err := tc.client.UpdateActor(context.Background(), &ateapipb.UpdateActorRequest{ + updateResp, err := tc.client.UpdateActor(context.Background(), &ateapipb.UpdateActorRequest{Atespace: testAtespace, ActorId: "id1", WorkerSelector: &ateapipb.Selector{ MatchLabels: map[string]string{"tier": "paid"}, @@ -1504,6 +1599,7 @@ func TestUpdateActor_Success(t *testing.T) { wantActor := &ateapipb.Actor{ ActorId: "id1", Version: 2, + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", Status: ateapipb.Actor_STATUS_SUSPENDED, @@ -1516,7 +1612,7 @@ func TestUpdateActor_Success(t *testing.T) { t.Errorf("UpdateActor response mismatch (-want +got):\n%s", diff) } - getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ActorId: "id1"}) + getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{Atespace: testAtespace, ActorId: "id1"}) if err != nil { t.Fatalf("GetActor failed: %v", err) } @@ -1531,7 +1627,7 @@ func TestUpdateActor_NotFound(t *testing.T) { tc := setupTest(t, ns) defer tc.cleanup() - _, err := tc.client.UpdateActor(context.Background(), &ateapipb.UpdateActorRequest{ActorId: "does-not-exist"}) + _, err := tc.client.UpdateActor(context.Background(), &ateapipb.UpdateActorRequest{Atespace: testAtespace, ActorId: "does-not-exist"}) assertGrpcError(t, err, codes.NotFound, "Actor does-not-exist not found") } @@ -1563,7 +1659,7 @@ func TestResumeActor_ReleasesStaleWorkerWhenPoolBecomesIneligible(t *testing.T) createWorkerPod(t, tc, ns, "worker-b", "node1", "pool-b") id := "id1" - _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: id, @@ -1574,24 +1670,24 @@ func TestResumeActor_ReleasesStaleWorkerWhenPoolBecomesIneligible(t *testing.T) } tc.fakeAtelet.FailRun = fmt.Errorf("mock atelet failure") - _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ActorId: id}) + _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{Atespace: testAtespace, ActorId: id}) if err == nil { t.Fatalf("expected first ResumeActor (onto worker-a) to fail") } tc.fakeAtelet.FailRun = nil - if _, err := tc.client.UpdateActor(context.Background(), &ateapipb.UpdateActorRequest{ + if _, err := tc.client.UpdateActor(context.Background(), &ateapipb.UpdateActorRequest{Atespace: testAtespace, ActorId: id, WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"tier": "b"}}, }); err != nil { t.Fatalf("UpdateActor failed: %v", err) } - if _, err := tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ActorId: id}); err != nil { + if _, err := tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{Atespace: testAtespace, ActorId: id}); err != nil { t.Fatalf("second ResumeActor failed: %v", err) } - getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ActorId: id}) + getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{Atespace: testAtespace, ActorId: id}) if err != nil { t.Fatalf("GetActor failed: %v", err) } @@ -1651,7 +1747,7 @@ func TestUpdateActor_ReassignsPoolAcrossSuspendResume(t *testing.T) { createWorkerPod(t, tc, ns, "worker-b", "node1", "pool-b") id := "id1" - _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: id, @@ -1663,11 +1759,11 @@ func TestUpdateActor_ReassignsPoolAcrossSuspendResume(t *testing.T) { t.Fatalf("CreateActor failed: %v", err) } - if _, err := tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ActorId: id}); err != nil { + if _, err := tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{Atespace: testAtespace, ActorId: id}); err != nil { t.Fatalf("first ResumeActor failed: %v", err) } - getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ActorId: id}) + getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{Atespace: testAtespace, ActorId: id}) if err != nil { t.Fatalf("GetActor failed: %v", err) } @@ -1678,7 +1774,7 @@ func TestUpdateActor_ReassignsPoolAcrossSuspendResume(t *testing.T) { t.Fatalf("expected actor to first resume onto worker-a, got ateom_pod_name=%q", got) } - if _, err := tc.client.UpdateActor(context.Background(), &ateapipb.UpdateActorRequest{ + if _, err := tc.client.UpdateActor(context.Background(), &ateapipb.UpdateActorRequest{Atespace: testAtespace, ActorId: id, WorkerSelector: &ateapipb.Selector{ MatchLabels: map[string]string{"tier": "b"}, @@ -1687,14 +1783,14 @@ func TestUpdateActor_ReassignsPoolAcrossSuspendResume(t *testing.T) { t.Fatalf("UpdateActor failed: %v", err) } - if _, err := tc.client.SuspendActor(context.Background(), &ateapipb.SuspendActorRequest{ActorId: id}); err != nil { + if _, err := tc.client.SuspendActor(context.Background(), &ateapipb.SuspendActorRequest{Atespace: testAtespace, ActorId: id}); err != nil { t.Fatalf("SuspendActor failed: %v", err) } - if _, err := tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ActorId: id}); err != nil { + if _, err := tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{Atespace: testAtespace, ActorId: id}); err != nil { t.Fatalf("second ResumeActor failed: %v", err) } - getResp, err = tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ActorId: id}) + getResp, err = tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{Atespace: testAtespace, ActorId: id}) if err != nil { t.Fatalf("GetActor failed: %v", err) } @@ -1727,37 +1823,37 @@ func TestValidation(t *testing.T) { }{ { "missing namespace", - &ateapipb.CreateActorRequest{ActorTemplateName: "tmpl1", ActorId: "id1"}, + &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateName: "tmpl1", ActorId: "id1"}, "actor_template_namespace is required"}, { "missing template name", - &ateapipb.CreateActorRequest{ActorTemplateNamespace: "ns1", ActorId: "id1"}, + &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: "ns1", ActorId: "id1"}, "actor_template_name is required"}, { "missing actor id", - &ateapipb.CreateActorRequest{ActorTemplateNamespace: "ns1", ActorTemplateName: "tmpl1"}, + &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: "ns1", ActorTemplateName: "tmpl1"}, "actor_id is required"}, { "invalid actor id (capitals)", - &ateapipb.CreateActorRequest{ActorTemplateNamespace: "ns1", ActorTemplateName: "tmpl1", ActorId: "ID1"}, + &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: "ns1", ActorTemplateName: "tmpl1", ActorId: "ID1"}, "invalid actor_id: must start and end with a lower case alphanumeric character, and consist only of lower case alphanumeric characters or '-'"}, { "invalid actor id (special chars)", - &ateapipb.CreateActorRequest{ActorTemplateNamespace: "ns1", ActorTemplateName: "tmpl1", ActorId: "id_1"}, + &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: "ns1", ActorTemplateName: "tmpl1", ActorId: "id_1"}, "invalid actor_id: must start and end with a lower case alphanumeric character, and consist only of lower case alphanumeric characters or '-'"}, { "invalid worker_selector label key", - &ateapipb.CreateActorRequest{ActorTemplateNamespace: "ns1", ActorTemplateName: "tmpl1", ActorId: "id1", WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"bad key!": "x"}}}, + &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: "ns1", ActorTemplateName: "tmpl1", ActorId: "id1", WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"bad key!": "x"}}}, `invalid worker_selector label key "bad key!": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`, }, { "invalid worker_selector label value", - &ateapipb.CreateActorRequest{ActorTemplateNamespace: "ns1", ActorTemplateName: "tmpl1", ActorId: "id1", WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"tier": "not valid!"}}}, + &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: "ns1", ActorTemplateName: "tmpl1", ActorId: "id1", WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"tier": "not valid!"}}}, `invalid worker_selector label value "not valid!" for key "tier": a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')`, }, { "too many worker_selector match_labels", - &ateapipb.CreateActorRequest{ActorTemplateNamespace: "ns1", ActorTemplateName: "tmpl1", ActorId: "id1", WorkerSelector: &ateapipb.Selector{MatchLabels: selectorLabelsOfSize(11)}}, + &ateapipb.CreateActorRequest{Atespace: testAtespace, ActorTemplateNamespace: "ns1", ActorTemplateName: "tmpl1", ActorId: "id1", WorkerSelector: &ateapipb.Selector{MatchLabels: selectorLabelsOfSize(11)}}, "worker_selector has 11 match_labels entries, exceeding the limit of 10", }, } @@ -1775,7 +1871,7 @@ func TestValidation(t *testing.T) { req *ateapipb.GetActorRequest wantMsg string }{ - {"missing id", &ateapipb.GetActorRequest{}, "id is required"}, + {"missing id", &ateapipb.GetActorRequest{Atespace: testAtespace}, "id is required"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1791,7 +1887,7 @@ func TestValidation(t *testing.T) { req *ateapipb.ResumeActorRequest wantMsg string }{ - {"missing id", &ateapipb.ResumeActorRequest{}, "id is required"}, + {"missing id", &ateapipb.ResumeActorRequest{Atespace: testAtespace}, "id is required"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1807,7 +1903,7 @@ func TestValidation(t *testing.T) { req *ateapipb.SuspendActorRequest wantMsg string }{ - {"missing id", &ateapipb.SuspendActorRequest{}, "id is required"}, + {"missing id", &ateapipb.SuspendActorRequest{Atespace: testAtespace}, "id is required"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1823,20 +1919,20 @@ func TestValidation(t *testing.T) { req *ateapipb.UpdateActorRequest wantMsg string }{ - {"missing id", &ateapipb.UpdateActorRequest{}, "actor_id is required"}, + {"missing id", &ateapipb.UpdateActorRequest{Atespace: testAtespace}, "actor_id is required"}, { "invalid worker_selector label key", - &ateapipb.UpdateActorRequest{ActorId: "id1", WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"bad key!": "x"}}}, + &ateapipb.UpdateActorRequest{Atespace: testAtespace, ActorId: "id1", WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"bad key!": "x"}}}, `invalid worker_selector label key "bad key!": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`, }, { "invalid worker_selector label value", - &ateapipb.UpdateActorRequest{ActorId: "id1", WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"tier": "not valid!"}}}, + &ateapipb.UpdateActorRequest{Atespace: testAtespace, ActorId: "id1", WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"tier": "not valid!"}}}, `invalid worker_selector label value "not valid!" for key "tier": a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')`, }, { "too many worker_selector match_labels", - &ateapipb.UpdateActorRequest{ActorId: "id1", WorkerSelector: &ateapipb.Selector{MatchLabels: selectorLabelsOfSize(11)}}, + &ateapipb.UpdateActorRequest{Atespace: testAtespace, ActorId: "id1", WorkerSelector: &ateapipb.Selector{MatchLabels: selectorLabelsOfSize(11)}}, "worker_selector has 11 match_labels entries, exceeding the limit of 10", }, } @@ -1854,9 +1950,9 @@ func TestValidation(t *testing.T) { req *ateapipb.DeleteActorRequest wantMsg string }{ - {"missing id", &ateapipb.DeleteActorRequest{}, "actor_id is required"}, - {"invalid actor id (capitals)", &ateapipb.DeleteActorRequest{ActorId: "ID1"}, "invalid actor_id: must start and end with a lower case alphanumeric character, and consist only of lower case alphanumeric characters or '-'"}, - {"invalid actor id (special chars)", &ateapipb.DeleteActorRequest{ActorId: "id_1"}, "invalid actor_id: must start and end with a lower case alphanumeric character, and consist only of lower case alphanumeric characters or '-'"}, + {"missing id", &ateapipb.DeleteActorRequest{Atespace: testAtespace}, "actor_id is required"}, + {"invalid actor id (capitals)", &ateapipb.DeleteActorRequest{Atespace: testAtespace, ActorId: "ID1"}, "invalid actor_id: must start and end with a lower case alphanumeric character, and consist only of lower case alphanumeric characters or '-'"}, + {"invalid actor id (special chars)", &ateapipb.DeleteActorRequest{Atespace: testAtespace, ActorId: "id_1"}, "invalid actor_id: must start and end with a lower case alphanumeric character, and consist only of lower case alphanumeric characters or '-'"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1877,6 +1973,7 @@ func TestResumeActor_LockConflict(t *testing.T) { createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -1893,7 +1990,8 @@ func TestResumeActor_LockConflict(t *testing.T) { errChan := make(chan error, 1) go func() { _, err := tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) errChan <- err }() @@ -1903,7 +2001,8 @@ func TestResumeActor_LockConflict(t *testing.T) { // Launch Request B (should fail due to lock conflict) _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) assertGrpcError(t, err, codes.Aborted, "another operation is in progress for this actor") @@ -1924,6 +2023,7 @@ func TestResumeActor_DanglingWorker(t *testing.T) { createWorkerPod(t, tc, ns, "worker-a", "node1", "pool1") _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -1938,7 +2038,8 @@ func TestResumeActor_DanglingWorker(t *testing.T) { // 3. Call ResumeActor -> Expect failure _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err == nil { t.Fatalf("expected ResumeActor to fail due to atelet error") @@ -1946,7 +2047,8 @@ func TestResumeActor_DanglingWorker(t *testing.T) { // Verify actor state is RESUMING with worker A assigned getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("GetActor failed: %v", err) @@ -1970,7 +2072,8 @@ func TestResumeActor_DanglingWorker(t *testing.T) { // 8. Call ResumeActor again -> Expect success and picking Worker B! _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("ResumeActor failed on retry: %v", err) @@ -1981,7 +2084,7 @@ func TestResumeActor_DanglingWorker(t *testing.T) { } // Verify actor state is RUNNING with worker B assigned - actor, err = tc.persistence.GetActor(context.Background(), id) + actor, err = tc.persistence.GetActor(context.Background(), testAtespace, id) if err != nil { t.Fatalf("failed to get actor from store: %v", err) } @@ -2004,6 +2107,7 @@ func TestSuspendActor_DanglingWorker(t *testing.T) { createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -2015,7 +2119,8 @@ func TestSuspendActor_DanglingWorker(t *testing.T) { // Resume first to make it running _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("ResumeActor failed: %v", err) @@ -2024,14 +2129,15 @@ func TestSuspendActor_DanglingWorker(t *testing.T) { deleteWorkerPod(t, tc, ns, "worker-1") // 3. Call SuspendActor -> Should succeed (our fix skips missing pod execution) - actors, _, _ := tc.persistence.ListActors(context.Background(), maxPageSize, "") + actors, _, _ := tc.persistence.ListActors(context.Background(), testAtespace, maxPageSize, "") t.Logf("Actors in Redis before Suspend: %d", len(actors)) for _, a := range actors { t.Logf(" Actor: %s/%s/%s", a.GetActorTemplateNamespace(), a.GetActorTemplateName(), a.GetActorId()) } _, err = tc.client.SuspendActor(context.Background(), &ateapipb.SuspendActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("SuspendActor failed: %v", err) @@ -2039,7 +2145,8 @@ func TestSuspendActor_DanglingWorker(t *testing.T) { // 4. Verify it becomes SUSPENDED in Redis getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ - ActorId: id, + Atespace: testAtespace, + ActorId: id, }) if err != nil { t.Fatalf("GetActor failed: %v", err) @@ -2060,6 +2167,7 @@ func TestDeleteActor_Success(t *testing.T) { createTemplate(t, tc, ns) _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -2069,14 +2177,16 @@ func TestDeleteActor_Success(t *testing.T) { } _, err = tc.client.DeleteActor(context.Background(), &ateapipb.DeleteActorRequest{ - ActorId: "id1", + Atespace: testAtespace, + ActorId: "id1", }) if err != nil { t.Fatalf("DeleteActor failed: %v", err) } _, err = tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ - ActorId: "id1", + Atespace: testAtespace, + ActorId: "id1", }) assertGrpcError(t, err, codes.NotFound, "Actor id1 not found") } @@ -2090,6 +2200,7 @@ func TestDeleteActor_NotSuspended(t *testing.T) { createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -2099,14 +2210,16 @@ func TestDeleteActor_NotSuspended(t *testing.T) { } _, err = tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{ - ActorId: "id1", + Atespace: testAtespace, + ActorId: "id1", }) if err != nil { t.Fatalf("ResumeActor failed: %v", err) } _, err = tc.client.DeleteActor(context.Background(), &ateapipb.DeleteActorRequest{ - ActorId: "id1", + Atespace: testAtespace, + ActorId: "id1", }) assertGrpcError(t, err, codes.FailedPrecondition, "Actor id1 is not suspended (status: STATUS_RUNNING)") } @@ -2117,7 +2230,8 @@ func TestDeleteActor_NotFound(t *testing.T) { defer tc.cleanup() _, err := tc.client.DeleteActor(context.Background(), &ateapipb.DeleteActorRequest{ - ActorId: "non-existent", + Atespace: testAtespace, + ActorId: "non-existent", }) assertGrpcError(t, err, codes.NotFound, "Actor non-existent not found") } diff --git a/cmd/ateapi/internal/controlapi/get_actor.go b/cmd/ateapi/internal/controlapi/get_actor.go index d2b5cc63..ebac995b 100644 --- a/cmd/ateapi/internal/controlapi/get_actor.go +++ b/cmd/ateapi/internal/controlapi/get_actor.go @@ -29,7 +29,7 @@ func (s *Service) GetActor(ctx context.Context, req *ateapipb.GetActorRequest) ( if err := validateGetActorRequest(req); err != nil { return nil, err } - actor, err := s.persistence.GetActor(ctx, req.GetActorId()) + actor, err := s.persistence.GetActor(ctx, req.GetAtespace(), req.GetActorId()) if errors.Is(err, store.ErrNotFound) { return nil, status.Errorf(codes.NotFound, "Actor %s not found", req.GetActorId()) } else if err != nil { @@ -44,5 +44,8 @@ func validateGetActorRequest(req *ateapipb.GetActorRequest) error { if req.GetActorId() == "" { return status.Error(codes.InvalidArgument, "id is required") } + if req.GetAtespace() == "" { + return status.Error(codes.InvalidArgument, "atespace is required") + } return nil } diff --git a/cmd/ateapi/internal/controlapi/list_actors.go b/cmd/ateapi/internal/controlapi/list_actors.go index 7f863990..054812ea 100644 --- a/cmd/ateapi/internal/controlapi/list_actors.go +++ b/cmd/ateapi/internal/controlapi/list_actors.go @@ -18,6 +18,7 @@ import ( "context" "fmt" + "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" @@ -34,7 +35,7 @@ func (s *Service) ListActors(ctx context.Context, req *ateapipb.ListActorsReques pageSize = maxPageSize } - actors, nextToken, err := s.persistence.ListActors(ctx, pageSize, req.GetPageToken()) + actors, nextToken, err := s.persistence.ListActors(ctx, req.GetAtespace(), pageSize, req.GetPageToken()) if err != nil { return nil, fmt.Errorf("while listing actors in db: %w", err) } @@ -45,6 +46,12 @@ func (s *Service) ListActors(ctx context.Context, req *ateapipb.ListActorsReques } func validateListActorsRequest(req *ateapipb.ListActorsRequest) error { + if req.GetAtespace() == "" { + return fmt.Errorf("atespace is required") + } + if err := resources.ValidateAtespace(req.GetAtespace()); err != nil { + return err + } pageSize := req.GetPageSize() if pageSize < 0 { return fmt.Errorf("page_size cannot be negative") diff --git a/cmd/ateapi/internal/controlapi/pause_actor.go b/cmd/ateapi/internal/controlapi/pause_actor.go index 74f02329..f76bc4c7 100644 --- a/cmd/ateapi/internal/controlapi/pause_actor.go +++ b/cmd/ateapi/internal/controlapi/pause_actor.go @@ -29,7 +29,7 @@ func (s *Service) PauseActor(ctx context.Context, req *ateapipb.PauseActorReques return nil, err } - actor, err := s.actorWorkflow.PauseActor(ctx, req.GetActorId()) + actor, err := s.actorWorkflow.PauseActor(ctx, req.GetAtespace(), req.GetActorId()) if err != nil { if errors.Is(err, store.ErrPersistenceRetry) { return nil, status.Error(codes.Aborted, "concurrent update conflict, please retry") @@ -47,5 +47,8 @@ func validatePauseActorRequest(req *ateapipb.PauseActorRequest) error { if req.GetActorId() == "" { return status.Error(codes.InvalidArgument, "id is required") } + if req.GetAtespace() == "" { + return status.Error(codes.InvalidArgument, "atespace is required") + } return nil } diff --git a/cmd/ateapi/internal/controlapi/resume_actor.go b/cmd/ateapi/internal/controlapi/resume_actor.go index 671afb6e..623a29ef 100644 --- a/cmd/ateapi/internal/controlapi/resume_actor.go +++ b/cmd/ateapi/internal/controlapi/resume_actor.go @@ -29,7 +29,7 @@ func (s *Service) ResumeActor(ctx context.Context, req *ateapipb.ResumeActorRequ return nil, err } - actor, err := s.actorWorkflow.ResumeActor(ctx, req.GetActorId(), req.GetBoot()) + actor, err := s.actorWorkflow.ResumeActor(ctx, req.GetAtespace(), req.GetActorId(), req.GetBoot()) if err != nil { if errors.Is(err, store.ErrPersistenceRetry) { return nil, status.Error(codes.Aborted, "concurrent update conflict, please retry") @@ -47,5 +47,8 @@ func validateResumeActorRequest(req *ateapipb.ResumeActorRequest) error { if req.GetActorId() == "" { return status.Error(codes.InvalidArgument, "id is required") } + if req.GetAtespace() == "" { + return status.Error(codes.InvalidArgument, "atespace is required") + } return nil } diff --git a/cmd/ateapi/internal/controlapi/suspend_actor.go b/cmd/ateapi/internal/controlapi/suspend_actor.go index c8a88b50..531f2a2d 100644 --- a/cmd/ateapi/internal/controlapi/suspend_actor.go +++ b/cmd/ateapi/internal/controlapi/suspend_actor.go @@ -29,7 +29,7 @@ func (s *Service) SuspendActor(ctx context.Context, req *ateapipb.SuspendActorRe return nil, err } - actor, err := s.actorWorkflow.SuspendActor(ctx, req.GetActorId()) + actor, err := s.actorWorkflow.SuspendActor(ctx, req.GetAtespace(), req.GetActorId()) if err != nil { if errors.Is(err, store.ErrPersistenceRetry) { return nil, status.Error(codes.Aborted, "concurrent update conflict, please retry") @@ -47,5 +47,8 @@ func validateSuspendActorRequest(req *ateapipb.SuspendActorRequest) error { if req.GetActorId() == "" { return status.Error(codes.InvalidArgument, "id is required") } + if req.GetAtespace() == "" { + return status.Error(codes.InvalidArgument, "atespace is required") + } return nil } diff --git a/cmd/ateapi/internal/controlapi/syncer.go b/cmd/ateapi/internal/controlapi/syncer.go index f0501387..c16c8dd5 100644 --- a/cmd/ateapi/internal/controlapi/syncer.go +++ b/cmd/ateapi/internal/controlapi/syncer.go @@ -170,7 +170,7 @@ func (s *WorkerPoolSyncer) releaseActorOnDeadWorker(ctx context.Context, namespa if worker.GetActorId() == "" { return nil } - actor, err := s.persistence.GetActor(ctx, worker.GetActorId()) + actor, err := s.persistence.GetActor(ctx, worker.GetActorAtespace(), worker.GetActorId()) if err != nil { if errors.Is(err, store.ErrNotFound) { return nil diff --git a/cmd/ateapi/internal/controlapi/syncer_test.go b/cmd/ateapi/internal/controlapi/syncer_test.go index c05b6f11..2672c11a 100644 --- a/cmd/ateapi/internal/controlapi/syncer_test.go +++ b/cmd/ateapi/internal/controlapi/syncer_test.go @@ -180,7 +180,7 @@ func TestSyncer_DeleteBoundWorker_ClearsActor(t *testing.T) { } actorID := "actor-orphan" if err := persistence.CreateActor(ctx, &ateapipb.Actor{ - ActorId: actorID, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl", + ActorId: actorID, Atespace: "team-orphan", ActorTemplateNamespace: ns, ActorTemplateName: "tmpl", Status: ateapipb.Actor_STATUS_RUNNING, AteomPodNamespace: ns, AteomPodName: pod, AteomPodIp: ip, InProgressSnapshot: "gs://snapshots/partial", @@ -196,7 +196,7 @@ func TestSyncer_DeleteBoundWorker_ClearsActor(t *testing.T) { t.Fatalf("create actor: %v", err) } w, _ := persistence.GetWorker(ctx, ns, pool, pod) - w.ActorId, w.ActorNamespace, w.ActorTemplate = actorID, ns, "tmpl" + w.ActorId, w.ActorNamespace, w.ActorTemplate, w.ActorAtespace = actorID, ns, "tmpl", "team-orphan" if err := persistence.UpdateWorker(ctx, w, w.Version); err != nil { t.Fatalf("update worker: %v", err) } @@ -206,7 +206,7 @@ func TestSyncer_DeleteBoundWorker_ClearsActor(t *testing.T) { } var got *ateapipb.Actor if err := wait.PollUntilContextTimeout(ctx, 50*time.Millisecond, 2*time.Second, true, func(c context.Context) (bool, error) { - a, gerr := persistence.GetActor(c, actorID) + a, gerr := persistence.GetActor(c, "team-orphan", actorID) if gerr != nil { return false, gerr } diff --git a/cmd/ateapi/internal/controlapi/update_actor.go b/cmd/ateapi/internal/controlapi/update_actor.go index f45e5a33..0aeb35cb 100644 --- a/cmd/ateapi/internal/controlapi/update_actor.go +++ b/cmd/ateapi/internal/controlapi/update_actor.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "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" @@ -30,7 +31,7 @@ func (s *Service) UpdateActor(ctx context.Context, req *ateapipb.UpdateActorRequ return nil, err } - actor, err := s.persistence.GetActor(ctx, req.GetActorId()) + actor, err := s.persistence.GetActor(ctx, req.GetAtespace(), req.GetActorId()) if err != nil { if errors.Is(err, store.ErrNotFound) { return nil, status.Errorf(codes.NotFound, "Actor %s not found", req.GetActorId()) @@ -53,6 +54,12 @@ func validateUpdateActorRequest(req *ateapipb.UpdateActorRequest) error { if req.GetActorId() == "" { return status.Error(codes.InvalidArgument, "actor_id is required") } + if req.GetAtespace() == "" { + return status.Error(codes.InvalidArgument, "atespace is required") + } + if err := resources.ValidateAtespace(req.GetAtespace()); err != nil { + return status.Error(codes.InvalidArgument, err.Error()) + } if err := validateSelector(req.GetWorkerSelector()); err != nil { return status.Error(codes.InvalidArgument, err.Error()) } diff --git a/cmd/ateapi/internal/controlapi/workflow.go b/cmd/ateapi/internal/controlapi/workflow.go index d24d50f4..4307dc92 100644 --- a/cmd/ateapi/internal/controlapi/workflow.go +++ b/cmd/ateapi/internal/controlapi/workflow.go @@ -144,10 +144,11 @@ func NewActorWorkflow( } // ResumeActor executes the workflow to resume a suspended actor. Idempotent. -func (w *ActorWorkflow) ResumeActor(ctx context.Context, id string, boot bool) (*ateapipb.Actor, error) { +func (w *ActorWorkflow) ResumeActor(ctx context.Context, atespace, id string, boot bool) (*ateapipb.Actor, error) { input := &ResumeInput{ - ActorID: id, - Boot: boot, + ActorID: id, + Atespace: atespace, + Boot: boot, } state := &ResumeState{} @@ -174,9 +175,10 @@ func (w *ActorWorkflow) ResumeActor(ctx context.Context, id string, boot bool) ( } // SuspendActor executes the workflow to suspend a running actor. Idempotent. -func (w *ActorWorkflow) SuspendActor(ctx context.Context, id string) (*ateapipb.Actor, error) { +func (w *ActorWorkflow) SuspendActor(ctx context.Context, atespace, id string) (*ateapipb.Actor, error) { input := &SuspendInput{ - ActorID: id, + ActorID: id, + Atespace: atespace, } state := &SuspendState{} @@ -203,9 +205,10 @@ func (w *ActorWorkflow) SuspendActor(ctx context.Context, id string) (*ateapipb. } // PauseActor executes the workflow to pause a running actor. Idempotent. -func (w *ActorWorkflow) PauseActor(ctx context.Context, id string) (*ateapipb.Actor, error) { +func (w *ActorWorkflow) PauseActor(ctx context.Context, atespace, id string) (*ateapipb.Actor, error) { input := &PauseInput{ - ActorID: id, + ActorID: id, + Atespace: atespace, } state := &PauseState{} diff --git a/cmd/ateapi/internal/controlapi/workflow_pause.go b/cmd/ateapi/internal/controlapi/workflow_pause.go index a7aaa41b..49f9c407 100644 --- a/cmd/ateapi/internal/controlapi/workflow_pause.go +++ b/cmd/ateapi/internal/controlapi/workflow_pause.go @@ -32,7 +32,8 @@ import ( // PauseInput holds the immutable parameters requested by the client. type PauseInput struct { - ActorID string + ActorID string + Atespace string } // PauseState holds the mutable state loaded and modified during execution. @@ -52,7 +53,7 @@ func (s *LoadActorForPauseStep) IsComplete(ctx context.Context, input *PauseInpu return false, nil } func (s *LoadActorForPauseStep) Execute(ctx context.Context, input *PauseInput, state *PauseState) error { - actor, err := s.store.GetActor(ctx, input.ActorID) + actor, err := s.store.GetActor(ctx, input.Atespace, input.ActorID) if err != nil { return err } @@ -171,7 +172,7 @@ func (s *FinalizePausedStep) IsComplete(ctx context.Context, input *PauseInput, return state.Actor.GetStatus() == ateapipb.Actor_STATUS_PAUSED && state.Actor.GetAteomPodNamespace() == "", nil } func (s *FinalizePausedStep) Execute(ctx context.Context, input *PauseInput, state *PauseState) error { - latestActor, err := s.store.GetActor(ctx, input.ActorID) + latestActor, err := s.store.GetActor(ctx, input.Atespace, input.ActorID) if err != nil { return err } @@ -198,6 +199,7 @@ func (s *FinalizePausedStep) Execute(ctx context.Context, input *PauseInput, sta worker.ActorNamespace = "" worker.ActorTemplate = "" worker.ActorId = "" + worker.ActorAtespace = "" err = s.store.UpdateWorker(ctx, worker, worker.Version) if err != nil { @@ -207,7 +209,7 @@ func (s *FinalizePausedStep) Execute(ctx context.Context, input *PauseInput, sta } // 2. Safely clear ActiveWorker now that the worker object in DB is freed - latestActor, err = s.store.GetActor(ctx, input.ActorID) + latestActor, err = s.store.GetActor(ctx, input.Atespace, input.ActorID) if err != nil { return err } diff --git a/cmd/ateapi/internal/controlapi/workflow_resume.go b/cmd/ateapi/internal/controlapi/workflow_resume.go index a604e170..b2cf44c7 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume.go @@ -39,8 +39,9 @@ import ( // ResumeInput holds the immutable parameters requested by the client. type ResumeInput struct { - ActorID string - Boot bool + ActorID string + Atespace string + Boot bool } // ResumeState holds the mutable state loaded and modified during execution. @@ -60,7 +61,7 @@ func (s *LoadActorForResumeStep) IsComplete(ctx context.Context, input *ResumeIn return false, nil } func (s *LoadActorForResumeStep) Execute(ctx context.Context, input *ResumeInput, state *ResumeState) error { - actor, err := s.store.GetActor(ctx, input.ActorID) + actor, err := s.store.GetActor(ctx, input.Atespace, input.ActorID) if err != nil { if errors.Is(err, store.ErrNotFound) { return status.Errorf(codes.NotFound, "Actor %s not found", input.ActorID) @@ -167,6 +168,7 @@ func (s *AssignWorkerStep) Execute(ctx context.Context, input *ResumeInput, stat assignedWorker.ActorId = input.ActorID assignedWorker.ActorNamespace = state.Actor.GetActorTemplateNamespace() assignedWorker.ActorTemplate = state.Actor.GetActorTemplateName() + assignedWorker.ActorAtespace = state.Actor.GetAtespace() if err := s.store.UpdateWorker(ctx, assignedWorker, assignedWorker.Version); err != nil { return err @@ -338,7 +340,7 @@ func (s *FinalizeRunningStep) IsComplete(ctx context.Context, input *ResumeInput return state.Actor.GetStatus() == ateapipb.Actor_STATUS_RUNNING, nil } func (s *FinalizeRunningStep) Execute(ctx context.Context, input *ResumeInput, state *ResumeState) error { - latestActor, err := s.store.GetActor(ctx, input.ActorID) + latestActor, err := s.store.GetActor(ctx, input.Atespace, input.ActorID) if err != nil { return err } diff --git a/cmd/ateapi/internal/controlapi/workflow_suspend.go b/cmd/ateapi/internal/controlapi/workflow_suspend.go index 1c0bcec2..281e958a 100644 --- a/cmd/ateapi/internal/controlapi/workflow_suspend.go +++ b/cmd/ateapi/internal/controlapi/workflow_suspend.go @@ -33,7 +33,8 @@ import ( // SuspendInput holds the immutable parameters requested by the client. type SuspendInput struct { - ActorID string + ActorID string + Atespace string } // SuspendState holds the mutable state loaded and modified during execution. @@ -53,7 +54,7 @@ func (s *LoadActorForSuspendStep) IsComplete(ctx context.Context, input *Suspend return false, nil } func (s *LoadActorForSuspendStep) Execute(ctx context.Context, input *SuspendInput, state *SuspendState) error { - actor, err := s.store.GetActor(ctx, input.ActorID) + actor, err := s.store.GetActor(ctx, input.Atespace, input.ActorID) if err != nil { return err } @@ -173,7 +174,7 @@ func (s *FinalizeSuspendedStep) IsComplete(ctx context.Context, input *SuspendIn return state.Actor.GetStatus() == ateapipb.Actor_STATUS_SUSPENDED && state.Actor.GetAteomPodNamespace() == "", nil } func (s *FinalizeSuspendedStep) Execute(ctx context.Context, input *SuspendInput, state *SuspendState) error { - latestActor, err := s.store.GetActor(ctx, input.ActorID) + latestActor, err := s.store.GetActor(ctx, input.Atespace, input.ActorID) if err != nil { return err } @@ -197,6 +198,7 @@ func (s *FinalizeSuspendedStep) Execute(ctx context.Context, input *SuspendInput worker.ActorNamespace = "" worker.ActorTemplate = "" worker.ActorId = "" + worker.ActorAtespace = "" err = s.store.UpdateWorker(ctx, worker, worker.Version) if err != nil { @@ -206,7 +208,7 @@ func (s *FinalizeSuspendedStep) Execute(ctx context.Context, input *SuspendInput } // 2. Safely clear ActiveWorker now that the worker object in DB is freed - latestActor, err = s.store.GetActor(ctx, input.ActorID) + latestActor, err = s.store.GetActor(ctx, input.Atespace, input.ActorID) if err != nil { return err } diff --git a/cmd/ateapi/internal/store/ateredis/ateredis.go b/cmd/ateapi/internal/store/ateredis/ateredis.go index 206395c9..eb81f05b 100644 --- a/cmd/ateapi/internal/store/ateredis/ateredis.go +++ b/cmd/ateapi/internal/store/ateredis/ateredis.go @@ -15,7 +15,7 @@ // Package ateredis is an ate storage backend built on Redis. // // Actors are stored in keys of the form -// `actor:`. They are +// `actor::`. They are // stored as DBActor JSON-serialized objects, which lets us manipulate them from // Redis lua. // @@ -79,8 +79,8 @@ func NewPersistence(redisClient *redis.ClusterClient) *Persistence { } } -func actorDBKey(id string) string { - return "actor:" + id +func actorDBKey(atespace, id string) string { + return "actor:" + atespace + ":" + id } func workerDBKey(namespace, poolName, podName string) string { @@ -101,8 +101,8 @@ func (s *Persistence) DebugClearAll(ctx context.Context) error { return err } -func (s *Persistence) GetActor(ctx context.Context, id string) (*ateapipb.Actor, error) { - dbKey := actorDBKey(id) +func (s *Persistence) GetActor(ctx context.Context, atespace, id string) (*ateapipb.Actor, error) { + dbKey := actorDBKey(atespace, id) dbActorBytes, err := s.rdb.Get(ctx, dbKey).Bytes() if err != nil { @@ -117,15 +117,15 @@ func (s *Persistence) GetActor(ctx context.Context, id string) (*ateapipb.Actor, return nil, fmt.Errorf("while unmarshaling actor: %w", err) } - if actor.GetActorId() != id { - return nil, fmt.Errorf("(impossible) mismatch between stored id and key id") + if actor.GetActorId() != id || actor.GetAtespace() != atespace { + return nil, fmt.Errorf("(impossible) mismatch between stored id/atespace and key") } return actor, nil } func (s *Persistence) CreateActor(ctx context.Context, actor *ateapipb.Actor) error { - dbKey := actorDBKey(actor.GetActorId()) + dbKey := actorDBKey(actor.GetAtespace(), actor.GetActorId()) // Clone because we will update the version field, and we don't want to // stomp the caller's copy. @@ -263,8 +263,8 @@ func (s *Persistence) DeleteWorker(ctx context.Context, namespace, pool, pod str return nil } -func (s *Persistence) DeleteActor(ctx context.Context, id string) error { - dbKey := actorDBKey(id) +func (s *Persistence) DeleteActor(ctx context.Context, atespace, id string) error { + dbKey := actorDBKey(atespace, id) err := s.rdb.Watch(ctx, func(tx *redis.Tx) error { currentVal, err := tx.Get(ctx, dbKey).Bytes() if err != nil { @@ -301,7 +301,7 @@ func (s *Persistence) DeleteActor(ctx context.Context, id string) error { } func (s *Persistence) UpdateActor(ctx context.Context, actor *ateapipb.Actor, expectedVersion int64) error { - dbKey := actorDBKey(actor.GetActorId()) + dbKey := actorDBKey(actor.GetAtespace(), actor.GetActorId()) // Clone because we will update the version field, and we don't want to // stomp the caller's copy. @@ -328,6 +328,9 @@ func (s *Persistence) UpdateActor(ctx context.Context, actor *ateapipb.Actor, ex if currentActor.GetActorId() != dbActor.GetActorId() { return fmt.Errorf("actor_id is immutable") } + if currentActor.GetAtespace() != dbActor.GetAtespace() { + return fmt.Errorf("atespace is immutable") + } if currentActor.GetActorTemplateNamespace() != dbActor.GetActorTemplateNamespace() { return fmt.Errorf("actor_template_namespace is immutable") } @@ -426,7 +429,7 @@ func hashShardAddr(addr string) string { return hex.EncodeToString(h[:]) } -func (s *Persistence) ListActors(ctx context.Context, pageSize int32, pageTokenStr string) ([]*ateapipb.Actor, string, error) { +func (s *Persistence) ListActors(ctx context.Context, atespace string, pageSize int32, pageTokenStr string) ([]*ateapipb.Actor, string, error) { token, err := decodePageToken(pageTokenStr) if err != nil { return nil, "", fmt.Errorf("invalid page token: %w", err) @@ -475,7 +478,7 @@ func (s *Persistence) ListActors(ctx context.Context, pageSize int32, pageTokenS } var keys []string - keys, cursor, err = master.Scan(ctx, cursor, "actor:*", int64(remaining)).Result() + keys, cursor, err = master.Scan(ctx, cursor, "actor:"+atespace+":*", int64(remaining)).Result() if err != nil { return nil, "", fmt.Errorf("while scanning shard %s: %w", shardAddr, err) } diff --git a/cmd/ateapi/internal/store/ateredis/ateredis_test.go b/cmd/ateapi/internal/store/ateredis/ateredis_test.go index eb41fc6d..d41b69a9 100644 --- a/cmd/ateapi/internal/store/ateredis/ateredis_test.go +++ b/cmd/ateapi/internal/store/ateredis/ateredis_test.go @@ -49,7 +49,7 @@ func TestGetActor_NotFound(t *testing.T) { mr, s, ctx := setupTest(t) defer mr.Close() - _, err := s.GetActor(ctx, "non-existent") + _, err := s.GetActor(ctx, "", "non-existent") if !errors.Is(err, store.ErrNotFound) { t.Errorf("expected ErrNotFound, got %v", err) } @@ -71,7 +71,7 @@ func TestCreateActor_Success(t *testing.T) { t.Fatalf("CreateActor failed: %v", err) } - got, err := s.GetActor(ctx, actor.ActorId) + got, err := s.GetActor(ctx, actor.GetAtespace(), actor.ActorId) if err != nil { t.Fatalf("GetActor failed: %v", err) } @@ -134,7 +134,7 @@ func TestUpdateActor_Success(t *testing.T) { t.Errorf("expected actor.Version to be updated to 2, got %d", actor.Version) } - updated, err := s.GetActor(ctx, actor.ActorId) + updated, err := s.GetActor(ctx, actor.GetAtespace(), actor.ActorId) if err != nil { t.Fatalf("GetActor failed: %v", err) } @@ -163,13 +163,13 @@ func TestUpdateActor_Conflict(t *testing.T) { } // Fetch instance 1 - actor1, err := s.GetActor(ctx, actor.ActorId) + actor1, err := s.GetActor(ctx, actor.GetAtespace(), actor.ActorId) if err != nil { t.Fatalf("GetActor failed: %v", err) } // Fetch instance 2 (stale after actor1 updates) - actor2, err := s.GetActor(ctx, actor.ActorId) + actor2, err := s.GetActor(ctx, actor.GetAtespace(), actor.ActorId) if err != nil { t.Fatalf("GetActor failed: %v", err) } @@ -310,12 +310,12 @@ func TestDeleteActor(t *testing.T) { t.Fatalf("CreateActor failed: %v", err) } - err = s.DeleteActor(ctx, "session-1") + err = s.DeleteActor(ctx, "", "session-1") if err != nil { t.Fatalf("DeleteActor failed: %v", err) } - _, err = s.GetActor(ctx, "session-1") + _, err = s.GetActor(ctx, "", "session-1") if !errors.Is(err, store.ErrNotFound) { t.Errorf("expected ErrNotFound after delete, got %v", err) } @@ -337,7 +337,7 @@ func TestDeleteActor_NotSuspended(t *testing.T) { t.Fatalf("CreateActor failed: %v", err) } - err = s.DeleteActor(ctx, "session-1") + err = s.DeleteActor(ctx, "", "session-1") if !errors.Is(err, store.ErrFailedPrecondition) { t.Errorf("expected ErrFailedPrecondition deleting running actor, got %v", err) } @@ -347,7 +347,7 @@ func TestDeleteActor_NotFound(t *testing.T) { mr, s, ctx := setupTest(t) defer mr.Close() - err := s.DeleteActor(ctx, "non-existent") + err := s.DeleteActor(ctx, "", "non-existent") if !errors.Is(err, store.ErrNotFound) { t.Errorf("expected ErrNotFound deleting non-existent actor, got %v", err) } @@ -439,7 +439,7 @@ func TestListActors(t *testing.T) { t.Fatalf("failed to create actor2: %v", err) } - actors, _, err := s.ListActors(ctx, 1000, "") + actors, _, err := s.ListActors(ctx, "", 1000, "") if err != nil { t.Fatalf("ListActors failed: %v", err) } @@ -544,7 +544,7 @@ func TestListActors_Empty(t *testing.T) { mr, s, ctx := setupTest(t) defer mr.Close() - actors, _, err := s.ListActors(ctx, 1000, "") + actors, _, err := s.ListActors(ctx, "", 1000, "") if err != nil { t.Fatalf("ListActors failed: %v", err) } @@ -574,7 +574,7 @@ func TestListActors_Pagination(t *testing.T) { pageToken := "" for { - actors, nextToken, err := s.ListActors(ctx, 2, pageToken) + actors, nextToken, err := s.ListActors(ctx, "", 2, pageToken) if err != nil { t.Fatalf("ListActors failed: %v", err) } @@ -781,3 +781,72 @@ func TestAcquireLock_NonReentry(t *testing.T) { t.Errorf("expected second lock acquisition to fail (non-reentrant)") } } + +func TestListActors_ScopedByAtespace(t *testing.T) { + mr, s, ctx := setupTest(t) + defer mr.Close() + + mkActor := func(id, atespace string) *ateapipb.Actor { + return &ateapipb.Actor{ + ActorId: id, + Atespace: atespace, + ActorTemplateNamespace: "ns1", + ActorTemplateName: "tmpl1", + Status: ateapipb.Actor_STATUS_SUSPENDED, + } + } + for _, a := range []*ateapipb.Actor{ + mkActor("a1", "team-a"), + mkActor("a2", "team-a"), + mkActor("b1", "team-b"), + } { + if err := s.CreateActor(ctx, a); err != nil { + t.Fatalf("CreateActor(%s/%s) failed: %v", a.GetAtespace(), a.GetActorId(), err) + } + } + + // List is scoped to one atespace. + teamA, _, err := s.ListActors(ctx, "team-a", 1000, "") + if err != nil { + t.Fatalf("ListActors(team-a) failed: %v", err) + } + if got := actorIDSet(teamA); !got["a1"] || !got["a2"] || got["b1"] || len(got) != 2 { + t.Errorf("ListActors(team-a) = %v, want exactly {a1, a2}", got) + } + + teamB, _, err := s.ListActors(ctx, "team-b", 1000, "") + if err != nil { + t.Fatalf("ListActors(team-b) failed: %v", err) + } + if got := actorIDSet(teamB); !got["b1"] || got["a1"] || len(got) != 1 { + t.Errorf("ListActors(team-b) = %v, want exactly {b1}", got) + } + + // The empty (default) atespace sees none of the namespaced actors. + empty, _, err := s.ListActors(ctx, "", 1000, "") + if err != nil { + t.Fatalf("ListActors(empty) failed: %v", err) + } + if len(empty) != 0 { + t.Errorf("ListActors(empty) = %v, want none", actorIDSet(empty)) + } + + // Get is scoped too: right atespace hits, wrong/empty atespace misses. + if _, err := s.GetActor(ctx, "team-a", "a1"); err != nil { + t.Errorf("GetActor(team-a, a1) failed: %v", err) + } + if _, err := s.GetActor(ctx, "team-b", "a1"); !errors.Is(err, store.ErrNotFound) { + t.Errorf("GetActor(team-b, a1) = %v, want ErrNotFound", err) + } + if _, err := s.GetActor(ctx, "", "a1"); !errors.Is(err, store.ErrNotFound) { + t.Errorf("GetActor(empty, a1) = %v, want ErrNotFound", err) + } +} + +func actorIDSet(actors []*ateapipb.Actor) map[string]bool { + set := make(map[string]bool, len(actors)) + for _, a := range actors { + set[a.GetActorId()] = true + } + return set +} diff --git a/cmd/ateapi/internal/store/store.go b/cmd/ateapi/internal/store/store.go index 638e7f0e..89012025 100644 --- a/cmd/ateapi/internal/store/store.go +++ b/cmd/ateapi/internal/store/store.go @@ -39,8 +39,8 @@ var ( // Interface defines the contract for the persistence layer storing actor state. type Interface interface { - // Fetches an actor by id. Returns ErrNotFound if missing. - GetActor(ctx context.Context, id string) (*ateapipb.Actor, error) + // Fetches an actor by (atespace, id). Returns ErrNotFound if missing. + GetActor(ctx context.Context, atespace, id string) (*ateapipb.Actor, error) // Stores a new actor in suspended state. Returns ErrAlreadyExists if key is taken. CreateActor(ctx context.Context, actor *ateapipb.Actor) error @@ -49,10 +49,10 @@ type Interface interface { UpdateActor(ctx context.Context, actor *ateapipb.Actor, expectedVersion int64) error // Removes an actor. Returns ErrNotFound if missing, or ErrFailedPrecondition if not suspended. - DeleteActor(ctx context.Context, id string) error + DeleteActor(ctx context.Context, atespace, id string) error - // Lists all known actors. Returns a page of actors and a next page token. - ListActors(ctx context.Context, pageSize int32, pageToken string) ([]*ateapipb.Actor, string, error) + // Lists actors in the given atespace (scoped scan). Returns a page of actors and a next page token. + ListActors(ctx context.Context, atespace string, pageSize int32, pageToken string) ([]*ateapipb.Actor, string, error) // Fetches worker state by namespace, pool, and pod name. Returns ErrNotFound if missing. GetWorker(ctx context.Context, namespace, pool, pod string) (*ateapipb.Worker, error) diff --git a/internal/e2e/suites/demo/demo_test.go b/internal/e2e/suites/demo/demo_test.go index a0afe37e..26b8493e 100644 --- a/internal/e2e/suites/demo/demo_test.go +++ b/internal/e2e/suites/demo/demo_test.go @@ -33,6 +33,8 @@ import ( "k8s.io/client-go/transport/spdy" ) +const demoAtespace = "demo" + func TestActorLifecycle(t *testing.T) { // Create namespace nsObj := e2e.CreateNamespace(t) @@ -81,6 +83,7 @@ func createActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj t.Logf("Creating Actor %q using Substrate API...", actorID) createResp, err := clients.SubstrateAPI.CreateActor(ctx, &ateapipb.CreateActorRequest{ + Atespace: demoAtespace, ActorId: actorID, ActorTemplateNamespace: nsObj.Name, ActorTemplateName: at.Name, @@ -91,11 +94,12 @@ func createActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj t.Logf("Successfully created Actor: %s", createResp.GetActor().GetActorId()) defer func() { clients.SubstrateAPI.DeleteActor(ctx, &ateapipb.DeleteActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }) }() - listResp, err := clients.SubstrateAPI.ListActors(ctx, &ateapipb.ListActorsRequest{}) + listResp, err := clients.SubstrateAPI.ListActors(ctx, &ateapipb.ListActorsRequest{Atespace: demoAtespace}) if err != nil { t.Fatalf("ListActors RPC failed: %v", err) } @@ -135,6 +139,7 @@ func pauseActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj * // Creating an actor t.Logf("Creating Actor %q...", actorID) if _, err := clients.SubstrateAPI.CreateActor(ctx, &ateapipb.CreateActorRequest{ + Atespace: demoAtespace, ActorId: actorID, ActorTemplateNamespace: nsObj.Name, ActorTemplateName: at.Name, @@ -146,7 +151,8 @@ func pauseActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj * // Resuming the actor t.Logf("Resuming Actor %q...", actorID) if _, err := clients.SubstrateAPI.ResumeActor(ctx, &ateapipb.ResumeActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }); err != nil { t.Fatalf("failed to resume Actor: %v", err) } @@ -163,7 +169,8 @@ func pauseActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj * // Pausing the actor t.Logf("Pausing Actor %q...", actorID) if _, err := clients.SubstrateAPI.PauseActor(ctx, &ateapipb.PauseActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }); err != nil { t.Fatalf("failed to pause Actor: %v", err) } @@ -172,7 +179,8 @@ func pauseActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj * // Resuming the actor again t.Logf("Resuming Actor %q again...", actorID) if _, err := clients.SubstrateAPI.ResumeActor(ctx, &ateapipb.ResumeActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }); err != nil { t.Fatalf("failed to resume Actor again: %v", err) } @@ -189,7 +197,8 @@ func pauseActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj * // Suspending the actor before deletion t.Logf("Suspending Actor %q before deletion...", actorID) if _, err := clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }); err != nil { t.Fatalf("failed to suspend Actor: %v", err) } @@ -198,13 +207,15 @@ func pauseActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj * // Deleting the actor t.Logf("Deleting Actor %q...", actorID) if _, err := clients.SubstrateAPI.DeleteActor(ctx, &ateapipb.DeleteActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }); err != nil { t.Fatalf("failed to delete Actor: %v", err) } // Verify deletion if _, err := clients.SubstrateAPI.GetActor(ctx, &ateapipb.GetActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }); err == nil { t.Fatalf("expected actor %q to be deleted, but it still exists", actorID) } @@ -218,6 +229,7 @@ func suspendActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj // Creating an actor t.Logf("Creating Actor %q...", actorID) if _, err := clients.SubstrateAPI.CreateActor(ctx, &ateapipb.CreateActorRequest{ + Atespace: demoAtespace, ActorId: actorID, ActorTemplateNamespace: nsObj.Name, ActorTemplateName: at.Name, @@ -229,7 +241,8 @@ func suspendActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj // Resuming the actor t.Logf("Resuming Actor %q...", actorID) if _, err := clients.SubstrateAPI.ResumeActor(ctx, &ateapipb.ResumeActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }); err != nil { t.Fatalf("failed to resume Actor: %v", err) } @@ -246,7 +259,8 @@ func suspendActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj // Suspending the actor t.Logf("Suspending Actor %q...", actorID) if _, err := clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }); err != nil { t.Fatalf("failed to suspend Actor: %v", err) } @@ -255,7 +269,8 @@ func suspendActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj // Resuming the actor again t.Logf("Resuming Actor %q again...", actorID) if _, err := clients.SubstrateAPI.ResumeActor(ctx, &ateapipb.ResumeActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }); err != nil { t.Fatalf("failed to resume Actor again: %v", err) } @@ -272,7 +287,8 @@ func suspendActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj // Suspending the actor before deletion t.Logf("Suspending Actor %q before deletion...", actorID) if _, err := clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }); err != nil { t.Fatalf("failed to suspend Actor: %v", err) } @@ -281,13 +297,15 @@ func suspendActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj // Deleting the actor t.Logf("Deleting Actor %q...", actorID) if _, err := clients.SubstrateAPI.DeleteActor(ctx, &ateapipb.DeleteActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }); err != nil { t.Fatalf("failed to delete Actor: %v", err) } // Verify deletion if _, err := clients.SubstrateAPI.GetActor(ctx, &ateapipb.GetActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }); err == nil { t.Fatalf("expected actor %q to be deleted, but it still exists", actorID) } @@ -391,7 +409,8 @@ func waitForActorStatus(ctx context.Context, t *testing.T, clients *e2e.Clients, deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { resp, err := clients.SubstrateAPI.GetActor(ctx, &ateapipb.GetActorRequest{ - ActorId: actorID, + Atespace: demoAtespace, + ActorId: actorID, }) if err == nil { if resp.GetActor().GetStatus() == expectedStatus { diff --git a/internal/e2e/suites/identity/identity_test.go b/internal/e2e/suites/identity/identity_test.go index 3b0d4879..2081bf99 100644 --- a/internal/e2e/suites/identity/identity_test.go +++ b/internal/e2e/suites/identity/identity_test.go @@ -160,6 +160,7 @@ func waitForGolden(t *testing.T, ctx context.Context, clients *e2e.Clients) stri func createAndResumeActor(t *testing.T, ctx context.Context, clients *e2e.Clients, id string) { t.Helper() if _, err := clients.SubstrateAPI.CreateActor(ctx, &ateapipb.CreateActorRequest{ + Atespace: probeNamespace, ActorId: id, ActorTemplateNamespace: probeNamespace, ActorTemplateName: probeTemplate, @@ -168,12 +169,12 @@ func createAndResumeActor(t *testing.T, ctx context.Context, clients *e2e.Client } t.Cleanup(func() { // DeleteActor requires the actor to be suspended. - _, _ = clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{ActorId: id}) - _, _ = clients.SubstrateAPI.DeleteActor(ctx, &ateapipb.DeleteActorRequest{ActorId: id}) + _, _ = clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Atespace: probeNamespace, ActorId: id}) + _, _ = clients.SubstrateAPI.DeleteActor(ctx, &ateapipb.DeleteActorRequest{Atespace: probeNamespace, ActorId: id}) }) // Resume from the golden snapshot (the restore path, not --boot). - if _, err := clients.SubstrateAPI.ResumeActor(ctx, &ateapipb.ResumeActorRequest{ActorId: id}); err != nil { + if _, err := clients.SubstrateAPI.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Atespace: probeNamespace, ActorId: id}); err != nil { t.Fatalf("ResumeActor %q: %v", id, err) } } diff --git a/internal/resources/actor.go b/internal/resources/actor.go index 5dc42212..a077e094 100644 --- a/internal/resources/actor.go +++ b/internal/resources/actor.go @@ -45,3 +45,15 @@ func ValidateActorID(id string) error { } return nil } + +// ValidateAtespace validates whether the provided atespace name is valid. An +// atespace must be a valid DNS-1123 label (same rules as an actor ID above). +func ValidateAtespace(atespace string) error { + if len(atespace) > 63 { + return fmt.Errorf("invalid atespace: must be no more than 63 characters") + } + if !actorIDRegex.MatchString(atespace) { + return fmt.Errorf("invalid atespace: must start and end with a lower case alphanumeric character, and consist only of lower case alphanumeric characters or '-'") + } + return nil +} From 61d221bbded1d7910ee32571458e6d028ee4156c Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 13:49:06 -0700 Subject: [PATCH 3/6] Add --atespace flags to kubectl-ate --- cmd/kubectl-ate/internal/cmd/create_actor.go | 4 ++++ cmd/kubectl-ate/internal/cmd/delete_actor.go | 7 ++++++- cmd/kubectl-ate/internal/cmd/get_actors.go | 7 ++++++- cmd/kubectl-ate/internal/cmd/logs_actors.go | 11 ++++++++--- cmd/kubectl-ate/internal/cmd/pause_actor.go | 7 ++++++- cmd/kubectl-ate/internal/cmd/resume_actor.go | 8 ++++++-- cmd/kubectl-ate/internal/cmd/suspend_actor.go | 7 ++++++- 7 files changed, 42 insertions(+), 9 deletions(-) diff --git a/cmd/kubectl-ate/internal/cmd/create_actor.go b/cmd/kubectl-ate/internal/cmd/create_actor.go index cd3a4b6b..057c33ac 100644 --- a/cmd/kubectl-ate/internal/cmd/create_actor.go +++ b/cmd/kubectl-ate/internal/cmd/create_actor.go @@ -25,6 +25,7 @@ import ( ) var templateFlag string +var atespaceFlag string var createActorCmd = &cobra.Command{ Use: "actor [actor-id]", @@ -48,6 +49,7 @@ var createActorCmd = &cobra.Command{ ActorTemplateNamespace: parts[0], ActorTemplateName: parts[1], ActorId: actorID, + Atespace: atespaceFlag, }) if err != nil { return fmt.Errorf("failed to create actor: %w", err) @@ -60,5 +62,7 @@ var createActorCmd = &cobra.Command{ func init() { createActorCmd.Flags().StringVarP(&templateFlag, "template", "t", "", "Template to derive the actor from in / format (required)") _ = createActorCmd.MarkFlagRequired("template") + createActorCmd.Flags().StringVar(&atespaceFlag, "atespace", "", "Atespace (tenant) to create the actor in (required)") + _ = createActorCmd.MarkFlagRequired("atespace") createCmd.AddCommand(createActorCmd) } diff --git a/cmd/kubectl-ate/internal/cmd/delete_actor.go b/cmd/kubectl-ate/internal/cmd/delete_actor.go index 54a12df3..df3b610f 100644 --- a/cmd/kubectl-ate/internal/cmd/delete_actor.go +++ b/cmd/kubectl-ate/internal/cmd/delete_actor.go @@ -22,6 +22,8 @@ import ( "github.com/spf13/cobra" ) +var deleteAtespaceFlag string + var deleteActorCmd = &cobra.Command{ Use: "actor [actor-id]", Short: "Delete an actor", @@ -36,7 +38,8 @@ var deleteActorCmd = &cobra.Command{ id := args[0] _, err = c.ControlClient.DeleteActor(ctx, &ateapipb.DeleteActorRequest{ - ActorId: id, + ActorId: id, + Atespace: deleteAtespaceFlag, }) if err != nil { return err @@ -48,5 +51,7 @@ var deleteActorCmd = &cobra.Command{ } func init() { + deleteActorCmd.Flags().StringVar(&deleteAtespaceFlag, "atespace", "", "Atespace (tenant) the actor lives in") + _ = deleteActorCmd.MarkFlagRequired("atespace") deleteCmd.AddCommand(deleteActorCmd) } diff --git a/cmd/kubectl-ate/internal/cmd/get_actors.go b/cmd/kubectl-ate/internal/cmd/get_actors.go index 3ec2ba76..121aa66f 100644 --- a/cmd/kubectl-ate/internal/cmd/get_actors.go +++ b/cmd/kubectl-ate/internal/cmd/get_actors.go @@ -23,6 +23,8 @@ import ( "github.com/spf13/cobra" ) +var getActorsAtespaceFlag string + var getActorsCmd = &cobra.Command{ Use: "actors [actor-id]", Aliases: []string{"actor"}, @@ -39,7 +41,7 @@ var getActorsCmd = &cobra.Command{ // 2. Handle Get Single Actor if len(args) > 0 { - resp, err := apiClient.GetActor(ctx, &ateapipb.GetActorRequest{ActorId: args[0]}) + resp, err := apiClient.GetActor(ctx, &ateapipb.GetActorRequest{ActorId: args[0], Atespace: getActorsAtespaceFlag}) if err != nil { return fmt.Errorf("failed to get actor: %w", err) } @@ -54,6 +56,7 @@ var getActorsCmd = &cobra.Command{ resp, err := apiClient.ListActors(ctx, &ateapipb.ListActorsRequest{ PageSize: 1000, PageToken: pageToken, + Atespace: getActorsAtespaceFlag, }) if err != nil { return fmt.Errorf("failed to list actors: %w", err) @@ -71,5 +74,7 @@ var getActorsCmd = &cobra.Command{ } func init() { + getActorsCmd.Flags().StringVar(&getActorsAtespaceFlag, "atespace", "", "Atespace (tenant) to list/get actors in (required)") + _ = getActorsCmd.MarkFlagRequired("atespace") getCmd.AddCommand(getActorsCmd) } diff --git a/cmd/kubectl-ate/internal/cmd/logs_actors.go b/cmd/kubectl-ate/internal/cmd/logs_actors.go index 2b818787..c70db355 100644 --- a/cmd/kubectl-ate/internal/cmd/logs_actors.go +++ b/cmd/kubectl-ate/internal/cmd/logs_actors.go @@ -39,6 +39,7 @@ import ( ) var followLogs bool +var logsAtespaceFlag string var logsActorsCmd = &cobra.Command{ Use: "actors ", @@ -50,6 +51,8 @@ var logsActorsCmd = &cobra.Command{ func init() { logsActorsCmd.Flags().BoolVarP(&followLogs, "follow", "f", false, "Specify if the logs should be streamed.") + logsActorsCmd.Flags().StringVar(&logsAtespaceFlag, "atespace", "", "Atespace (tenant) the actor lives in") + _ = logsActorsCmd.MarkFlagRequired("atespace") logsCmd.AddCommand(logsActorsCmd) } @@ -77,6 +80,7 @@ func (s *k8sPodLogsStreamer) StreamLogs(ctx context.Context, namespace, podName type LogsActorRunner struct { apiClient AteAPIClient streamer PodLogsStreamer + atespace string stdout io.Writer stderr io.Writer follow bool @@ -105,7 +109,7 @@ func (r *LogsActorRunner) Run(ctx context.Context, actorID string) error { } func (r *LogsActorRunner) runOneShot(ctx context.Context, actorID string) error { - actorResp, err := r.apiClient.GetActor(ctx, &ateapipb.GetActorRequest{ActorId: actorID}) + actorResp, err := r.apiClient.GetActor(ctx, &ateapipb.GetActorRequest{ActorId: actorID, Atespace: r.atespace}) if err != nil { return fmt.Errorf("failed to get actor: %w", err) } @@ -152,7 +156,7 @@ func (r *LogsActorRunner) runFollow(ctx context.Context, actorID string) error { default: } - actorResp, err := r.apiClient.GetActor(ctx, &ateapipb.GetActorRequest{ActorId: actorID}) + actorResp, err := r.apiClient.GetActor(ctx, &ateapipb.GetActorRequest{ActorId: actorID, Atespace: r.atespace}) if err != nil { if status.Code(err) == codes.NotFound { return fmt.Errorf("actor %s not found: %w", actorID, err) @@ -260,7 +264,7 @@ func (r *LogsActorRunner) startMigrationMonitor( case <-ctx.Done(): return case <-ticker.C: - resp, err := r.apiClient.GetActor(ctx, &ateapipb.GetActorRequest{ActorId: actorID}) + resp, err := r.apiClient.GetActor(ctx, &ateapipb.GetActorRequest{ActorId: actorID, Atespace: r.atespace}) if err == nil { act := resp.GetActor() if act.GetStatus() != ateapipb.Actor_STATUS_RUNNING || act.GetAteomPodName() != currentPod { @@ -292,6 +296,7 @@ func runLogsActor(cmd *cobra.Command, args []string) error { runner := &LogsActorRunner{ apiClient: apiClient, streamer: &k8sPodLogsStreamer{clientset: k8sClient}, + atespace: logsAtespaceFlag, stdout: os.Stdout, stderr: os.Stderr, follow: followLogs, diff --git a/cmd/kubectl-ate/internal/cmd/pause_actor.go b/cmd/kubectl-ate/internal/cmd/pause_actor.go index e7a7e75f..697d1fe3 100644 --- a/cmd/kubectl-ate/internal/cmd/pause_actor.go +++ b/cmd/kubectl-ate/internal/cmd/pause_actor.go @@ -23,6 +23,8 @@ import ( "github.com/spf13/cobra" ) +var pauseAtespaceFlag string + var pauseActorCmd = &cobra.Command{ Use: "actor [actor-id]", Short: "Pause an actor", @@ -36,7 +38,8 @@ var pauseActorCmd = &cobra.Command{ defer apiClient.Close() resp, err := apiClient.PauseActor(ctx, &ateapipb.PauseActorRequest{ - ActorId: args[0], + ActorId: args[0], + Atespace: pauseAtespaceFlag, }) if err != nil { return fmt.Errorf("failed to pause actor: %w", err) @@ -47,5 +50,7 @@ var pauseActorCmd = &cobra.Command{ } func init() { + pauseActorCmd.Flags().StringVar(&pauseAtespaceFlag, "atespace", "", "Atespace (tenant) the actor lives in") + _ = pauseActorCmd.MarkFlagRequired("atespace") pauseCmd.AddCommand(pauseActorCmd) } diff --git a/cmd/kubectl-ate/internal/cmd/resume_actor.go b/cmd/kubectl-ate/internal/cmd/resume_actor.go index 440643ab..51835ef1 100644 --- a/cmd/kubectl-ate/internal/cmd/resume_actor.go +++ b/cmd/kubectl-ate/internal/cmd/resume_actor.go @@ -24,6 +24,7 @@ import ( ) var bootFlag bool +var resumeAtespaceFlag string var resumeActorCmd = &cobra.Command{ Use: "actor [actor-id]", @@ -38,8 +39,9 @@ var resumeActorCmd = &cobra.Command{ defer apiClient.Close() resp, err := apiClient.ResumeActor(ctx, &ateapipb.ResumeActorRequest{ - ActorId: args[0], - Boot: bootFlag, + ActorId: args[0], + Boot: bootFlag, + Atespace: resumeAtespaceFlag, }) if err != nil { return fmt.Errorf("failed to resume actor: %w", err) @@ -51,5 +53,7 @@ var resumeActorCmd = &cobra.Command{ func init() { resumeActorCmd.Flags().BoolVarP(&bootFlag, "boot", "", false, "Skip golden snapshot and boot from scratch.") + resumeActorCmd.Flags().StringVar(&resumeAtespaceFlag, "atespace", "", "Atespace (tenant) the actor lives in") + _ = resumeActorCmd.MarkFlagRequired("atespace") resumeCmd.AddCommand(resumeActorCmd) } diff --git a/cmd/kubectl-ate/internal/cmd/suspend_actor.go b/cmd/kubectl-ate/internal/cmd/suspend_actor.go index c36506a9..f1560a0b 100644 --- a/cmd/kubectl-ate/internal/cmd/suspend_actor.go +++ b/cmd/kubectl-ate/internal/cmd/suspend_actor.go @@ -23,6 +23,8 @@ import ( "github.com/spf13/cobra" ) +var suspendAtespaceFlag string + var suspendActorCmd = &cobra.Command{ Use: "actor [actor-id]", Short: "Suspend an actor", @@ -36,7 +38,8 @@ var suspendActorCmd = &cobra.Command{ defer apiClient.Close() resp, err := apiClient.SuspendActor(ctx, &ateapipb.SuspendActorRequest{ - ActorId: args[0], + ActorId: args[0], + Atespace: suspendAtespaceFlag, }) if err != nil { return fmt.Errorf("failed to suspend actor: %w", err) @@ -47,5 +50,7 @@ var suspendActorCmd = &cobra.Command{ } func init() { + suspendActorCmd.Flags().StringVar(&suspendAtespaceFlag, "atespace", "", "Atespace (tenant) the actor lives in") + _ = suspendActorCmd.MarkFlagRequired("atespace") suspendCmd.AddCommand(suspendActorCmd) } From 14c9e5afa1a58a04bd54401997268d2b1d0378d4 Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 21:26:46 -0700 Subject: [PATCH 4/6] List actors across all atespaces and show ATESPACE column --- .../internal/controlapi/functional_test.go | 39 +++++++++++++++++++ cmd/ateapi/internal/controlapi/list_actors.go | 11 +++--- .../internal/store/ateredis/ateredis.go | 15 ++++++- .../internal/store/ateredis/ateredis_test.go | 10 ++--- cmd/kubectl-ate/README.md | 14 +++++-- cmd/kubectl-ate/internal/cmd/get_actors.go | 26 +++++++++++-- cmd/kubectl-ate/internal/printer/printer.go | 8 +++- .../internal/printer/printer_test.go | 17 +++++--- 8 files changed, 114 insertions(+), 26 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/functional_test.go b/cmd/ateapi/internal/controlapi/functional_test.go index e49548f6..a91c366e 100644 --- a/cmd/ateapi/internal/controlapi/functional_test.go +++ b/cmd/ateapi/internal/controlapi/functional_test.go @@ -850,6 +850,45 @@ func TestListActors_ByAtespace(t *testing.T) { assertGrpcError(t, err, codes.NotFound, "Actor id1 not found") } +// TestListActors_AllAtespaces verifies that an empty atespace lists actors across +// all atespaces (the `-A` / admin view), unlike the scoped single-tenant listing. +func TestListActors_AllAtespaces(t *testing.T) { + ns := namespaceForTest("ns-list-all-atespaces") + tc := setupTest(t, ns) + defer tc.cleanup() + + createTemplate(t, tc, ns) + + create := func(id, atespace string) { + if _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + ActorTemplateNamespace: ns, + ActorTemplateName: "tmpl1", + ActorId: id, + Atespace: atespace, + }); err != nil { + t.Fatalf("CreateActor(%s, atespace=%q) failed: %v", id, atespace, err) + } + } + create("id1", "team-a") + create("id2", "team-b") + + // Empty atespace lists across all atespaces; returned actors carry their atespace. + resp, err := tc.client.ListActors(context.Background(), &ateapipb.ListActorsRequest{}) + if err != nil { + t.Fatalf("ListActors(all) failed: %v", err) + } + got := map[string]string{} + for _, a := range resp.GetActors() { + got[a.GetActorId()] = a.GetAtespace() + } + if got["id1"] != "team-a" { + t.Errorf("ListActors(all): got[id1]=%q, want team-a", got["id1"]) + } + if got["id2"] != "team-b" { + t.Errorf("ListActors(all): got[id2]=%q, want team-b", got["id2"]) + } +} + // TestListActors_Pagination tests that ListActors correctly paginates results. func TestListActors_Pagination(t *testing.T) { ns := namespaceForTest("ns-list-actors-pagination") diff --git a/cmd/ateapi/internal/controlapi/list_actors.go b/cmd/ateapi/internal/controlapi/list_actors.go index 054812ea..4c09e8c1 100644 --- a/cmd/ateapi/internal/controlapi/list_actors.go +++ b/cmd/ateapi/internal/controlapi/list_actors.go @@ -46,11 +46,12 @@ func (s *Service) ListActors(ctx context.Context, req *ateapipb.ListActorsReques } func validateListActorsRequest(req *ateapipb.ListActorsRequest) error { - if req.GetAtespace() == "" { - return fmt.Errorf("atespace is required") - } - if err := resources.ValidateAtespace(req.GetAtespace()); err != nil { - return err + // An empty atespace is allowed here and means "all atespaces"(used by `kubectl ate get actors -A`). + // A non-empty atespace is validated and scopes the listing to that tenant. + if req.GetAtespace() != "" { + if err := resources.ValidateAtespace(req.GetAtespace()); err != nil { + return err + } } pageSize := req.GetPageSize() if pageSize < 0 { diff --git a/cmd/ateapi/internal/store/ateredis/ateredis.go b/cmd/ateapi/internal/store/ateredis/ateredis.go index eb81f05b..4854f02f 100644 --- a/cmd/ateapi/internal/store/ateredis/ateredis.go +++ b/cmd/ateapi/internal/store/ateredis/ateredis.go @@ -83,6 +83,16 @@ func actorDBKey(atespace, id string) string { return "actor:" + atespace + ":" + id } +// actorScanPattern returns the SCAN match pattern for listing actors. An empty +// atespace lists across all atespaces (actor:*); a non-empty atespace scopes the +// scan to that tenant (actor::*). +func actorScanPattern(atespace string) string { + if atespace == "" { + return "actor:*" + } + return "actor:" + atespace + ":*" +} + func workerDBKey(namespace, poolName, podName string) string { return "worker:" + namespace + ":" + poolName + ":" + podName } @@ -429,6 +439,9 @@ func hashShardAddr(addr string) string { return hex.EncodeToString(h[:]) } +// ListActors lists actors, scoped to the given atespace. An empty atespace lists +// across all atespaces (SCAN actor:*); a non-empty atespace restricts the scan to +// that tenant (SCAN actor::*). func (s *Persistence) ListActors(ctx context.Context, atespace string, pageSize int32, pageTokenStr string) ([]*ateapipb.Actor, string, error) { token, err := decodePageToken(pageTokenStr) if err != nil { @@ -478,7 +491,7 @@ func (s *Persistence) ListActors(ctx context.Context, atespace string, pageSize } var keys []string - keys, cursor, err = master.Scan(ctx, cursor, "actor:"+atespace+":*", int64(remaining)).Result() + keys, cursor, err = master.Scan(ctx, cursor, actorScanPattern(atespace), int64(remaining)).Result() if err != nil { return nil, "", fmt.Errorf("while scanning shard %s: %w", shardAddr, err) } diff --git a/cmd/ateapi/internal/store/ateredis/ateredis_test.go b/cmd/ateapi/internal/store/ateredis/ateredis_test.go index d41b69a9..409838b0 100644 --- a/cmd/ateapi/internal/store/ateredis/ateredis_test.go +++ b/cmd/ateapi/internal/store/ateredis/ateredis_test.go @@ -822,13 +822,13 @@ func TestListActors_ScopedByAtespace(t *testing.T) { t.Errorf("ListActors(team-b) = %v, want exactly {b1}", got) } - // The empty (default) atespace sees none of the namespaced actors. - empty, _, err := s.ListActors(ctx, "", 1000, "") + // An empty atespace lists across all atespaces (the admin/dev `-A` view). + all, _, err := s.ListActors(ctx, "", 1000, "") if err != nil { - t.Fatalf("ListActors(empty) failed: %v", err) + t.Fatalf("ListActors(all) failed: %v", err) } - if len(empty) != 0 { - t.Errorf("ListActors(empty) = %v, want none", actorIDSet(empty)) + if got := actorIDSet(all); !got["a1"] || !got["a2"] || !got["b1"] || len(got) != 3 { + t.Errorf("ListActors(all) = %v, want exactly {a1, a2, b1}", got) } // Get is scoped too: right atespace hits, wrong/empty atespace misses. diff --git a/cmd/kubectl-ate/README.md b/cmd/kubectl-ate/README.md index b45679ae..c1ff4373 100644 --- a/cmd/kubectl-ate/README.md +++ b/cmd/kubectl-ate/README.md @@ -74,23 +74,29 @@ These flags can be appended to any command: List and inspect the state of actors and workers across the cluster. ```bash -# List all actors in a clean table format -kubectl ate get actors +# List actors in one atespace (tenant) +kubectl ate get actors --atespace + +# List actors across all atespaces +kubectl ate get actors -A # Get a specific actor by ID and output as raw YAML -kubectl ate get actor -o yaml +kubectl ate get actor --atespace -o yaml # List all physical workers and see which actors are assigned to them kubectl ate get workers ``` +> **Note:** `get actors` requires either `--atespace ` (one tenant) or `-A`/`--all-atespaces` (all tenants) — there is no default atespace. Getting a single actor always requires `--atespace`, since an actor is addressed by `(atespace, id)`. + > **Note:** Actors and workers are not Kubernetes CRDs — they live in the Substrate control plane (valkey/redis), not `etcd`. `kubectl get actor` and `kubectl get worker` will not return anything; only `kubectl ate get …` queries the control plane. `kubectl get actortemplate` and `kubectl get workerpool` *do* work, because those are CRDs. #### `kubectl ate get actor` output columns | Column | Meaning | |---|---| -| `NAMESPACE` | The namespace of the `ActorTemplate` the actor was created from. | +| `ATESPACE` | The atespace (tenant boundary) the actor belongs to. Part of the actor's identity; folded into the storage key as `actor::`. | +| `NAMESPACE` | The namespace of the `ActorTemplate` the actor was created from (distinct from `ATESPACE`). | | `TEMPLATE` | The `ActorTemplate` name. | | `ID` | Actor ID. User-provided for application actors; UUID for the golden actor that each template materialises during `ResumeGoldenActor`. | | `STATUS` | One of `STATUS_RESUMING`, `STATUS_RUNNING`, `STATUS_SUSPENDING`, `STATUS_SUSPENDED`. | diff --git a/cmd/kubectl-ate/internal/cmd/get_actors.go b/cmd/kubectl-ate/internal/cmd/get_actors.go index 121aa66f..29551ec4 100644 --- a/cmd/kubectl-ate/internal/cmd/get_actors.go +++ b/cmd/kubectl-ate/internal/cmd/get_actors.go @@ -23,7 +23,10 @@ import ( "github.com/spf13/cobra" ) -var getActorsAtespaceFlag string +var ( + getActorsAtespaceFlag string + getActorsAllAtespaces bool +) var getActorsCmd = &cobra.Command{ Use: "actors [actor-id]", @@ -41,6 +44,14 @@ var getActorsCmd = &cobra.Command{ // 2. Handle Get Single Actor if len(args) > 0 { + // A single actor is addressed by (atespace, id), so the tenant is + // mandatory and "all atespaces" is meaningless here. + if getActorsAllAtespaces { + return fmt.Errorf("-A/--all-atespaces cannot be used when getting a specific actor; pass --atespace") + } + if getActorsAtespaceFlag == "" { + return fmt.Errorf("--atespace is required when getting a specific actor") + } resp, err := apiClient.GetActor(ctx, &ateapipb.GetActorRequest{ActorId: args[0], Atespace: getActorsAtespaceFlag}) if err != nil { return fmt.Errorf("failed to get actor: %w", err) @@ -48,6 +59,15 @@ var getActorsCmd = &cobra.Command{ return printer.PrintActor(resp.GetActor(), outputFmt) } + // Listing requires exactly one of --atespace (one tenant) or -A (all + // tenants). There is no default atespace to fall back on. + if getActorsAllAtespaces && getActorsAtespaceFlag != "" { + return fmt.Errorf("--atespace and -A/--all-atespaces are mutually exclusive") + } + if !getActorsAllAtespaces && getActorsAtespaceFlag == "" { + return fmt.Errorf("specify --atespace to list one atespace, or -A/--all-atespaces for all") + } + // 3. Handle List All Actors var allActors []*ateapipb.Actor pageToken := "" @@ -74,7 +94,7 @@ var getActorsCmd = &cobra.Command{ } func init() { - getActorsCmd.Flags().StringVar(&getActorsAtespaceFlag, "atespace", "", "Atespace (tenant) to list/get actors in (required)") - _ = getActorsCmd.MarkFlagRequired("atespace") + getActorsCmd.Flags().StringVar(&getActorsAtespaceFlag, "atespace", "", "Atespace (tenant) to list/get actors in. Required for a single actor; for listing, use this or -A.") + getActorsCmd.Flags().BoolVarP(&getActorsAllAtespaces, "all-atespaces", "A", false, "List actors across all atespaces (listing only; mutually exclusive with --atespace)") getCmd.AddCommand(getActorsCmd) } diff --git a/cmd/kubectl-ate/internal/printer/printer.go b/cmd/kubectl-ate/internal/printer/printer.go index 12312e08..97f66e2c 100644 --- a/cmd/kubectl-ate/internal/printer/printer.go +++ b/cmd/kubectl-ate/internal/printer/printer.go @@ -36,6 +36,9 @@ func PrintActors(actors []*ateapipb.Actor, format string) error { func sortActors(actors []*ateapipb.Actor) { slices.SortFunc(actors, func(a, b *ateapipb.Actor) int { + if c := cmp.Compare(a.GetAtespace(), b.GetAtespace()); c != 0 { + return c + } if c := cmp.Compare(a.GetActorTemplateNamespace(), b.GetActorTemplateNamespace()); c != 0 { return c } @@ -54,8 +57,9 @@ func PrintActorsTo(out io.Writer, actors []*ateapipb.Actor, format string) error return printProto(out, &ateapipb.ListActorsResponse{Actors: actors}, format) case "table": w := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) - fmt.Fprintln(w, "NAMESPACE\tTEMPLATE\tID\tSTATUS\tATEOM POD\tATEOM IP\tVERSION") + fmt.Fprintln(w, "ATESPACE\tNAMESPACE\tTEMPLATE\tID\tSTATUS\tATEOM POD\tATEOM IP\tVERSION") for _, actor := range actors { + atespace := actor.GetAtespace() ns := actor.GetActorTemplateNamespace() tmpl := actor.GetActorTemplateName() id := actor.GetActorId() @@ -67,7 +71,7 @@ func PrintActorsTo(out io.Writer, actors []*ateapipb.Actor, format string) error } version := actor.GetVersion() - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%d\n", ns, tmpl, id, status, worker, actor.GetAteomPodIp(), version) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%d\n", atespace, ns, tmpl, id, status, worker, actor.GetAteomPodIp(), version) } return w.Flush() default: diff --git a/cmd/kubectl-ate/internal/printer/printer_test.go b/cmd/kubectl-ate/internal/printer/printer_test.go index 69f55bfc..185b38fe 100644 --- a/cmd/kubectl-ate/internal/printer/printer_test.go +++ b/cmd/kubectl-ate/internal/printer/printer_test.go @@ -27,6 +27,7 @@ func TestPrintActorsTo_Table(t *testing.T) { actors := []*ateapipb.Actor{ { ActorId: "id-1", + Atespace: "team-a", ActorTemplateNamespace: "default", ActorTemplateName: "template-1", Status: ateapipb.Actor_STATUS_RUNNING, @@ -42,8 +43,8 @@ func TestPrintActorsTo_Table(t *testing.T) { } output := buf.String() - expected := `NAMESPACE TEMPLATE ID STATUS ATEOM POD ATEOM IP VERSION -default template-1 id-1 STATUS_RUNNING worker-ns/pod-1 1.2.3.4 2 + expected := `ATESPACE NAMESPACE TEMPLATE ID STATUS ATEOM POD ATEOM IP VERSION +team-a default template-1 id-1 STATUS_RUNNING worker-ns/pod-1 1.2.3.4 2 ` if diff := cmp.Diff(expected, output); diff != "" { t.Errorf("output mismatch (-want +got):\n%s", diff) @@ -106,18 +107,21 @@ func TestPrintActorsTo_Table_Sorted(t *testing.T) { actors := []*ateapipb.Actor{ { ActorId: "zebra", + Atespace: "team-b", ActorTemplateNamespace: "default", ActorTemplateName: "template-1", Status: ateapipb.Actor_STATUS_SUSPENDED, }, { ActorId: "alpha", + Atespace: "team-a", ActorTemplateNamespace: "default", ActorTemplateName: "template-1", Status: ateapipb.Actor_STATUS_RUNNING, }, { ActorId: "beta", + Atespace: "team-a", ActorTemplateNamespace: "other", ActorTemplateName: "template-2", Status: ateapipb.Actor_STATUS_SUSPENDED, @@ -128,10 +132,11 @@ func TestPrintActorsTo_Table_Sorted(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - expected := `NAMESPACE TEMPLATE ID STATUS ATEOM POD ATEOM IP VERSION -default template-1 alpha STATUS_RUNNING 0 -default template-1 zebra STATUS_SUSPENDED 0 -other template-2 beta STATUS_SUSPENDED 0 + // Sorted by atespace first, then template namespace, template name, id. + expected := `ATESPACE NAMESPACE TEMPLATE ID STATUS ATEOM POD ATEOM IP VERSION +team-a default template-1 alpha STATUS_RUNNING 0 +team-a other template-2 beta STATUS_SUSPENDED 0 +team-b default template-1 zebra STATUS_SUSPENDED 0 ` if diff := cmp.Diff(expected, buf.String()); diff != "" { t.Errorf("output mismatch (-want +got):\n%s", diff) From c52692ff2082d8eba8c9597ac34328749a4fc47e Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 21:57:15 -0700 Subject: [PATCH 5/6] kubectl-ate: -a shorthand for --atespace and TEMPLATE NS column --- cmd/kubectl-ate/README.md | 7 ++++--- cmd/kubectl-ate/internal/cmd/create_actor.go | 2 +- cmd/kubectl-ate/internal/cmd/delete_actor.go | 2 +- cmd/kubectl-ate/internal/cmd/get_actors.go | 2 +- cmd/kubectl-ate/internal/cmd/logs_actors.go | 2 +- cmd/kubectl-ate/internal/cmd/pause_actor.go | 2 +- cmd/kubectl-ate/internal/cmd/resume_actor.go | 2 +- cmd/kubectl-ate/internal/cmd/suspend_actor.go | 2 +- cmd/kubectl-ate/internal/printer/printer.go | 2 +- cmd/kubectl-ate/internal/printer/printer_test.go | 12 ++++++------ 10 files changed, 18 insertions(+), 17 deletions(-) diff --git a/cmd/kubectl-ate/README.md b/cmd/kubectl-ate/README.md index c1ff4373..7ad875ba 100644 --- a/cmd/kubectl-ate/README.md +++ b/cmd/kubectl-ate/README.md @@ -74,8 +74,9 @@ These flags can be appended to any command: List and inspect the state of actors and workers across the cluster. ```bash -# List actors in one atespace (tenant) +# List actors in one atespace (tenant); -a is shorthand for --atespace kubectl ate get actors --atespace +kubectl ate get actors -a # List actors across all atespaces kubectl ate get actors -A @@ -87,7 +88,7 @@ kubectl ate get actor --atespace -o yaml kubectl ate get workers ``` -> **Note:** `get actors` requires either `--atespace ` (one tenant) or `-A`/`--all-atespaces` (all tenants) — there is no default atespace. Getting a single actor always requires `--atespace`, since an actor is addressed by `(atespace, id)`. +> **Note:** `get actors` requires either `--atespace ` / `-a ` (one tenant) or `-A`/`--all-atespaces` (all tenants) — there is no default atespace. Getting a single actor always requires `--atespace`/`-a`, since an actor is addressed by `(atespace, id)`. `-a` (lower-case) scopes to one atespace; `-A` (upper-case) spans all. > **Note:** Actors and workers are not Kubernetes CRDs — they live in the Substrate control plane (valkey/redis), not `etcd`. `kubectl get actor` and `kubectl get worker` will not return anything; only `kubectl ate get …` queries the control plane. `kubectl get actortemplate` and `kubectl get workerpool` *do* work, because those are CRDs. @@ -96,7 +97,7 @@ kubectl ate get workers | Column | Meaning | |---|---| | `ATESPACE` | The atespace (tenant boundary) the actor belongs to. Part of the actor's identity; folded into the storage key as `actor::`. | -| `NAMESPACE` | The namespace of the `ActorTemplate` the actor was created from (distinct from `ATESPACE`). | +| `TEMPLATE NS` | The namespace of the `ActorTemplate` the actor was created from (distinct from `ATESPACE`). | | `TEMPLATE` | The `ActorTemplate` name. | | `ID` | Actor ID. User-provided for application actors; UUID for the golden actor that each template materialises during `ResumeGoldenActor`. | | `STATUS` | One of `STATUS_RESUMING`, `STATUS_RUNNING`, `STATUS_SUSPENDING`, `STATUS_SUSPENDED`. | diff --git a/cmd/kubectl-ate/internal/cmd/create_actor.go b/cmd/kubectl-ate/internal/cmd/create_actor.go index 057c33ac..ecc08fdb 100644 --- a/cmd/kubectl-ate/internal/cmd/create_actor.go +++ b/cmd/kubectl-ate/internal/cmd/create_actor.go @@ -62,7 +62,7 @@ var createActorCmd = &cobra.Command{ func init() { createActorCmd.Flags().StringVarP(&templateFlag, "template", "t", "", "Template to derive the actor from in / format (required)") _ = createActorCmd.MarkFlagRequired("template") - createActorCmd.Flags().StringVar(&atespaceFlag, "atespace", "", "Atespace (tenant) to create the actor in (required)") + createActorCmd.Flags().StringVarP(&atespaceFlag, "atespace", "a", "", "Atespace (tenant) to create the actor in (required)") _ = createActorCmd.MarkFlagRequired("atespace") createCmd.AddCommand(createActorCmd) } diff --git a/cmd/kubectl-ate/internal/cmd/delete_actor.go b/cmd/kubectl-ate/internal/cmd/delete_actor.go index df3b610f..27cfee82 100644 --- a/cmd/kubectl-ate/internal/cmd/delete_actor.go +++ b/cmd/kubectl-ate/internal/cmd/delete_actor.go @@ -51,7 +51,7 @@ var deleteActorCmd = &cobra.Command{ } func init() { - deleteActorCmd.Flags().StringVar(&deleteAtespaceFlag, "atespace", "", "Atespace (tenant) the actor lives in") + deleteActorCmd.Flags().StringVarP(&deleteAtespaceFlag, "atespace", "a", "", "Atespace (tenant) the actor lives in") _ = deleteActorCmd.MarkFlagRequired("atespace") deleteCmd.AddCommand(deleteActorCmd) } diff --git a/cmd/kubectl-ate/internal/cmd/get_actors.go b/cmd/kubectl-ate/internal/cmd/get_actors.go index 29551ec4..91c4a60f 100644 --- a/cmd/kubectl-ate/internal/cmd/get_actors.go +++ b/cmd/kubectl-ate/internal/cmd/get_actors.go @@ -94,7 +94,7 @@ var getActorsCmd = &cobra.Command{ } func init() { - getActorsCmd.Flags().StringVar(&getActorsAtespaceFlag, "atespace", "", "Atespace (tenant) to list/get actors in. Required for a single actor; for listing, use this or -A.") + getActorsCmd.Flags().StringVarP(&getActorsAtespaceFlag, "atespace", "a", "", "Atespace (tenant) to list/get actors in. Required for a single actor; for listing, use this or -A.") getActorsCmd.Flags().BoolVarP(&getActorsAllAtespaces, "all-atespaces", "A", false, "List actors across all atespaces (listing only; mutually exclusive with --atespace)") getCmd.AddCommand(getActorsCmd) } diff --git a/cmd/kubectl-ate/internal/cmd/logs_actors.go b/cmd/kubectl-ate/internal/cmd/logs_actors.go index c70db355..d7ab65bf 100644 --- a/cmd/kubectl-ate/internal/cmd/logs_actors.go +++ b/cmd/kubectl-ate/internal/cmd/logs_actors.go @@ -51,7 +51,7 @@ var logsActorsCmd = &cobra.Command{ func init() { logsActorsCmd.Flags().BoolVarP(&followLogs, "follow", "f", false, "Specify if the logs should be streamed.") - logsActorsCmd.Flags().StringVar(&logsAtespaceFlag, "atespace", "", "Atespace (tenant) the actor lives in") + logsActorsCmd.Flags().StringVarP(&logsAtespaceFlag, "atespace", "a", "", "Atespace (tenant) the actor lives in") _ = logsActorsCmd.MarkFlagRequired("atespace") logsCmd.AddCommand(logsActorsCmd) } diff --git a/cmd/kubectl-ate/internal/cmd/pause_actor.go b/cmd/kubectl-ate/internal/cmd/pause_actor.go index 697d1fe3..cad22b3a 100644 --- a/cmd/kubectl-ate/internal/cmd/pause_actor.go +++ b/cmd/kubectl-ate/internal/cmd/pause_actor.go @@ -50,7 +50,7 @@ var pauseActorCmd = &cobra.Command{ } func init() { - pauseActorCmd.Flags().StringVar(&pauseAtespaceFlag, "atespace", "", "Atespace (tenant) the actor lives in") + pauseActorCmd.Flags().StringVarP(&pauseAtespaceFlag, "atespace", "a", "", "Atespace (tenant) the actor lives in") _ = pauseActorCmd.MarkFlagRequired("atespace") pauseCmd.AddCommand(pauseActorCmd) } diff --git a/cmd/kubectl-ate/internal/cmd/resume_actor.go b/cmd/kubectl-ate/internal/cmd/resume_actor.go index 51835ef1..5bd04b27 100644 --- a/cmd/kubectl-ate/internal/cmd/resume_actor.go +++ b/cmd/kubectl-ate/internal/cmd/resume_actor.go @@ -53,7 +53,7 @@ var resumeActorCmd = &cobra.Command{ func init() { resumeActorCmd.Flags().BoolVarP(&bootFlag, "boot", "", false, "Skip golden snapshot and boot from scratch.") - resumeActorCmd.Flags().StringVar(&resumeAtespaceFlag, "atespace", "", "Atespace (tenant) the actor lives in") + resumeActorCmd.Flags().StringVarP(&resumeAtespaceFlag, "atespace", "a", "", "Atespace (tenant) the actor lives in") _ = resumeActorCmd.MarkFlagRequired("atespace") resumeCmd.AddCommand(resumeActorCmd) } diff --git a/cmd/kubectl-ate/internal/cmd/suspend_actor.go b/cmd/kubectl-ate/internal/cmd/suspend_actor.go index f1560a0b..c2b53439 100644 --- a/cmd/kubectl-ate/internal/cmd/suspend_actor.go +++ b/cmd/kubectl-ate/internal/cmd/suspend_actor.go @@ -50,7 +50,7 @@ var suspendActorCmd = &cobra.Command{ } func init() { - suspendActorCmd.Flags().StringVar(&suspendAtespaceFlag, "atespace", "", "Atespace (tenant) the actor lives in") + suspendActorCmd.Flags().StringVarP(&suspendAtespaceFlag, "atespace", "a", "", "Atespace (tenant) the actor lives in") _ = suspendActorCmd.MarkFlagRequired("atespace") suspendCmd.AddCommand(suspendActorCmd) } diff --git a/cmd/kubectl-ate/internal/printer/printer.go b/cmd/kubectl-ate/internal/printer/printer.go index 97f66e2c..f6c5e3da 100644 --- a/cmd/kubectl-ate/internal/printer/printer.go +++ b/cmd/kubectl-ate/internal/printer/printer.go @@ -57,7 +57,7 @@ func PrintActorsTo(out io.Writer, actors []*ateapipb.Actor, format string) error return printProto(out, &ateapipb.ListActorsResponse{Actors: actors}, format) case "table": w := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) - fmt.Fprintln(w, "ATESPACE\tNAMESPACE\tTEMPLATE\tID\tSTATUS\tATEOM POD\tATEOM IP\tVERSION") + fmt.Fprintln(w, "ATESPACE\tTEMPLATE NS\tTEMPLATE\tID\tSTATUS\tATEOM POD\tATEOM IP\tVERSION") for _, actor := range actors { atespace := actor.GetAtespace() ns := actor.GetActorTemplateNamespace() diff --git a/cmd/kubectl-ate/internal/printer/printer_test.go b/cmd/kubectl-ate/internal/printer/printer_test.go index 185b38fe..fd0e1c97 100644 --- a/cmd/kubectl-ate/internal/printer/printer_test.go +++ b/cmd/kubectl-ate/internal/printer/printer_test.go @@ -43,8 +43,8 @@ func TestPrintActorsTo_Table(t *testing.T) { } output := buf.String() - expected := `ATESPACE NAMESPACE TEMPLATE ID STATUS ATEOM POD ATEOM IP VERSION -team-a default template-1 id-1 STATUS_RUNNING worker-ns/pod-1 1.2.3.4 2 + expected := `ATESPACE TEMPLATE NS TEMPLATE ID STATUS ATEOM POD ATEOM IP VERSION +team-a default template-1 id-1 STATUS_RUNNING worker-ns/pod-1 1.2.3.4 2 ` if diff := cmp.Diff(expected, output); diff != "" { t.Errorf("output mismatch (-want +got):\n%s", diff) @@ -133,10 +133,10 @@ func TestPrintActorsTo_Table_Sorted(t *testing.T) { } // Sorted by atespace first, then template namespace, template name, id. - expected := `ATESPACE NAMESPACE TEMPLATE ID STATUS ATEOM POD ATEOM IP VERSION -team-a default template-1 alpha STATUS_RUNNING 0 -team-a other template-2 beta STATUS_SUSPENDED 0 -team-b default template-1 zebra STATUS_SUSPENDED 0 + expected := `ATESPACE TEMPLATE NS TEMPLATE ID STATUS ATEOM POD ATEOM IP VERSION +team-a default template-1 alpha STATUS_RUNNING 0 +team-a other template-2 beta STATUS_SUSPENDED 0 +team-b default template-1 zebra STATUS_SUSPENDED 0 ` if diff := cmp.Diff(expected, buf.String()); diff != "" { t.Errorf("output mismatch (-want +got):\n%s", diff) From 26d007411e2a784f52c75e7d7216d84c29840dd5 Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 23:26:47 -0700 Subject: [PATCH 6/6] Wire atespace through atecontroller golden actors and atenet routing --- .../controllers/actortemplate_controller.go | 7 +- cmd/atenet/internal/dns/corefile.go | 5 +- cmd/atenet/internal/dns/corefile_test.go | 2 +- cmd/atenet/internal/router/extproc.go | 6 +- cmd/atenet/internal/router/extproc_in.go | 24 +++---- cmd/atenet/internal/router/extproc_in_test.go | 66 +++++++++++-------- cmd/atenet/internal/router/extproc_test.go | 16 ++--- cmd/atenet/internal/router/resumer.go | 10 +-- cmd/atenet/internal/router/resumer_test.go | 9 +-- demos/sandbox/client/main.go | 15 +++-- internal/e2e/router_client.go | 9 +-- internal/e2e/suites/demo/demo_test.go | 13 ++-- internal/e2e/suites/identity/identity_test.go | 2 +- internal/resources/actor.go | 32 ++++++++- 14 files changed, 132 insertions(+), 84 deletions(-) diff --git a/cmd/atecontroller/internal/controllers/actortemplate_controller.go b/cmd/atecontroller/internal/controllers/actortemplate_controller.go index 37289011..630820d0 100644 --- a/cmd/atecontroller/internal/controllers/actortemplate_controller.go +++ b/cmd/atecontroller/internal/controllers/actortemplate_controller.go @@ -73,6 +73,7 @@ func (r *ActorTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Reques createReq := &ateapipb.CreateActorRequest{ ActorId: actorID, + Atespace: at.ObjectMeta.Namespace, ActorTemplateNamespace: at.ObjectMeta.Namespace, ActorTemplateName: at.ObjectMeta.Name, } @@ -101,7 +102,8 @@ func (r *ActorTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Reques // TODO: Maybe this should go through a different RPC dedicated to // booting an actor from scratch. resumeReq := &ateapipb.ResumeActorRequest{ - ActorId: at.Status.GoldenActorID, + ActorId: at.Status.GoldenActorID, + Atespace: at.ObjectMeta.Namespace, } _, err := r.AteClient.ResumeActor(ctx, resumeReq) if err != nil { @@ -127,7 +129,8 @@ func (r *ActorTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Reques // from it. req := &ateapipb.SuspendActorRequest{ - ActorId: at.Status.GoldenActorID, + ActorId: at.Status.GoldenActorID, + Atespace: at.ObjectMeta.Namespace, } resp, err := r.AteClient.SuspendActor(ctx, req) if err != nil { diff --git a/cmd/atenet/internal/dns/corefile.go b/cmd/atenet/internal/dns/corefile.go index 7e243f93..6e545a8b 100644 --- a/cmd/atenet/internal/dns/corefile.go +++ b/cmd/atenet/internal/dns/corefile.go @@ -39,11 +39,12 @@ func buildTemplate() string { directives = append(directives, "ready :8181") directives = append(directives, "reload") - // Construct match pattern for .. + // Construct match pattern for ... Both the + // actor id and the atespace are DNS-1123 labels (same regex). directives = append(directives, fmt.Sprintf("template IN A %s {", resources.ActorDNSSuffix)) dnsDomainParts := strings.Split("."+resources.ActorDNSSuffix+".", ".") dnsDomainRef := strings.Join(dnsDomainParts, `\.`) - directives = append(directives, fmt.Sprintf(` match "^%s%s$"`, resources.ActorIDRegexPattern, dnsDomainRef)) + directives = append(directives, fmt.Sprintf(` match "^%s\.%s%s$"`, resources.ActorIDRegexPattern, resources.ActorIDRegexPattern, dnsDomainRef)) // Note the %s -- this will be filled with the router IP. directives = append(directives, ` answer "{{ .Name }} 60 IN A %s"`) directives = append(directives, "}") diff --git a/cmd/atenet/internal/dns/corefile_test.go b/cmd/atenet/internal/dns/corefile_test.go index df565ad0..a8cffcf2 100644 --- a/cmd/atenet/internal/dns/corefile_test.go +++ b/cmd/atenet/internal/dns/corefile_test.go @@ -38,7 +38,7 @@ func TestMakeCoreFile(t *testing.T) { "ready :8181", "reload", "template IN A actors.resources.substrate.ate.dev {", - `match "^` + resources.ActorIDRegexPattern + `\.actors\.resources\.substrate\.ate\.dev\.$"`, + `match "^` + resources.ActorIDRegexPattern + `\.` + resources.ActorIDRegexPattern + `\.actors\.resources\.substrate\.ate\.dev\.$"`, `answer "{{ .Name }} 60 IN A 10.240.0.10"`, }, }, diff --git a/cmd/atenet/internal/router/extproc.go b/cmd/atenet/internal/router/extproc.go index 7c8c184e..26efbf5f 100644 --- a/cmd/atenet/internal/router/extproc.go +++ b/cmd/atenet/internal/router/extproc.go @@ -129,14 +129,14 @@ func (s *ExtProcServer) handleRequestHeaders( metadata := newRequestMetadata(reqHeaders.Headers.GetHeaders()) slog.InfoContext(ctx, "Request", slog.String("metadata", metadata.String())) - actorID, err := parseActorID(metadata.host) + atespace, actorID, err := parseActorRef(metadata.host) if err != nil { // Host is invalid, respond with 404. return nil, metadata, "", "", "", invalidHostErr(metadata.host, err) } - slog.InfoContext(ctx, "ResumeActor", slog.String("actorID", actorID)) - actor, err := s.resumer.ResumeActor(ctx, actorID) + slog.InfoContext(ctx, "ResumeActor", slog.String("atespace", atespace), slog.String("actorID", actorID)) + actor, err := s.resumer.ResumeActor(ctx, atespace, actorID) slog.InfoContext(ctx, "ResumeActor result", slog.String("actor", fmt.Sprintf("%+v", actor)), diff --git a/cmd/atenet/internal/router/extproc_in.go b/cmd/atenet/internal/router/extproc_in.go index a394f98d..6cca7a0e 100644 --- a/cmd/atenet/internal/router/extproc_in.go +++ b/cmd/atenet/internal/router/extproc_in.go @@ -61,21 +61,17 @@ func newRequestMetadata(headers []*corev3.HeaderValue) *requestMetadata { } } -func parseActorID(host string) (string, error) { - var err error +// parseActorRef extracts the (atespace, actor id) an incoming request is +// addressed to from its Host/:authority, which has the form +// "..actors.resources.substrate.ate.dev" (optionally with a +// port). The atespace is required because an actor id is only unique within its +// atespace. +func parseActorRef(host string) (atespace, actorID string, err error) { if strings.Contains(host, ":") { host, _, err = net.SplitHostPort(host) + if err != nil { + return "", "", err + } } - if err != nil { - return "", err - } - actorID, found := strings.CutSuffix(strings.TrimSuffix(host, "."), "."+resources.ActorDNSSuffix) - if !found { - return "", fmt.Errorf("invalid actor_id: must end with %s, got %q", resources.ActorDNSSuffix, host) - } - if err := resources.ValidateActorID(actorID); err != nil { - return "", err - } - - return actorID, nil + return resources.ParseActorDNSName(host) } diff --git a/cmd/atenet/internal/router/extproc_in_test.go b/cmd/atenet/internal/router/extproc_in_test.go index 8fa9ea07..e551a56c 100644 --- a/cmd/atenet/internal/router/extproc_in_test.go +++ b/cmd/atenet/internal/router/extproc_in_test.go @@ -135,60 +135,68 @@ func TestRequestMetadata_String(t *testing.T) { } } -func TestParseActorID(t *testing.T) { +func TestParseActorRef(t *testing.T) { tests := []struct { - name string - host string - wantID string - wantErr bool + name string + host string + wantAtespace string + wantID string + wantErr bool }{ { - name: "valid host without port", - host: "my-actor.actors.resources.substrate.ate.dev", - wantID: "my-actor", - wantErr: false, + name: "valid host without port", + host: "my-actor.team-a.actors.resources.substrate.ate.dev", + wantAtespace: "team-a", + wantID: "my-actor", + wantErr: false, + }, + { + name: "valid host with port", + host: "my-actor.team-a.actors.resources.substrate.ate.dev:8443", + wantAtespace: "team-a", + wantID: "my-actor", + wantErr: false, }, { - name: "valid host with port", - host: "my-actor.actors.resources.substrate.ate.dev:8443", - wantID: "my-actor", - wantErr: false, + name: "valid host with trailing dot", + host: "my-actor.team-a.actors.resources.substrate.ate.dev.", + wantAtespace: "team-a", + wantID: "my-actor", + wantErr: false, }, { - name: "valid host with trailing dot", - host: "my-actor.actors.resources.substrate.ate.dev.", - wantID: "my-actor", - wantErr: false, + name: "valid host with trailing dot and port", + host: "my-actor.team-a.actors.resources.substrate.ate.dev.:8080", + wantAtespace: "team-a", + wantID: "my-actor", + wantErr: false, }, { - name: "valid host with trailing dot and port", - host: "my-actor.actors.resources.substrate.ate.dev.:8080", - wantID: "my-actor", - wantErr: false, + name: "missing atespace label", + host: "my-actor.actors.resources.substrate.ate.dev", + wantErr: true, }, { name: "invalid suffix", - host: "my-actor.example.com", - wantID: "", + host: "my-actor.team-a.example.com", wantErr: true, }, { name: "invalid host port format", - host: "my-actor.actors.resources.substrate.ate.dev:invalid:port", - wantID: "", + host: "my-actor.team-a.actors.resources.substrate.ate.dev:invalid:port", wantErr: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - gotID, err := parseActorID(tc.host) + gotAtespace, gotID, err := parseActorRef(tc.host) if (err != nil) != tc.wantErr { - t.Errorf("parseActorID(%q) error = %v, wantErr %v", tc.host, err, tc.wantErr) + t.Errorf("parseActorRef(%q) error = %v, wantErr %v", tc.host, err, tc.wantErr) return } - if gotID != tc.wantID { - t.Errorf("parseActorID(%q) gotID = %v, want %v", tc.host, gotID, tc.wantID) + if gotAtespace != tc.wantAtespace || gotID != tc.wantID { + t.Errorf("parseActorRef(%q) = (%q, %q), want (%q, %q)", tc.host, gotAtespace, gotID, tc.wantAtespace, tc.wantID) } }) } diff --git a/cmd/atenet/internal/router/extproc_test.go b/cmd/atenet/internal/router/extproc_test.go index 005ef24d..304c570c 100644 --- a/cmd/atenet/internal/router/extproc_test.go +++ b/cmd/atenet/internal/router/extproc_test.go @@ -56,12 +56,12 @@ func TestExtProcHeadersEvaluation(t *testing.T) { name: "invalid host returns 404 identifying the host", authority: "invalid-host.com", expectErr: true, - expectedErrStr: `invalid host "invalid-host.com": invalid actor_id: must end with actors.resources.substrate.ate.dev, got "invalid-host.com"`, + expectedErrStr: `invalid host "invalid-host.com": invalid actor DNS name: must end with actors.resources.substrate.ate.dev, got "invalid-host.com"`, expectedStatus: envoy_type.StatusCode_NotFound, }, { name: "non-gRPC resume error collapses to 500 without leaking detail", - authority: testUUID + ".actors.resources.substrate.ate.dev", + authority: testUUID + ".team-a.actors.resources.substrate.ate.dev", resumeErr: errors.New("resume failed with sensitive detail"), expectErr: true, expectedErrStr: `error resuming actor "123e4567-e89b-12d3-a456-426614174000"`, @@ -69,7 +69,7 @@ func TestExtProcHeadersEvaluation(t *testing.T) { }, { name: "FailedPrecondition maps to 503 with preserved desc", - authority: testUUID + ".actors.resources.substrate.ate.dev", + authority: testUUID + ".team-a.actors.resources.substrate.ate.dev", resumeErr: status.Error(codes.FailedPrecondition, "no free workers available"), expectErr: true, expectedErrStr: `actor "123e4567-e89b-12d3-a456-426614174000" unavailable: no free workers available`, @@ -77,7 +77,7 @@ func TestExtProcHeadersEvaluation(t *testing.T) { }, { name: "NotFound maps to 404", - authority: testUUID + ".actors.resources.substrate.ate.dev", + authority: testUUID + ".team-a.actors.resources.substrate.ate.dev", resumeErr: status.Error(codes.NotFound, "actor missing"), expectErr: true, expectedErrStr: `actor "123e4567-e89b-12d3-a456-426614174000" not found`, @@ -85,7 +85,7 @@ func TestExtProcHeadersEvaluation(t *testing.T) { }, { name: "Unavailable maps to 503", - authority: testUUID + ".actors.resources.substrate.ate.dev", + authority: testUUID + ".team-a.actors.resources.substrate.ate.dev", resumeErr: status.Error(codes.Unavailable, "control-plane down"), expectErr: true, expectedErrStr: `actor "123e4567-e89b-12d3-a456-426614174000" unavailable`, @@ -93,7 +93,7 @@ func TestExtProcHeadersEvaluation(t *testing.T) { }, { name: "DeadlineExceeded maps to 504", - authority: testUUID + ".actors.resources.substrate.ate.dev", + authority: testUUID + ".team-a.actors.resources.substrate.ate.dev", resumeErr: status.Error(codes.DeadlineExceeded, "deadline"), expectErr: true, expectedErrStr: `actor "123e4567-e89b-12d3-a456-426614174000" request timed out`, @@ -101,7 +101,7 @@ func TestExtProcHeadersEvaluation(t *testing.T) { }, { name: "Bad Actor IP from resume returns 500 without leaking IP", - authority: testUUID + ".actors.resources.substrate.ate.dev", + authority: testUUID + ".team-a.actors.resources.substrate.ate.dev", resumeResp: &ateapipb.ResumeActorResponse{ Actor: &ateapipb.Actor{ AteomPodIp: "invalid-ip", @@ -113,7 +113,7 @@ func TestExtProcHeadersEvaluation(t *testing.T) { }, { name: "Successful resume", - authority: testUUID + ".actors.resources.substrate.ate.dev", + authority: testUUID + ".team-a.actors.resources.substrate.ate.dev", resumeResp: &ateapipb.ResumeActorResponse{ Actor: &ateapipb.Actor{ AteomPodIp: "10.0.0.52", diff --git a/cmd/atenet/internal/router/resumer.go b/cmd/atenet/internal/router/resumer.go index 2f9a420f..94ead000 100644 --- a/cmd/atenet/internal/router/resumer.go +++ b/cmd/atenet/internal/router/resumer.go @@ -38,9 +38,10 @@ func NewActorResumer(apiClient ateapipb.ControlClient) *ActorResumer { } // ResumeActor ensures the requested actor is running. It deduplicates concurrent -// requests within the process and retries when needed. -func (r *ActorResumer) ResumeActor(ctx context.Context, actorID string) (*ateapipb.Actor, error) { - ch := r.flight.DoChan(actorID, func() (interface{}, error) { +// requests within the process and retries when needed. The actor is addressed by +// (atespace, actorID) since an actor id is only unique within its atespace. +func (r *ActorResumer) ResumeActor(ctx context.Context, atespace, actorID string) (*ateapipb.Actor, error) { + ch := r.flight.DoChan(atespace+"/"+actorID, func() (interface{}, error) { // We detach the context from the first caller using a fixed background timeout. // This guarantees that if Caller 1 disconnects or times out, the underlying // resume operation continues running for Caller 2 and Caller 3 without failing. @@ -59,7 +60,8 @@ func (r *ActorResumer) ResumeActor(ctx context.Context, actorID string) (*ateapi err := wait.ExponentialBackoffWithContext(bgCtx, backoff, func(ctx context.Context) (bool, error) { var err error resumeResp, err = r.apiClient.ResumeActor(ctx, &ateapipb.ResumeActorRequest{ - ActorId: actorID, + ActorId: actorID, + Atespace: atespace, }) if err == nil { return true, nil diff --git a/cmd/atenet/internal/router/resumer_test.go b/cmd/atenet/internal/router/resumer_test.go index 8e10f7f5..061023d5 100644 --- a/cmd/atenet/internal/router/resumer_test.go +++ b/cmd/atenet/internal/router/resumer_test.go @@ -40,6 +40,7 @@ func (m *resumerMockClient) ResumeActor(ctx context.Context, in *ateapipb.Resume func TestActorResumer_ResumeActor(t *testing.T) { const testActorID = "actor-a" + const testAtespace = "team-a" const expectedIP = "10.0.0.52" t.Run("SuspendedResumedSuccessfully", func(t *testing.T) { @@ -58,7 +59,7 @@ func TestActorResumer_ResumeActor(t *testing.T) { } resumer := NewActorResumer(mock) - actor, err := resumer.ResumeActor(context.Background(), testActorID) + actor, err := resumer.ResumeActor(context.Background(), testAtespace, testActorID) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -89,7 +90,7 @@ func TestActorResumer_ResumeActor(t *testing.T) { } resumer := NewActorResumer(mock) - actor, err := resumer.ResumeActor(context.Background(), testActorID) + actor, err := resumer.ResumeActor(context.Background(), testAtespace, testActorID) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -109,7 +110,7 @@ func TestActorResumer_ResumeActor(t *testing.T) { } resumer := NewActorResumer(mock) - _, err := resumer.ResumeActor(context.Background(), testActorID) + _, err := resumer.ResumeActor(context.Background(), testAtespace, testActorID) if got := status.Code(err); got != codes.NotFound { t.Errorf("expected gRPC code NotFound, got %v (err=%v)", got, err) } @@ -146,7 +147,7 @@ func TestActorResumer_ResumeActor(t *testing.T) { for i := 0; i < concurrentRequests; i++ { go func(idx int) { defer wg.Done() - results[idx], errs[idx] = resumer.ResumeActor(context.Background(), testActorID) + results[idx], errs[idx] = resumer.ResumeActor(context.Background(), testAtespace, testActorID) }(i) } wg.Wait() diff --git a/demos/sandbox/client/main.go b/demos/sandbox/client/main.go index d2b2a539..6f97adc0 100644 --- a/demos/sandbox/client/main.go +++ b/demos/sandbox/client/main.go @@ -28,6 +28,7 @@ import ( "strings" "syscall" + "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/spf13/pflag" "google.golang.org/grpc" @@ -60,6 +61,7 @@ func dialAteAPI(endpoint string) (ateapipb.ControlClient, *grpc.ClientConn, erro func main() { actorID := pflag.String("id", "", "ID of the sandbox actor (required)") + atespace := pflag.String("atespace", "", "Atespace (tenant) the actor lives in (required)") ateapiAddr := pflag.String("ateapi", "localhost:8080", "Address of the ateapi gRPC server") atenetAddr := pflag.String("atenet", "localhost:8000", "Address of the atenet HTTP router") pflag.Parse() @@ -67,6 +69,9 @@ func main() { if *actorID == "" { log.Fatal("--id is required") } + if *atespace == "" { + log.Fatal("--atespace is required") + } ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -89,7 +94,7 @@ func main() { defer conn.Close() log.Printf("Resuming actor %s...", *actorID) - _, err = cli.ResumeActor(ctx, &ateapipb.ResumeActorRequest{ActorId: *actorID}) + _, err = cli.ResumeActor(ctx, &ateapipb.ResumeActorRequest{ActorId: *actorID, Atespace: *atespace}) if err != nil { log.Fatalf("Failed to resume actor: %v", err) } @@ -99,7 +104,7 @@ func main() { defer func() { log.Printf("Suspending actor %s...", *actorID) suspendCtx := context.Background() - _, err := cli.SuspendActor(suspendCtx, &ateapipb.SuspendActorRequest{ActorId: *actorID}) + _, err := cli.SuspendActor(suspendCtx, &ateapipb.SuspendActorRequest{ActorId: *actorID, Atespace: *atespace}) if err != nil { log.Printf("Failed to suspend actor: %v", err) } else { @@ -147,7 +152,7 @@ func main() { } // Send command to atenet router - output, err := runCommand(ctx, *atenetAddr, *actorID, line) + output, err := runCommand(ctx, *atenetAddr, *atespace, *actorID, line) if err != nil { fmt.Printf("Error: %v\n", err) continue @@ -166,7 +171,7 @@ func main() { } } -func runCommand(ctx context.Context, atenetAddr, actorID, command string) (*ProcessResponse, error) { +func runCommand(ctx context.Context, atenetAddr, atespace, actorID, command string) (*ProcessResponse, error) { url := fmt.Sprintf("http://%s/process", atenetAddr) reqBody := ProcessRequest{ @@ -179,7 +184,7 @@ func runCommand(ctx context.Context, atenetAddr, actorID, command string) (*Proc return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") - req.Host = fmt.Sprintf("%s.actors.resources.substrate.ate.dev", actorID) + req.Host = resources.ActorDNSName(atespace, actorID) resp, err := http.DefaultClient.Do(req) if err != nil { diff --git a/internal/e2e/router_client.go b/internal/e2e/router_client.go index 6896e5a6..48f1fcbb 100644 --- a/internal/e2e/router_client.go +++ b/internal/e2e/router_client.go @@ -193,14 +193,15 @@ func (c *RouterClient) Close() { close(c.stopCh) } -// Get issues GET path to actorID through the router, setting the actor's mesh -// Host so the router routes (and resumes) it. The caller must close the body. -func (c *RouterClient) Get(ctx context.Context, actorID, path string) (*http.Response, error) { +// Get issues GET path to (atespace, actorID) through the router, setting the +// actor's mesh Host so the router routes (and resumes) it. The caller must close +// the body. +func (c *RouterClient) Get(ctx context.Context, atespace, actorID, path string) (*http.Response, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil) if err != nil { return nil, err } // The router routes on the Host/:authority, not a header. - req.Host = fmt.Sprintf("%s.%s", actorID, resources.ActorDNSSuffix) + req.Host = resources.ActorDNSName(atespace, actorID) return c.http.Do(req) } diff --git a/internal/e2e/suites/demo/demo_test.go b/internal/e2e/suites/demo/demo_test.go index 26b8493e..bb881e1e 100644 --- a/internal/e2e/suites/demo/demo_test.go +++ b/internal/e2e/suites/demo/demo_test.go @@ -25,6 +25,7 @@ import ( "github.com/agent-substrate/substrate/internal/ateclient" "github.com/agent-substrate/substrate/internal/e2e" + "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/api/v1alpha1" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -158,7 +159,7 @@ func pauseActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj * } waitForActorStatus(ctx, t, clients, actorID, ateapipb.Actor_STATUS_RUNNING) - resp, err := callActor(t, actorID) + resp, err := callActor(t, demoAtespace, actorID) if err != nil { t.Fatalf("failed to call actor: %v", err) } @@ -186,7 +187,7 @@ func pauseActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj * } waitForActorStatus(ctx, t, clients, actorID, ateapipb.Actor_STATUS_RUNNING) - resp, err = callActor(t, actorID) + resp, err = callActor(t, demoAtespace, actorID) if err != nil { t.Fatalf("failed to call actor again: %v", err) } @@ -248,7 +249,7 @@ func suspendActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj } waitForActorStatus(ctx, t, clients, actorID, ateapipb.Actor_STATUS_RUNNING) - resp, err := callActor(t, actorID) + resp, err := callActor(t, demoAtespace, actorID) if err != nil { t.Fatalf("failed to call actor: %v", err) } @@ -276,7 +277,7 @@ func suspendActor(ctx context.Context, t *testing.T, clients *e2e.Clients, nsObj } waitForActorStatus(ctx, t, clients, actorID, ateapipb.Actor_STATUS_RUNNING) - resp, err = callActor(t, actorID) + resp, err = callActor(t, demoAtespace, actorID) if err != nil { t.Fatalf("failed to call actor again: %v", err) } @@ -423,7 +424,7 @@ func waitForActorStatus(ctx context.Context, t *testing.T, clients *e2e.Clients, t.Fatalf("timed out waiting for actor %q to reach status %v", actorID, expectedStatus) } -func callActor(t *testing.T, actorID string) (string, error) { +func callActor(t *testing.T, atespace, actorID string) (string, error) { t.Helper() clients := e2e.GetClients() @@ -494,7 +495,7 @@ func callActor(t *testing.T, actorID string) (string, error) { if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } - reqHttp.Host = fmt.Sprintf("%s.actors.resources.substrate.ate.dev", actorID) + reqHttp.Host = resources.ActorDNSName(atespace, actorID) httpClient := &http.Client{Timeout: 15 * time.Second} resp, err := httpClient.Do(reqHttp) diff --git a/internal/e2e/suites/identity/identity_test.go b/internal/e2e/suites/identity/identity_test.go index 2081bf99..99fb15ba 100644 --- a/internal/e2e/suites/identity/identity_test.go +++ b/internal/e2e/suites/identity/identity_test.go @@ -181,7 +181,7 @@ func createAndResumeActor(t *testing.T, ctx context.Context, clients *e2e.Client func whoami(t *testing.T, ctx context.Context, rc *e2e.RouterClient, id string) whoamiResponse { t.Helper() - resp, err := rc.Get(ctx, id, "/whoami") + resp, err := rc.Get(ctx, probeNamespace, id, "/whoami") if err != nil { t.Fatalf("GET /whoami for %q: %v", id, err) } diff --git a/internal/resources/actor.go b/internal/resources/actor.go index a077e094..53074cb7 100644 --- a/internal/resources/actor.go +++ b/internal/resources/actor.go @@ -17,13 +17,14 @@ package resources import ( "fmt" "regexp" + "strings" ) const ( // ActorIDRegexPattern is the regular expression pattern for matching valid actor IDs. ActorIDRegexPattern = `[a-z0-9]([-a-z0-9]*[a-z0-9])?` // ActorDNSSuffix is suffix to the DNS name for direct access to Actor - // ".actors.resources.substrate.ate.dev." + // "..actors.resources.substrate.ate.dev." ActorDNSSuffix = "actors.resources.substrate.ate.dev" ) @@ -57,3 +58,32 @@ func ValidateAtespace(atespace string) error { } return nil } + +// ActorDNSName returns the mesh DNS name an actor is reachable at: +// "..actors.resources.substrate.ate.dev". The atespace is +// part of the name because an actor id is only unique within its atespace. +func ActorDNSName(atespace, actorID string) string { + return actorID + "." + atespace + "." + ActorDNSSuffix +} + +// ParseActorDNSName parses a mesh DNS name of the form +// "..actors.resources.substrate.ate.dev" (a trailing dot is +// tolerated) into its atespace and actor id, validating both. It does not accept +// a host:port; callers must strip the port first. +func ParseActorDNSName(name string) (atespace, actorID string, err error) { + rest, found := strings.CutSuffix(strings.TrimSuffix(name, "."), "."+ActorDNSSuffix) + if !found { + return "", "", fmt.Errorf("invalid actor DNS name: must end with %s, got %q", ActorDNSSuffix, name) + } + actorID, atespace, found = strings.Cut(rest, ".") + if !found { + return "", "", fmt.Errorf("invalid actor DNS name: expected ..%s, got %q", ActorDNSSuffix, name) + } + if err := ValidateActorID(actorID); err != nil { + return "", "", err + } + if err := ValidateAtespace(atespace); err != nil { + return "", "", err + } + return atespace, actorID, nil +}