From 6d431ee7ac4248afdd843a67491a40ada9accb37 Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 13:49:06 -0700 Subject: [PATCH 1/9] 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 f9040f2d1..dcba4173a 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 20e80e5ca..774c45831 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 a6daa4e8e46ddd67a49cc2c8fbf2fd49dffbaf4d Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 13:49:06 -0700 Subject: [PATCH 2/9] 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 | 11 +- internal/e2e/suites/demo/demo_test.go | 49 +++- internal/e2e/suites/identity/identity_test.go | 7 +- internal/resources/actor.go | 15 + 21 files changed, 421 insertions(+), 154 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/create_actor.go b/cmd/ateapi/internal/controlapi/create_actor.go index 54e36521d..cb59b3188 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 db70a32f3..45a6ee0ac 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 13e0d1f59..b81018db5 100644 --- a/cmd/ateapi/internal/controlapi/functional_test.go +++ b/cmd/ateapi/internal/controlapi/functional_test.go @@ -61,6 +61,8 @@ var ( fakeAtelet = &FakeAteletServer{} ) +const testAtespace = "test-atespace" + func TestMain(m *testing.M) { binaryAssetsDirectory, err := envtestbins.BinaryAssetsDir() if err != nil { @@ -619,6 +621,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", @@ -636,6 +639,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, }, } @@ -651,6 +655,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", @@ -667,6 +672,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", @@ -676,6 +682,7 @@ func TestCreateActor_Duplicate(t *testing.T) { } _, err = tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + Atespace: testAtespace, ActorTemplateNamespace: ns, ActorTemplateName: "tmpl1", ActorId: "id1", @@ -692,6 +699,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", @@ -703,7 +711,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) @@ -728,7 +737,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") } @@ -747,6 +757,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", @@ -755,6 +766,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", @@ -763,7 +775,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) } @@ -789,6 +801,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") @@ -800,6 +869,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), @@ -815,6 +885,7 @@ func TestListActors_Pagination(t *testing.T) { for { listResp, err := tc.client.ListActors(context.Background(), &ateapipb.ListActorsRequest{ + Atespace: testAtespace, PageSize: 2, PageToken: pageToken, }) @@ -852,6 +923,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 { @@ -860,6 +932,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 { @@ -927,6 +1000,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", @@ -937,7 +1011,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) @@ -948,7 +1023,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) @@ -956,6 +1032,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, @@ -992,6 +1069,7 @@ func TestResumeActor(t *testing.T) { ActorNamespace: ns, ActorTemplate: "tmpl1", ActorId: id, + ActorAtespace: testAtespace, Ip: "127.0.0.1", NodeName: "node1", } @@ -1044,6 +1122,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", @@ -1052,7 +1131,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) @@ -1098,6 +1178,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", @@ -1109,7 +1190,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") } @@ -1127,7 +1209,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", @@ -1136,7 +1218,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's sandboxClass and the template/actor selectors") @@ -1159,7 +1241,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", @@ -1171,12 +1253,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) } @@ -1210,7 +1292,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", @@ -1222,11 +1304,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) } @@ -1256,6 +1338,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", @@ -1269,14 +1352,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) } @@ -1289,7 +1373,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) @@ -1300,7 +1385,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) } @@ -1329,6 +1414,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", @@ -1340,7 +1426,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) @@ -1348,7 +1435,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) @@ -1359,7 +1447,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) @@ -1367,6 +1456,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, @@ -1413,6 +1503,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", @@ -1424,7 +1515,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) @@ -1432,7 +1524,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) @@ -1443,7 +1536,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) @@ -1451,6 +1545,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, @@ -1487,7 +1582,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", @@ -1499,7 +1594,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"}, @@ -1512,6 +1607,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, @@ -1524,7 +1620,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) } @@ -1539,7 +1635,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") } @@ -1571,7 +1667,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, @@ -1582,24 +1678,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) } @@ -1659,7 +1755,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, @@ -1671,11 +1767,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) } @@ -1686,7 +1782,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"}, @@ -1695,14 +1791,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) } @@ -1735,37 +1831,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", }, } @@ -1783,7 +1879,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) { @@ -1799,7 +1895,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) { @@ -1815,7 +1911,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) { @@ -1831,20 +1927,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", }, } @@ -1862,9 +1958,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) { @@ -1885,6 +1981,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", @@ -1901,7 +1998,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 }() @@ -1911,7 +2009,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") @@ -1932,6 +2031,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", @@ -1946,7 +2046,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") @@ -1954,7 +2055,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) @@ -1978,7 +2080,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) @@ -1989,7 +2092,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) } @@ -2012,6 +2115,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", @@ -2023,7 +2127,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) @@ -2032,14 +2137,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) @@ -2047,7 +2153,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) @@ -2068,6 +2175,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", @@ -2077,14 +2185,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") } @@ -2098,6 +2208,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", @@ -2107,14 +2218,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)") } @@ -2125,7 +2238,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 d2b5cc637..ebac995b9 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 7f863990c..054812ea0 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 74f023298..f76bc4c79 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 671afb6ed..623a29efb 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 c8a88b505..531f2a2da 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 f0501387d..c16c8dd5e 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 c05b6f110..2672c11a8 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 f45e5a338..0aeb35cbd 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 5066a2c96..ff873a51a 100644 --- a/cmd/ateapi/internal/controlapi/workflow.go +++ b/cmd/ateapi/internal/controlapi/workflow.go @@ -148,10 +148,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{} @@ -178,9 +179,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{} @@ -207,9 +209,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 a7aaa41bc..49f9c4072 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 ddc877f9d..ba92bc9a7 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume.go @@ -41,8 +41,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. @@ -62,7 +63,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) @@ -183,6 +184,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 @@ -354,7 +356,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 1c0bcec2d..281e958a3 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 025516a50..743547c91 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. // @@ -86,8 +86,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 { @@ -179,8 +179,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 { @@ -195,15 +195,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. @@ -347,8 +347,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 { @@ -385,7 +385,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. @@ -412,6 +412,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") } @@ -510,7 +513,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) @@ -559,7 +562,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 779612e6c..31b980dc2 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) } @@ -348,12 +348,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) } @@ -375,7 +375,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) } @@ -385,7 +385,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) } @@ -477,7 +477,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) } @@ -582,7 +582,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) } @@ -612,7 +612,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) } @@ -833,3 +833,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 11ed7b11d..26dde9bda 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,11 @@ 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), or across ALL atespaces if atespace is + // empty. 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 06508fc7c..7467fad8d 100644 --- a/internal/e2e/suites/demo/demo_test.go +++ b/internal/e2e/suites/demo/demo_test.go @@ -34,6 +34,8 @@ import ( "k8s.io/client-go/transport/spdy" ) +const demoAtespace = "demo" + func TestActorLifecycle(t *testing.T) { // Create namespace nsObj := e2e.CreateNamespace(t) @@ -82,6 +84,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, @@ -92,11 +95,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) } @@ -136,6 +140,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, @@ -147,7 +152,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) } @@ -164,7 +170,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) } @@ -173,7 +180,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) } @@ -190,7 +198,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) } @@ -199,13 +208,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) } @@ -219,6 +230,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, @@ -230,7 +242,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) } @@ -247,7 +260,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) } @@ -256,7 +270,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) } @@ -273,7 +288,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) } @@ -282,13 +298,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) } @@ -417,7 +435,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 3b0d48790..2081bf998 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 5dc422128..ab570558a 100644 --- a/internal/resources/actor.go +++ b/internal/resources/actor.go @@ -29,6 +29,9 @@ const ( var actorIDRegex = regexp.MustCompile("^" + ActorIDRegexPattern + "$") +// TODO: unify actor/atespace validation across the control API RPCs — some only +// reject empty strings (get/pause/resume/suspend), others run the full validator. + // ValidateActorID validates whether the provided actor ID is valid or not. // Actor IDs must be valid DNS-1123 labels. // @@ -45,3 +48,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 358c9b6e445fe8417cec7d33e47575f801136bf9 Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 13:49:06 -0700 Subject: [PATCH 3/9] 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 cd3a4b6b3..057c33ac2 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 54a12df34..df3b610f1 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 3ec2ba760..121aa66fc 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 2b8187870..c70db355d 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 e7a7e75ff..697d1fe33 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 440643abf..51835ef17 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 c36506a9a..f1560a0b0 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 8877d4d8f8ec5a2cd4cfec7d6158a33536ab3e6e Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 21:26:46 -0700 Subject: [PATCH 4/9] 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 b81018db5..bfc069955 100644 --- a/cmd/ateapi/internal/controlapi/functional_test.go +++ b/cmd/ateapi/internal/controlapi/functional_test.go @@ -858,6 +858,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 054812ea0..4c09e8c16 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 743547c91..6b0dee8da 100644 --- a/cmd/ateapi/internal/store/ateredis/ateredis.go +++ b/cmd/ateapi/internal/store/ateredis/ateredis.go @@ -90,6 +90,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 } @@ -513,6 +523,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 { @@ -562,7 +575,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 31b980dc2..066d76aec 100644 --- a/cmd/ateapi/internal/store/ateredis/ateredis_test.go +++ b/cmd/ateapi/internal/store/ateredis/ateredis_test.go @@ -874,13 +874,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 b45679ae2..c1ff43734 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 121aa66fc..29551ec4e 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 12312e089..97f66e2cc 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 69f55bfc4..185b38fe9 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 274213bd40978b2591e37aeccf909dbbc0d16b96 Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 21:57:15 -0700 Subject: [PATCH 5/9] 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 c1ff43734..7ad875ba2 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 057c33ac2..9f9ea5d46 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 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 df3b610f1..27cfee82f 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 29551ec4e..91c4a60f3 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 c70db355d..d7ab65bf2 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 697d1fe33..cad22b3a4 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 51835ef17..5bd04b270 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 f1560a0b0..c2b534395 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 97f66e2cc..f6c5e3da4 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 185b38fe9..fd0e1c97d 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 351cd60d2d45542d9f24aec908da5af08ac35f87 Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 23:26:47 -0700 Subject: [PATCH 6/9] Wire atespace through atecontroller golden actors and atenet routing --- .../controllers/actortemplate_controller.go | 7 +- cmd/atenet/internal/dns/corefile.go | 9 +-- cmd/atenet/internal/dns/corefile_test.go | 2 +- cmd/atenet/internal/router/extproc.go | 7 +- cmd/atenet/internal/router/extproc_in.go | 25 +++---- cmd/atenet/internal/router/extproc_in_test.go | 66 +++++++++++-------- cmd/atenet/internal/router/extproc_test.go | 18 ++--- cmd/atenet/internal/router/resumer.go | 15 +++-- 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 ++++++++- internal/resources/actor_test.go | 47 +++++++++++++ 15 files changed, 187 insertions(+), 89 deletions(-) diff --git a/cmd/atecontroller/internal/controllers/actortemplate_controller.go b/cmd/atecontroller/internal/controllers/actortemplate_controller.go index 372890116..630820d06 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 7e243f93c..618248cf1 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)) + // Escape the suffix's dots so they match literally; the final \. matches the FQDN's trailing dot. + escapedSuffix := strings.ReplaceAll(resources.ActorDNSSuffix, ".", `\.`) + directives = append(directives, fmt.Sprintf(` match "^%s\.%s\.%s\.$"`, resources.ActorIDRegexPattern, resources.ActorIDRegexPattern, escapedSuffix)) // 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 df565ad06..a8cffcf27 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 3b3664774..ac5b5d5b8 100644 --- a/cmd/atenet/internal/router/extproc.go +++ b/cmd/atenet/internal/router/extproc.go @@ -142,14 +142,14 @@ func (s *ExtProcServer) handleRequestHeaders( ctx, span := otel.Tracer(routerServiceName).Start(ctx, "ExtProc.RequestHeaders") defer span.End() - 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) if err != nil { return nil, metadata, "", "", "", mapResumeError(actorID, err) } @@ -161,6 +161,7 @@ func (s *ExtProcServer) handleRequestHeaders( workerIP := actor.GetAteomPodIp() slog.InfoContext(ctx, "ResumeActor result", + slog.String("atespace", atespace), slog.String("actorID", actorID), slog.String("status", actor.GetStatus().String()), slog.String("workerIP", workerIP)) diff --git a/cmd/atenet/internal/router/extproc_in.go b/cmd/atenet/internal/router/extproc_in.go index ae3e1b4d2..aa15684d3 100644 --- a/cmd/atenet/internal/router/extproc_in.go +++ b/cmd/atenet/internal/router/extproc_in.go @@ -15,7 +15,6 @@ package router import ( - "fmt" "net" "strings" @@ -57,21 +56,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 901d98976..a23d47590 100644 --- a/cmd/atenet/internal/router/extproc_in_test.go +++ b/cmd/atenet/internal/router/extproc_in_test.go @@ -119,60 +119,68 @@ func TestExtractMetadata(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 52f345035..dc16ba169 100644 --- a/cmd/atenet/internal/router/extproc_test.go +++ b/cmd/atenet/internal/router/extproc_test.go @@ -61,7 +61,7 @@ func TestHandleRequestHeadersDoesNotLogSensitiveData(t *testing.T) { Headers: &corev3.HeaderMap{ Headers: []*corev3.HeaderValue{ {Key: ":path", Value: "/api/v1/reset?token=" + secret}, - {Key: ":authority", Value: testUUID + ".actors.resources.substrate.ate.dev"}, + {Key: ":authority", Value: testUUID + ".team-a.actors.resources.substrate.ate.dev"}, {Key: ":method", Value: "POST"}, {Key: "authorization", Value: "Bearer " + secret}, {Key: "cookie", Value: "session=" + secret}, @@ -107,12 +107,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"`, @@ -120,7 +120,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`, @@ -128,7 +128,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`, @@ -136,7 +136,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`, @@ -144,7 +144,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`, @@ -152,7 +152,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", @@ -164,7 +164,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 5ed2ad040..d3b415f7c 100644 --- a/cmd/atenet/internal/router/resumer.go +++ b/cmd/atenet/internal/router/resumer.go @@ -41,13 +41,17 @@ 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) { +// 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) { ctx, span := otel.Tracer(routerServiceName).Start(ctx, "ResumeActor", - trace.WithAttributes(attribute.String("actor_id", actorID))) + trace.WithAttributes( + attribute.String("atespace", atespace), + attribute.String("actor_id", actorID), + )) defer span.End() - ch := r.flight.DoChan(actorID, func() (interface{}, 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. @@ -66,7 +70,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 8e10f7f57..061023d58 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 d2b2a5391..6f97adc02 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 6896e5a64..48f1fcbb9 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 7467fad8d..9df4a09cc 100644 --- a/internal/e2e/suites/demo/demo_test.go +++ b/internal/e2e/suites/demo/demo_test.go @@ -26,6 +26,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" @@ -159,7 +160,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) } @@ -187,7 +188,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) } @@ -249,7 +250,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) } @@ -277,7 +278,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) } @@ -449,7 +450,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() @@ -520,7 +521,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 2081bf998..99fb15ba1 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 ab570558a..29a708ed9 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" ) @@ -60,3 +61,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 +} diff --git a/internal/resources/actor_test.go b/internal/resources/actor_test.go index c4e5b95b8..61d6776b9 100644 --- a/internal/resources/actor_test.go +++ b/internal/resources/actor_test.go @@ -43,3 +43,50 @@ func TestValidateActorID(t *testing.T) { }) } } + +func TestActorDNSName(t *testing.T) { + got := ActorDNSName("team-a", "act-1") + want := "act-1.team-a." + ActorDNSSuffix + if got != want { + t.Errorf("ActorDNSName() = %q, want %q", got, want) + } + + // Round-trips through ParseActorDNSName. + atespace, actorID, err := ParseActorDNSName(got) + if err != nil || atespace != "team-a" || actorID != "act-1" { + t.Errorf("round-trip = (%q, %q, %v), want (team-a, act-1, )", atespace, actorID, err) + } +} + +func TestParseActorDNSName(t *testing.T) { + tests := []struct { + name string + input string + wantAtespace string + wantActorID string + wantErr bool + }{ + {"valid", "act-1.team-a." + ActorDNSSuffix, "team-a", "act-1", false}, + {"valid trailing dot", "act-1.team-a." + ActorDNSSuffix + ".", "team-a", "act-1", false}, + {"wrong suffix", "act-1.team-a.example.com", "", "", true}, + {"missing atespace", "act-1." + ActorDNSSuffix, "", "", true}, + {"invalid actor id", "ACT-1.team-a." + ActorDNSSuffix, "", "", true}, + {"invalid atespace", "act-1.TEAM." + ActorDNSSuffix, "", "", true}, + {"host:port not accepted", "act-1.team-a." + ActorDNSSuffix + ":8080", "", "", true}, + {"empty", "", "", "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + atespace, actorID, err := ParseActorDNSName(tt.input) + if (err != nil) != tt.wantErr { + t.Fatalf("ParseActorDNSName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + } + if err != nil { + return + } + if atespace != tt.wantAtespace || actorID != tt.wantActorID { + t.Errorf("ParseActorDNSName(%q) = (%q, %q), want (%q, %q)", tt.input, atespace, actorID, tt.wantAtespace, tt.wantActorID) + } + }) + } +} From 253f711f1ceec010f9cc6d8d855dad7fd6111d64 Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 18 Jun 2026 23:26:47 -0700 Subject: [PATCH 7/9] Add Atespace object and CRUD API; require it on CreateActor --- .../internal/controlapi/create_actor.go | 9 + .../internal/controlapi/create_atespace.go | 53 ++ .../internal/controlapi/delete_atespace.go | 55 ++ .../internal/controlapi/functional_test.go | 243 +++++++ .../internal/controlapi/get_atespace.go | 52 ++ .../internal/controlapi/list_atespaces.go | 30 + .../internal/store/ateredis/ateredis.go | 110 +++ .../internal/store/ateredis/ateredis_test.go | 190 +++++ cmd/ateapi/internal/store/store.go | 16 + .../controllers/actortemplate_controller.go | 10 +- cmd/kubectl-ate/README.md | 32 +- .../internal/cmd/create_atespace.go | 49 ++ .../internal/cmd/delete_atespace.go | 49 ++ cmd/kubectl-ate/internal/cmd/get_atespaces.go | 56 ++ cmd/kubectl-ate/internal/printer/printer.go | 34 + .../internal/printer/printer_test.go | 89 +++ internal/e2e/suites/demo/demo_test.go | 3 + internal/e2e/suites/identity/identity_test.go | 2 + pkg/proto/ateapipb/ateapi.pb.go | 684 ++++++++++++++---- pkg/proto/ateapipb/ateapi.proto | 39 +- pkg/proto/ateapipb/ateapi_grpc.pb.go | 180 ++++- 21 files changed, 1811 insertions(+), 174 deletions(-) create mode 100644 cmd/ateapi/internal/controlapi/create_atespace.go create mode 100644 cmd/ateapi/internal/controlapi/delete_atespace.go create mode 100644 cmd/ateapi/internal/controlapi/get_atespace.go create mode 100644 cmd/ateapi/internal/controlapi/list_atespaces.go create mode 100644 cmd/kubectl-ate/internal/cmd/create_atespace.go create mode 100644 cmd/kubectl-ate/internal/cmd/delete_atespace.go create mode 100644 cmd/kubectl-ate/internal/cmd/get_atespaces.go diff --git a/cmd/ateapi/internal/controlapi/create_actor.go b/cmd/ateapi/internal/controlapi/create_actor.go index cb59b3188..f9daa24f1 100644 --- a/cmd/ateapi/internal/controlapi/create_actor.go +++ b/cmd/ateapi/internal/controlapi/create_actor.go @@ -41,6 +41,15 @@ func (s *Service) CreateActor(ctx context.Context, req *ateapipb.CreateActorRequ return nil, fmt.Errorf("while getting ActorTemplate: %w", err) } + // The atespace must already exist. + exists, err := s.persistence.AtespaceExists(ctx, req.GetAtespace()) + if err != nil { + return nil, fmt.Errorf("while checking atespace: %w", err) + } + if !exists { + return nil, status.Errorf(codes.FailedPrecondition, "Atespace %s not found", req.GetAtespace()) + } + id := req.GetActorId() actor := &ateapipb.Actor{ ActorId: id, diff --git a/cmd/ateapi/internal/controlapi/create_atespace.go b/cmd/ateapi/internal/controlapi/create_atespace.go new file mode 100644 index 000000000..6522c6084 --- /dev/null +++ b/cmd/ateapi/internal/controlapi/create_atespace.go @@ -0,0 +1,53 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controlapi + +import ( + "context" + "errors" + "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" +) + +func (s *Service) CreateAtespace(ctx context.Context, req *ateapipb.CreateAtespaceRequest) (*ateapipb.CreateAtespaceResponse, error) { + if err := validateCreateAtespaceRequest(req); err != nil { + return nil, err + } + + atespace := &ateapipb.Atespace{Name: req.GetName()} + if err := s.persistence.CreateAtespace(ctx, atespace); err != nil { + if errors.Is(err, store.ErrAlreadyExists) { + return nil, status.Errorf(codes.AlreadyExists, "Atespace %s already exists", req.GetName()) + } + return nil, fmt.Errorf("while recording atespace: %w", err) + } + + return &ateapipb.CreateAtespaceResponse{Atespace: atespace}, nil +} + +func validateCreateAtespaceRequest(req *ateapipb.CreateAtespaceRequest) error { + if req.GetName() == "" { + return status.Error(codes.InvalidArgument, "name is required") + } + if err := resources.ValidateAtespace(req.GetName()); err != nil { + return status.Error(codes.InvalidArgument, err.Error()) + } + return nil +} diff --git a/cmd/ateapi/internal/controlapi/delete_atespace.go b/cmd/ateapi/internal/controlapi/delete_atespace.go new file mode 100644 index 000000000..736690734 --- /dev/null +++ b/cmd/ateapi/internal/controlapi/delete_atespace.go @@ -0,0 +1,55 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controlapi + +import ( + "context" + "errors" + "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" +) + +func (s *Service) DeleteAtespace(ctx context.Context, req *ateapipb.DeleteAtespaceRequest) (*ateapipb.DeleteAtespaceResponse, error) { + if err := validateDeleteAtespaceRequest(req); err != nil { + return nil, err + } + + if err := s.persistence.DeleteAtespace(ctx, req.GetName()); err != nil { + if errors.Is(err, store.ErrNotFound) { + return nil, status.Errorf(codes.NotFound, "Atespace %s not found", req.GetName()) + } + if errors.Is(err, store.ErrFailedPrecondition) { + return nil, status.Errorf(codes.FailedPrecondition, "Atespace %s is not empty", req.GetName()) + } + return nil, fmt.Errorf("while deleting atespace from DB: %w", err) + } + + return &ateapipb.DeleteAtespaceResponse{}, nil +} + +func validateDeleteAtespaceRequest(req *ateapipb.DeleteAtespaceRequest) error { + if req.GetName() == "" { + return status.Error(codes.InvalidArgument, "name is required") + } + if err := resources.ValidateAtespace(req.GetName()); err != nil { + return status.Error(codes.InvalidArgument, err.Error()) + } + return nil +} diff --git a/cmd/ateapi/internal/controlapi/functional_test.go b/cmd/ateapi/internal/controlapi/functional_test.go index bfc069955..3ec58283e 100644 --- a/cmd/ateapi/internal/controlapi/functional_test.go +++ b/cmd/ateapi/internal/controlapi/functional_test.go @@ -345,6 +345,15 @@ func setupTest(t *testing.T, ns string) *testContext { t.Fatalf("failed to create namespace %s: %v", ns, err) } + // CreateActor now requires the atespace to exist first. + if _, err := client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Name: testAtespace}); err != nil { + conn.Close() + grpcServer.Stop() + cancel() + mr.Close() + t.Fatalf("failed to seed test atespace %q: %v", testAtespace, err) + } + cleanup := func() { conn.Close() grpcServer.Stop() @@ -390,6 +399,14 @@ func createTemplate(t *testing.T, tc *testContext, ns string) { }) } +// createAtespace creates an atespace via the API. +func createAtespace(t *testing.T, tc *testContext, name string) { + t.Helper() + if _, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Name: name}); err != nil { + t.Fatalf("CreateAtespace(%s) failed: %v", name, err) + } +} + const poolLabelKey = "pool" func createTemplateWithContainers(t *testing.T, tc *testContext, ns string, containers []atev1alpha1.Container) { @@ -810,6 +827,8 @@ func TestListActors_ByAtespace(t *testing.T) { defer tc.cleanup() createTemplate(t, tc, ns) + createAtespace(t, tc, "team-a") + createAtespace(t, tc, "team-b") create := func(id, atespace string) *ateapipb.Actor { resp, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ @@ -866,6 +885,8 @@ func TestListActors_AllAtespaces(t *testing.T) { defer tc.cleanup() createTemplate(t, tc, ns) + createAtespace(t, tc, "team-a") + createAtespace(t, tc, "team-b") create := func(id, atespace string) { if _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ @@ -2299,3 +2320,225 @@ func assertGrpcError(t *testing.T, err error, wantCode codes.Code, wantMsg strin t.Errorf("expected message %q, got %q", wantMsg, st.Message()) } } + +func TestCreateActor_AtespaceNotFound(t *testing.T) { + ns := namespaceForTest("ns-create-actor-no-atespace") + tc := setupTest(t, ns) + defer tc.cleanup() + createTemplate(t, tc, ns) + + // The template exists, but "missing-as" was never created. The template + // check fires first, so reaching this error proves the atespace check ran. + _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + ActorTemplateNamespace: ns, + ActorTemplateName: "tmpl1", + ActorId: "id1", + Atespace: "missing-as", + }) + assertGrpcError(t, err, codes.FailedPrecondition, "Atespace missing-as not found") +} + +func TestCreateAtespace_Success(t *testing.T) { + ns := namespaceForTest("ns-create-atespace") + tc := setupTest(t, ns) + defer tc.cleanup() + createTemplate(t, tc, ns) + + resp, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Name: "team-a"}) + if err != nil { + t.Fatalf("CreateAtespace failed: %v", err) + } + got := resp.GetAtespace() + if got.GetName() != "team-a" { + t.Errorf("Name = %q, want team-a", got.GetName()) + } + + // An actor can now be created into the new atespace. + if _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + ActorTemplateNamespace: ns, + ActorTemplateName: "tmpl1", + ActorId: "id1", + Atespace: "team-a", + }); err != nil { + t.Errorf("CreateActor into freshly created atespace failed: %v", err) + } +} + +func TestCreateAtespace_AlreadyExists(t *testing.T) { + ns := namespaceForTest("ns-create-atespace-dup") + tc := setupTest(t, ns) + defer tc.cleanup() + + if _, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Name: "team-a"}); err != nil { + t.Fatalf("first CreateAtespace failed: %v", err) + } + _, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Name: "team-a"}) + assertGrpcError(t, err, codes.AlreadyExists, "Atespace team-a already exists") +} + +func TestCreateAtespace_Validation(t *testing.T) { + ns := namespaceForTest("ns-create-atespace-validation") + tc := setupTest(t, ns) + defer tc.cleanup() + + _, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Name: ""}) + assertGrpcError(t, err, codes.InvalidArgument, "name is required") + + // Invalid names — uppercase/underscore plus Redis-key/SCAN metacharacters — + // are rejected by ValidateAtespace before any key is built (injection guard). + for _, bad := range []string{"Team_A", "a*", "a:b", "a/b"} { + _, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Name: bad}) + if status.Code(err) != codes.InvalidArgument { + t.Errorf("CreateAtespace(%q): got code %v, want InvalidArgument (err=%v)", bad, status.Code(err), err) + } + } +} + +func TestGetAtespace_Found(t *testing.T) { + ns := namespaceForTest("ns-get-atespace") + tc := setupTest(t, ns) + defer tc.cleanup() + + created, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Name: "team-a"}) + if err != nil { + t.Fatalf("CreateAtespace failed: %v", err) + } + resp, err := tc.client.GetAtespace(context.Background(), &ateapipb.GetAtespaceRequest{Name: "team-a"}) + if err != nil { + t.Fatalf("GetAtespace failed: %v", err) + } + if diff := cmp.Diff(created.GetAtespace(), resp.GetAtespace(), protocmp.Transform()); diff != "" { + t.Errorf("GetAtespace mismatch (-created +got):\n%s", diff) + } +} + +func TestGetAtespace_NotFound(t *testing.T) { + ns := namespaceForTest("ns-get-atespace-missing") + tc := setupTest(t, ns) + defer tc.cleanup() + + _, err := tc.client.GetAtespace(context.Background(), &ateapipb.GetAtespaceRequest{Name: "nope"}) + assertGrpcError(t, err, codes.NotFound, "Atespace nope not found") +} + +func TestListAtespaces(t *testing.T) { + ns := namespaceForTest("ns-list-atespaces") + tc := setupTest(t, ns) + defer tc.cleanup() + + for _, n := range []string{"team-a", "team-b"} { + if _, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Name: n}); err != nil { + t.Fatalf("CreateAtespace(%s) failed: %v", n, err) + } + } + resp, err := tc.client.ListAtespaces(context.Background(), &ateapipb.ListAtespacesRequest{}) + if err != nil { + t.Fatalf("ListAtespaces failed: %v", err) + } + got := map[string]bool{} + for _, a := range resp.GetAtespaces() { + got[a.GetName()] = true + } + // setupTest seeds testAtespace; team-a and team-b were created above. + for _, n := range []string{testAtespace, "team-a", "team-b"} { + if !got[n] { + t.Errorf("ListAtespaces missing %q; got %v", n, got) + } + } +} + +func TestDeleteAtespace_Empty_Success(t *testing.T) { + ns := namespaceForTest("ns-delete-atespace-empty") + tc := setupTest(t, ns) + defer tc.cleanup() + + if _, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Name: "team-a"}); err != nil { + t.Fatalf("CreateAtespace failed: %v", err) + } + if _, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{Name: "team-a"}); err != nil { + t.Fatalf("DeleteAtespace failed: %v", err) + } + _, err := tc.client.GetAtespace(context.Background(), &ateapipb.GetAtespaceRequest{Name: "team-a"}) + assertGrpcError(t, err, codes.NotFound, "Atespace team-a not found") +} + +func TestDeleteAtespace_NonEmpty_Rejected(t *testing.T) { + ns := namespaceForTest("ns-delete-atespace-nonempty") + tc := setupTest(t, ns) + defer tc.cleanup() + createTemplate(t, tc, ns) + + if _, err := tc.client.CreateAtespace(context.Background(), &ateapipb.CreateAtespaceRequest{Name: "team-a"}); err != nil { + t.Fatalf("CreateAtespace failed: %v", err) + } + if _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + ActorTemplateNamespace: ns, + ActorTemplateName: "tmpl1", + ActorId: "id1", + Atespace: "team-a", + }); err != nil { + t.Fatalf("CreateActor failed: %v", err) + } + _, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{Name: "team-a"}) + assertGrpcError(t, err, codes.FailedPrecondition, "Atespace team-a is not empty") + // The atespace must survive a rejected delete. + if _, err := tc.client.GetAtespace(context.Background(), &ateapipb.GetAtespaceRequest{Name: "team-a"}); err != nil { + t.Errorf("atespace should survive a rejected delete, got %v", err) + } +} + +// TestDeleteAtespace_ScopedToTargetAtespace pins (at the RPC layer) that the +// emptiness check is scoped to the target atespace: deleting an empty atespace +// succeeds even when a different atespace holds actors. +func TestDeleteAtespace_ScopedToTargetAtespace(t *testing.T) { + ns := namespaceForTest("ns-delete-atespace-scoped") + tc := setupTest(t, ns) + defer tc.cleanup() + createTemplate(t, tc, ns) + createAtespace(t, tc, "team-a") + createAtespace(t, tc, "team-b") + + // Actor only in team-b. + if _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{ + ActorTemplateNamespace: ns, + ActorTemplateName: "tmpl1", + ActorId: "id1", + Atespace: "team-b", + }); err != nil { + t.Fatalf("CreateActor failed: %v", err) + } + + // Empty team-a deletes fine despite team-b holding an actor. + if _, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{Name: "team-a"}); err != nil { + t.Errorf("DeleteAtespace(team-a, empty) failed: %v", err) + } + // team-b is still non-empty → rejected. + _, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{Name: "team-b"}) + assertGrpcError(t, err, codes.FailedPrecondition, "Atespace team-b is not empty") +} + +func TestDeleteAtespace_NotFound(t *testing.T) { + ns := namespaceForTest("ns-delete-atespace-missing") + tc := setupTest(t, ns) + defer tc.cleanup() + + _, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{Name: "nope"}) + assertGrpcError(t, err, codes.NotFound, "Atespace nope not found") +} + +func TestDeleteAtespace_Validation(t *testing.T) { + ns := namespaceForTest("ns-delete-atespace-validation") + tc := setupTest(t, ns) + defer tc.cleanup() + + _, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{Name: ""}) + assertGrpcError(t, err, codes.InvalidArgument, "name is required") + + // Metacharacter names are rejected before the emptiness glob scan ever runs. + for _, bad := range []string{"a*", "a:b"} { + _, err := tc.client.DeleteAtespace(context.Background(), &ateapipb.DeleteAtespaceRequest{Name: bad}) + if status.Code(err) != codes.InvalidArgument { + t.Errorf("DeleteAtespace(%q): got code %v, want InvalidArgument", bad, status.Code(err)) + } + } +} diff --git a/cmd/ateapi/internal/controlapi/get_atespace.go b/cmd/ateapi/internal/controlapi/get_atespace.go new file mode 100644 index 000000000..8d6c52508 --- /dev/null +++ b/cmd/ateapi/internal/controlapi/get_atespace.go @@ -0,0 +1,52 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controlapi + +import ( + "context" + "errors" + "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" +) + +func (s *Service) GetAtespace(ctx context.Context, req *ateapipb.GetAtespaceRequest) (*ateapipb.GetAtespaceResponse, error) { + if err := validateGetAtespaceRequest(req); err != nil { + return nil, err + } + + atespace, err := s.persistence.GetAtespace(ctx, req.GetName()) + if errors.Is(err, store.ErrNotFound) { + return nil, status.Errorf(codes.NotFound, "Atespace %s not found", req.GetName()) + } else if err != nil { + return nil, fmt.Errorf("while getting atespace from DB: %w", err) + } + + return &ateapipb.GetAtespaceResponse{Atespace: atespace}, nil +} + +func validateGetAtespaceRequest(req *ateapipb.GetAtespaceRequest) error { + if req.GetName() == "" { + return status.Error(codes.InvalidArgument, "name is required") + } + if err := resources.ValidateAtespace(req.GetName()); err != nil { + return status.Error(codes.InvalidArgument, err.Error()) + } + return nil +} diff --git a/cmd/ateapi/internal/controlapi/list_atespaces.go b/cmd/ateapi/internal/controlapi/list_atespaces.go new file mode 100644 index 000000000..27b15e31e --- /dev/null +++ b/cmd/ateapi/internal/controlapi/list_atespaces.go @@ -0,0 +1,30 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controlapi + +import ( + "context" + "fmt" + + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" +) + +func (s *Service) ListAtespaces(ctx context.Context, req *ateapipb.ListAtespacesRequest) (*ateapipb.ListAtespacesResponse, error) { + atespaces, err := s.persistence.ListAtespaces(ctx) + if err != nil { + return nil, fmt.Errorf("while listing atespaces in db: %w", err) + } + return &ateapipb.ListAtespacesResponse{Atespaces: atespaces}, nil +} diff --git a/cmd/ateapi/internal/store/ateredis/ateredis.go b/cmd/ateapi/internal/store/ateredis/ateredis.go index 6b0dee8da..058a27785 100644 --- a/cmd/ateapi/internal/store/ateredis/ateredis.go +++ b/cmd/ateapi/internal/store/ateredis/ateredis.go @@ -100,6 +100,116 @@ func actorScanPattern(atespace string) string { return "actor:" + atespace + ":*" } +func atespaceDBKey(name string) string { + return "atespace:" + name +} + +func (s *Persistence) CreateAtespace(ctx context.Context, atespace *ateapipb.Atespace) error { + dbKey := atespaceDBKey(atespace.GetName()) + dbBytes, err := protojson.Marshal(atespace) + if err != nil { + return fmt.Errorf("in protojson.Marshal: %w", err) + } + ok, err := s.rdb.SetNX(ctx, dbKey, dbBytes, 0).Result() + if err != nil { + return fmt.Errorf("while executing redis set: %w", err) + } + if !ok { + return store.ErrAlreadyExists + } + return nil +} + +func (s *Persistence) GetAtespace(ctx context.Context, name string) (*ateapipb.Atespace, error) { + dbKey := atespaceDBKey(name) + dbBytes, err := s.rdb.Get(ctx, dbKey).Bytes() + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, store.ErrNotFound + } + return nil, fmt.Errorf("while getting atespace key %q: %w", dbKey, err) + } + atespace := &ateapipb.Atespace{} + if err := protojson.Unmarshal(dbBytes, atespace); err != nil { + return nil, fmt.Errorf("while unmarshaling atespace: %w", err) + } + if atespace.GetName() != name { + return nil, fmt.Errorf("(impossible) mismatch between stored name and key %q", dbKey) + } + return atespace, nil +} + +// AtespaceExists reports whether the atespace object exists. This is a plain +// EXISTS check and is NOT atomic with respect to a concurrent DeleteAtespace. +func (s *Persistence) AtespaceExists(ctx context.Context, name string) (bool, error) { + n, err := s.rdb.Exists(ctx, atespaceDBKey(name)).Result() + if err != nil { + return false, fmt.Errorf("while checking atespace existence: %w", err) + } + return n > 0, nil +} + +func (s *Persistence) ListAtespaces(ctx context.Context) ([]*ateapipb.Atespace, error) { + var result []*ateapipb.Atespace + var mu sync.Mutex + + err := s.rdb.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error { + iter := master.Scan(ctx, 0, "atespace:*", 0).Iterator() + for iter.Next(ctx) { + key := iter.Val() + getCmd := master.Get(ctx, key) + if getCmd.Err() != nil { + return fmt.Errorf("while getting atespace %q: %w", key, getCmd.Err()) + } + atespace := &ateapipb.Atespace{} + if err := protojson.Unmarshal([]byte(getCmd.Val()), atespace); err != nil { + return fmt.Errorf("in protojson.Unmarshal: %w", err) + } + mu.Lock() + result = append(result, atespace) + mu.Unlock() + } + if err := iter.Err(); err != nil { + return fmt.Errorf("error from iterator: %w", err) + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("while iterating all redis master: %w", err) + } + return result, nil +} + +// DeleteAtespace deletes an empty atespace. Returns store.ErrNotFound if the +// atespace does not exist, or store.ErrFailedPrecondition if any actor still +// lives in it. +func (s *Persistence) DeleteAtespace(ctx context.Context, name string) error { + dbKey := atespaceDBKey(name) + + // Existence first, so a missing atespace returns NotFound, not a silent no-op. + exists, err := s.rdb.Exists(ctx, dbKey).Result() + if err != nil { + return fmt.Errorf("while checking atespace key %q: %w", dbKey, err) + } + if exists == 0 { + return store.ErrNotFound + } + + // Reject a non-empty atespace. + actors, _, err := s.ListActors(ctx, name, 1, "") + if err != nil { + return fmt.Errorf("while checking atespace emptiness: %w", err) + } + if len(actors) > 0 { + return store.ErrFailedPrecondition + } + + if err := s.rdb.Del(ctx, dbKey).Err(); err != nil { + return fmt.Errorf("while deleting atespace key %q: %w", dbKey, err) + } + return nil +} + func workerDBKey(namespace, poolName, podName string) string { return "worker:" + namespace + ":" + poolName + ":" + podName } diff --git a/cmd/ateapi/internal/store/ateredis/ateredis_test.go b/cmd/ateapi/internal/store/ateredis/ateredis_test.go index 066d76aec..6b3c61837 100644 --- a/cmd/ateapi/internal/store/ateredis/ateredis_test.go +++ b/cmd/ateapi/internal/store/ateredis/ateredis_test.go @@ -902,3 +902,193 @@ func actorIDSet(actors []*ateapipb.Actor) map[string]bool { } return set } + +func newTestAtespace(name string) *ateapipb.Atespace { + return &ateapipb.Atespace{Name: name} +} + +func TestCreateAtespace_Success(t *testing.T) { + mr, s, ctx := setupTest(t) + defer mr.Close() + + want := newTestAtespace("team-a") + if err := s.CreateAtespace(ctx, want); err != nil { + t.Fatalf("CreateAtespace failed: %v", err) + } + got, err := s.GetAtespace(ctx, "team-a") + if err != nil { + t.Fatalf("GetAtespace failed: %v", err) + } + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("round-trip mismatch (-want +got):\n%s", diff) + } +} + +func TestCreateAtespace_AlreadyExists(t *testing.T) { + mr, s, ctx := setupTest(t) + defer mr.Close() + + if err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { + t.Fatalf("first CreateAtespace failed: %v", err) + } + if err := s.CreateAtespace(ctx, newTestAtespace("team-a")); !errors.Is(err, store.ErrAlreadyExists) { + t.Errorf("expected ErrAlreadyExists, got %v", err) + } +} + +func TestGetAtespace_NotFound(t *testing.T) { + mr, s, ctx := setupTest(t) + defer mr.Close() + + if _, err := s.GetAtespace(ctx, "nope"); !errors.Is(err, store.ErrNotFound) { + t.Errorf("expected ErrNotFound, got %v", err) + } +} + +func TestAtespaceExists(t *testing.T) { + mr, s, ctx := setupTest(t) + defer mr.Close() + + if ok, err := s.AtespaceExists(ctx, "team-a"); err != nil || ok { + t.Fatalf("AtespaceExists before create = (%v, %v), want (false, nil)", ok, err) + } + if err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { + t.Fatalf("CreateAtespace failed: %v", err) + } + if ok, err := s.AtespaceExists(ctx, "team-a"); err != nil || !ok { + t.Fatalf("AtespaceExists after create = (%v, %v), want (true, nil)", ok, err) + } +} + +func TestListAtespaces(t *testing.T) { + mr, s, ctx := setupTest(t) + defer mr.Close() + + names := []string{"team-a", "team-b", "team-c"} + for _, n := range names { + if err := s.CreateAtespace(ctx, newTestAtespace(n)); err != nil { + t.Fatalf("CreateAtespace(%s) failed: %v", n, err) + } + } + got, err := s.ListAtespaces(ctx) + if err != nil { + t.Fatalf("ListAtespaces failed: %v", err) + } + if len(got) != len(names) { + t.Fatalf("ListAtespaces returned %d atespaces, want %d", len(got), len(names)) + } + gotNames := map[string]bool{} + for _, a := range got { + gotNames[a.GetName()] = true + } + for _, n := range names { + if !gotNames[n] { + t.Errorf("ListAtespaces missing %q; got %v", n, gotNames) + } + } +} + +func TestListAtespaces_Empty(t *testing.T) { + mr, s, ctx := setupTest(t) + defer mr.Close() + + got, err := s.ListAtespaces(ctx) + if err != nil { + t.Fatalf("ListAtespaces failed: %v", err) + } + if len(got) != 0 { + t.Errorf("ListAtespaces on empty store = %v, want empty", got) + } +} + +func TestDeleteAtespace_Empty(t *testing.T) { + mr, s, ctx := setupTest(t) + defer mr.Close() + + if err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { + t.Fatalf("CreateAtespace failed: %v", err) + } + if err := s.DeleteAtespace(ctx, "team-a"); err != nil { + t.Fatalf("DeleteAtespace failed: %v", err) + } + if _, err := s.GetAtespace(ctx, "team-a"); !errors.Is(err, store.ErrNotFound) { + t.Errorf("after delete, GetAtespace = %v, want ErrNotFound", err) + } +} + +func TestDeleteAtespace_NotFound(t *testing.T) { + mr, s, ctx := setupTest(t) + defer mr.Close() + + if err := s.DeleteAtespace(ctx, "nope"); !errors.Is(err, store.ErrNotFound) { + t.Errorf("expected ErrNotFound, got %v", err) + } +} + +func TestDeleteAtespace_NonEmpty_Rejected(t *testing.T) { + mr, s, ctx := setupTest(t) + defer mr.Close() + + if err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { + t.Fatalf("CreateAtespace failed: %v", err) + } + if err := s.CreateActor(ctx, &ateapipb.Actor{ActorId: "id1", Atespace: "team-a", Status: ateapipb.Actor_STATUS_SUSPENDED}); err != nil { + t.Fatalf("CreateActor failed: %v", err) + } + if err := s.DeleteAtespace(ctx, "team-a"); !errors.Is(err, store.ErrFailedPrecondition) { + t.Errorf("DeleteAtespace on non-empty = %v, want ErrFailedPrecondition", err) + } + // The atespace must survive a rejected delete. + if _, err := s.GetAtespace(ctx, "team-a"); err != nil { + t.Errorf("atespace should still exist after rejected delete, got %v", err) + } +} + +func TestDeleteAtespace_EmptyAfterActorsRemoved(t *testing.T) { + mr, s, ctx := setupTest(t) + defer mr.Close() + + if err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { + t.Fatalf("CreateAtespace failed: %v", err) + } + if err := s.CreateActor(ctx, &ateapipb.Actor{ActorId: "id1", Atespace: "team-a", Status: ateapipb.Actor_STATUS_SUSPENDED}); err != nil { + t.Fatalf("CreateActor failed: %v", err) + } + if err := s.DeleteAtespace(ctx, "team-a"); !errors.Is(err, store.ErrFailedPrecondition) { + t.Fatalf("expected rejection while non-empty, got %v", err) + } + if err := s.DeleteActor(ctx, "team-a", "id1"); err != nil { + t.Fatalf("DeleteActor failed: %v", err) + } + if err := s.DeleteAtespace(ctx, "team-a"); err != nil { + t.Errorf("DeleteAtespace after actor removed = %v, want nil (re-scan should find it empty)", err) + } +} + +func TestDeleteAtespace_EmptyWhileOtherTenantNonEmpty(t *testing.T) { + mr, s, ctx := setupTest(t) + defer mr.Close() + + if err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { + t.Fatalf("CreateAtespace(team-a) failed: %v", err) + } + if err := s.CreateAtespace(ctx, newTestAtespace("team-b")); err != nil { + t.Fatalf("CreateAtespace(team-b) failed: %v", err) + } + // Actor lives ONLY in team-b. + if err := s.CreateActor(ctx, &ateapipb.Actor{ActorId: "id1", Atespace: "team-b", Status: ateapipb.Actor_STATUS_SUSPENDED}); err != nil { + t.Fatalf("CreateActor failed: %v", err) + } + + // team-a is empty → delete must succeed. + if err := s.DeleteAtespace(ctx, "team-a"); err != nil { + t.Errorf("DeleteAtespace(team-a, empty) = %v, want nil (must not be blocked by team-b's actor)", err) + } + if _, err := s.GetAtespace(ctx, "team-a"); !errors.Is(err, store.ErrNotFound) { + t.Errorf("after delete, GetAtespace(team-a) = %v, want ErrNotFound", err) + } + // team-b is still non-empty → still rejected. + if err := s.DeleteAtespace(ctx, "team-b"); !errors.Is(err, store.ErrFailedPrecondition) { + t.Errorf("DeleteAtespace(team-b, non-empty) = %v, want ErrFailedPrecondition", err) + } +} diff --git a/cmd/ateapi/internal/store/store.go b/cmd/ateapi/internal/store/store.go index 26dde9bda..5de4bdc30 100644 --- a/cmd/ateapi/internal/store/store.go +++ b/cmd/ateapi/internal/store/store.go @@ -55,6 +55,22 @@ type Interface interface { // empty. Returns a page of actors and a next page token. ListActors(ctx context.Context, atespace string, pageSize int32, pageToken string) ([]*ateapipb.Actor, string, error) + // Stores a new atespace. Returns ErrAlreadyExists if the name is taken. + CreateAtespace(ctx context.Context, atespace *ateapipb.Atespace) error + + // Fetches an atespace by name. Returns ErrNotFound if missing. + GetAtespace(ctx context.Context, name string) (*ateapipb.Atespace, error) + + // Lists all atespaces. Returns nil if none found. + ListAtespaces(ctx context.Context) ([]*ateapipb.Atespace, error) + + // AtespaceExists reports whether the atespace object exists. + AtespaceExists(ctx context.Context, name string) (bool, error) + + // Removes an empty atespace. Returns ErrNotFound if missing, or + // ErrFailedPrecondition if any actor::* key still exists. + DeleteAtespace(ctx context.Context, name 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/cmd/atecontroller/internal/controllers/actortemplate_controller.go b/cmd/atecontroller/internal/controllers/actortemplate_controller.go index 630820d06..8adea9510 100644 --- a/cmd/atecontroller/internal/controllers/actortemplate_controller.go +++ b/cmd/atecontroller/internal/controllers/actortemplate_controller.go @@ -22,6 +22,8 @@ import ( atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" k8errors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -71,13 +73,19 @@ func (r *ActorTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Reques case atev1alpha1.PhaseInitial: actorID := uuid.NewString() + // The golden actor lives in the template's namespace as its atespace. + _, err := r.AteClient.CreateAtespace(ctx, &ateapipb.CreateAtespaceRequest{Name: at.ObjectMeta.Namespace}) + if err != nil && status.Code(err) != codes.AlreadyExists { + return ctrl.Result{}, fmt.Errorf("while ensuring atespace %q: %w", at.ObjectMeta.Namespace, err) + } + createReq := &ateapipb.CreateActorRequest{ ActorId: actorID, Atespace: at.ObjectMeta.Namespace, ActorTemplateNamespace: at.ObjectMeta.Namespace, ActorTemplateName: at.ObjectMeta.Name, } - _, err := r.AteClient.CreateActor(ctx, createReq) + _, err = r.AteClient.CreateActor(ctx, createReq) if err != nil { return ctrl.Result{}, fmt.Errorf("while creating golden actor: %w", err) } diff --git a/cmd/kubectl-ate/README.md b/cmd/kubectl-ate/README.md index 7ad875ba2..557650511 100644 --- a/cmd/kubectl-ate/README.md +++ b/cmd/kubectl-ate/README.md @@ -115,22 +115,44 @@ kubectl ate get workers | `STATUS` | `FREE` (idle, ready to receive an actor) or `ASSIGNED` (currently hosting an actor). | | `ASSIGNED ACTOR` | If `STATUS=ASSIGNED`, the actor reference `/