From ba59a7cf13a7f651c432167b05bc3875c16cd12c Mon Sep 17 00:00:00 2001 From: milx Date: Tue, 17 Feb 2026 21:27:12 +0330 Subject: [PATCH 01/25] Feat: Add Actions list endpint to gRPC --- api/proto/agent.proto | 26 +++ pkg/api/v1/agent.pb.go | 446 ++++++++++++++++++++++++++++-------- pkg/api/v1/agent_grpc.pb.go | 40 ++++ 3 files changed, 414 insertions(+), 98 deletions(-) diff --git a/api/proto/agent.proto b/api/proto/agent.proto index 3e85a50..ad20d12 100644 --- a/api/proto/agent.proto +++ b/api/proto/agent.proto @@ -20,6 +20,9 @@ service AgentService { // HealthCheck returns agent health status rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse); + + // ListActions returns action/task history tracked by the agent since startup + rpc ListActions(ListActionsRequest) returns (ListActionsResponse); } message ApplyWorkloadRequest { @@ -73,6 +76,29 @@ message HealthCheckResponse { double disk_utilization = 6; // Disk utilization percentage for root mount (0-100) } +message ListActionsRequest { + string workload_id = 1; // optional filter + string action_type = 2; // optional filter: apply_workload, delete_workload, ... + string status = 3; // optional filter: pending, running, completed, failed + int32 limit = 4; // optional max number of actions (0 = all) + bool newest_first = 5; // if true sorts by created_at descending +} + +message AgentAction { + string task_id = 1; + string workload_id = 2; + string action_type = 3; + string status = 4; + string error = 5; + int64 created_at = 6; + int64 started_at = 7; + int64 ended_at = 8; +} + +message ListActionsResponse { + repeated AgentAction actions = 1; +} + enum WorkloadType { WORKLOAD_TYPE_UNSPECIFIED = 0; WORKLOAD_TYPE_CONTAINER = 1; diff --git a/pkg/api/v1/agent.pb.go b/pkg/api/v1/agent.pb.go index f87a44d..604e129 100644 --- a/pkg/api/v1/agent.pb.go +++ b/pkg/api/v1/agent.pb.go @@ -716,6 +716,226 @@ func (x *HealthCheckResponse) GetDiskUtilization() float64 { return 0 } +type ListActionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkloadId string `protobuf:"bytes,1,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` // optional filter + ActionType string `protobuf:"bytes,2,opt,name=action_type,json=actionType,proto3" json:"action_type,omitempty"` // optional filter: apply_workload, delete_workload, ... + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` // optional filter: pending, running, completed, failed + Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` // optional max number of actions (0 = all) + NewestFirst bool `protobuf:"varint,5,opt,name=newest_first,json=newestFirst,proto3" json:"newest_first,omitempty"` // if true sorts by created_at descending + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListActionsRequest) Reset() { + *x = ListActionsRequest{} + mi := &file_agent_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListActionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListActionsRequest) ProtoMessage() {} + +func (x *ListActionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[10] + 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 ListActionsRequest.ProtoReflect.Descriptor instead. +func (*ListActionsRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{10} +} + +func (x *ListActionsRequest) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +func (x *ListActionsRequest) GetActionType() string { + if x != nil { + return x.ActionType + } + return "" +} + +func (x *ListActionsRequest) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ListActionsRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ListActionsRequest) GetNewestFirst() bool { + if x != nil { + return x.NewestFirst + } + return false +} + +type AgentAction struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + WorkloadId string `protobuf:"bytes,2,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` + ActionType string `protobuf:"bytes,3,opt,name=action_type,json=actionType,proto3" json:"action_type,omitempty"` + Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` + CreatedAt int64 `protobuf:"varint,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + StartedAt int64 `protobuf:"varint,7,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` + EndedAt int64 `protobuf:"varint,8,opt,name=ended_at,json=endedAt,proto3" json:"ended_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentAction) Reset() { + *x = AgentAction{} + mi := &file_agent_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentAction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentAction) ProtoMessage() {} + +func (x *AgentAction) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[11] + 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 AgentAction.ProtoReflect.Descriptor instead. +func (*AgentAction) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{11} +} + +func (x *AgentAction) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *AgentAction) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +func (x *AgentAction) GetActionType() string { + if x != nil { + return x.ActionType + } + return "" +} + +func (x *AgentAction) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *AgentAction) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *AgentAction) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *AgentAction) GetStartedAt() int64 { + if x != nil { + return x.StartedAt + } + return 0 +} + +func (x *AgentAction) GetEndedAt() int64 { + if x != nil { + return x.EndedAt + } + return 0 +} + +type ListActionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Actions []*AgentAction `protobuf:"bytes,1,rep,name=actions,proto3" json:"actions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListActionsResponse) Reset() { + *x = ListActionsResponse{} + mi := &file_agent_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListActionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListActionsResponse) ProtoMessage() {} + +func (x *ListActionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[12] + 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 ListActionsResponse.ProtoReflect.Descriptor instead. +func (*ListActionsResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{12} +} + +func (x *ListActionsResponse) GetActions() []*AgentAction { + if x != nil { + return x.Actions + } + return nil +} + type WorkloadSpec struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Spec: @@ -730,7 +950,7 @@ type WorkloadSpec struct { func (x *WorkloadSpec) Reset() { *x = WorkloadSpec{} - mi := &file_agent_proto_msgTypes[10] + mi := &file_agent_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -742,7 +962,7 @@ func (x *WorkloadSpec) String() string { func (*WorkloadSpec) ProtoMessage() {} func (x *WorkloadSpec) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[10] + mi := &file_agent_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -755,7 +975,7 @@ func (x *WorkloadSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkloadSpec.ProtoReflect.Descriptor instead. func (*WorkloadSpec) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{10} + return file_agent_proto_rawDescGZIP(), []int{13} } func (x *WorkloadSpec) GetSpec() isWorkloadSpec_Spec { @@ -831,7 +1051,7 @@ type ContainerSpec struct { func (x *ContainerSpec) Reset() { *x = ContainerSpec{} - mi := &file_agent_proto_msgTypes[11] + mi := &file_agent_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -843,7 +1063,7 @@ func (x *ContainerSpec) String() string { func (*ContainerSpec) ProtoMessage() {} func (x *ContainerSpec) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[11] + mi := &file_agent_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -856,7 +1076,7 @@ func (x *ContainerSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerSpec.ProtoReflect.Descriptor instead. func (*ContainerSpec) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{11} + return file_agent_proto_rawDescGZIP(), []int{14} } func (x *ContainerSpec) GetImage() string { @@ -933,7 +1153,7 @@ type ComposeSpec struct { func (x *ComposeSpec) Reset() { *x = ComposeSpec{} - mi := &file_agent_proto_msgTypes[12] + mi := &file_agent_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -945,7 +1165,7 @@ func (x *ComposeSpec) String() string { func (*ComposeSpec) ProtoMessage() {} func (x *ComposeSpec) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[12] + mi := &file_agent_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -958,7 +1178,7 @@ func (x *ComposeSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use ComposeSpec.ProtoReflect.Descriptor instead. func (*ComposeSpec) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{12} + return file_agent_proto_rawDescGZIP(), []int{15} } func (x *ComposeSpec) GetProjectName() string { @@ -998,7 +1218,7 @@ type VMSpec struct { func (x *VMSpec) Reset() { *x = VMSpec{} - mi := &file_agent_proto_msgTypes[13] + mi := &file_agent_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1010,7 +1230,7 @@ func (x *VMSpec) String() string { func (*VMSpec) ProtoMessage() {} func (x *VMSpec) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[13] + mi := &file_agent_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1023,7 +1243,7 @@ func (x *VMSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use VMSpec.ProtoReflect.Descriptor instead. func (*VMSpec) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{13} + return file_agent_proto_rawDescGZIP(), []int{16} } func (x *VMSpec) GetName() string { @@ -1094,7 +1314,7 @@ type CloudInitConfig struct { func (x *CloudInitConfig) Reset() { *x = CloudInitConfig{} - mi := &file_agent_proto_msgTypes[14] + mi := &file_agent_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1106,7 +1326,7 @@ func (x *CloudInitConfig) String() string { func (*CloudInitConfig) ProtoMessage() {} func (x *CloudInitConfig) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[14] + mi := &file_agent_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1119,7 +1339,7 @@ func (x *CloudInitConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use CloudInitConfig.ProtoReflect.Descriptor instead. func (*CloudInitConfig) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{14} + return file_agent_proto_rawDescGZIP(), []int{17} } func (x *CloudInitConfig) GetUserData() string { @@ -1161,7 +1381,7 @@ type VolumeMount struct { func (x *VolumeMount) Reset() { *x = VolumeMount{} - mi := &file_agent_proto_msgTypes[15] + mi := &file_agent_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1173,7 +1393,7 @@ func (x *VolumeMount) String() string { func (*VolumeMount) ProtoMessage() {} func (x *VolumeMount) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[15] + mi := &file_agent_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1186,7 +1406,7 @@ func (x *VolumeMount) ProtoReflect() protoreflect.Message { // Deprecated: Use VolumeMount.ProtoReflect.Descriptor instead. func (*VolumeMount) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{15} + return file_agent_proto_rawDescGZIP(), []int{18} } func (x *VolumeMount) GetHostPath() string { @@ -1221,7 +1441,7 @@ type PortMapping struct { func (x *PortMapping) Reset() { *x = PortMapping{} - mi := &file_agent_proto_msgTypes[16] + mi := &file_agent_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1233,7 +1453,7 @@ func (x *PortMapping) String() string { func (*PortMapping) ProtoMessage() {} func (x *PortMapping) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[16] + mi := &file_agent_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1246,7 +1466,7 @@ func (x *PortMapping) ProtoReflect() protoreflect.Message { // Deprecated: Use PortMapping.ProtoReflect.Descriptor instead. func (*PortMapping) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{16} + return file_agent_proto_rawDescGZIP(), []int{19} } func (x *PortMapping) GetHostPort() int32 { @@ -1281,7 +1501,7 @@ type ResourceLimits struct { func (x *ResourceLimits) Reset() { *x = ResourceLimits{} - mi := &file_agent_proto_msgTypes[17] + mi := &file_agent_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1293,7 +1513,7 @@ func (x *ResourceLimits) String() string { func (*ResourceLimits) ProtoMessage() {} func (x *ResourceLimits) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[17] + mi := &file_agent_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1306,7 +1526,7 @@ func (x *ResourceLimits) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceLimits.ProtoReflect.Descriptor instead. func (*ResourceLimits) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{17} + return file_agent_proto_rawDescGZIP(), []int{20} } func (x *ResourceLimits) GetCpuShares() int64 { @@ -1340,7 +1560,7 @@ type RestartPolicy struct { func (x *RestartPolicy) Reset() { *x = RestartPolicy{} - mi := &file_agent_proto_msgTypes[18] + mi := &file_agent_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1352,7 +1572,7 @@ func (x *RestartPolicy) String() string { func (*RestartPolicy) ProtoMessage() {} func (x *RestartPolicy) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[18] + mi := &file_agent_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1365,7 +1585,7 @@ func (x *RestartPolicy) ProtoReflect() protoreflect.Message { // Deprecated: Use RestartPolicy.ProtoReflect.Descriptor instead. func (*RestartPolicy) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{18} + return file_agent_proto_rawDescGZIP(), []int{21} } func (x *RestartPolicy) GetPolicy() string { @@ -1396,7 +1616,7 @@ type DiskConfig struct { func (x *DiskConfig) Reset() { *x = DiskConfig{} - mi := &file_agent_proto_msgTypes[19] + mi := &file_agent_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1408,7 +1628,7 @@ func (x *DiskConfig) String() string { func (*DiskConfig) ProtoMessage() {} func (x *DiskConfig) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[19] + mi := &file_agent_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1421,7 +1641,7 @@ func (x *DiskConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use DiskConfig.ProtoReflect.Descriptor instead. func (*DiskConfig) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{19} + return file_agent_proto_rawDescGZIP(), []int{22} } func (x *DiskConfig) GetPath() string { @@ -1477,7 +1697,7 @@ type NetworkConfig struct { func (x *NetworkConfig) Reset() { *x = NetworkConfig{} - mi := &file_agent_proto_msgTypes[20] + mi := &file_agent_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1489,7 +1709,7 @@ func (x *NetworkConfig) String() string { func (*NetworkConfig) ProtoMessage() {} func (x *NetworkConfig) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[20] + mi := &file_agent_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1502,7 +1722,7 @@ func (x *NetworkConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead. func (*NetworkConfig) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{20} + return file_agent_proto_rawDescGZIP(), []int{23} } func (x *NetworkConfig) GetNetwork() string { @@ -1543,7 +1763,7 @@ type WorkloadStatus struct { func (x *WorkloadStatus) Reset() { *x = WorkloadStatus{} - mi := &file_agent_proto_msgTypes[21] + mi := &file_agent_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1555,7 +1775,7 @@ func (x *WorkloadStatus) String() string { func (*WorkloadStatus) ProtoMessage() {} func (x *WorkloadStatus) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[21] + mi := &file_agent_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1568,7 +1788,7 @@ func (x *WorkloadStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkloadStatus.ProtoReflect.Descriptor instead. func (*WorkloadStatus) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{21} + return file_agent_proto_rawDescGZIP(), []int{24} } func (x *WorkloadStatus) GetId() string { @@ -1674,7 +1894,30 @@ const file_agent_proto_rawDesc = "" + "\x10disk_utilization\x18\x06 \x01(\x01R\x0fdiskUtilization\x1a@\n" + "\x12RuntimeStatusEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbb\x01\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa7\x01\n" + + "\x12ListActionsRequest\x12\x1f\n" + + "\vworkload_id\x18\x01 \x01(\tR\n" + + "workloadId\x12\x1f\n" + + "\vaction_type\x18\x02 \x01(\tR\n" + + "actionType\x12\x16\n" + + "\x06status\x18\x03 \x01(\tR\x06status\x12\x14\n" + + "\x05limit\x18\x04 \x01(\x05R\x05limit\x12!\n" + + "\fnewest_first\x18\x05 \x01(\bR\vnewestFirst\"\xef\x01\n" + + "\vAgentAction\x12\x17\n" + + "\atask_id\x18\x01 \x01(\tR\x06taskId\x12\x1f\n" + + "\vworkload_id\x18\x02 \x01(\tR\n" + + "workloadId\x12\x1f\n" + + "\vaction_type\x18\x03 \x01(\tR\n" + + "actionType\x12\x16\n" + + "\x06status\x18\x04 \x01(\tR\x06status\x12\x14\n" + + "\x05error\x18\x05 \x01(\tR\x05error\x12\x1d\n" + + "\n" + + "created_at\x18\x06 \x01(\x03R\tcreatedAt\x12\x1d\n" + + "\n" + + "started_at\x18\a \x01(\x03R\tstartedAt\x12\x19\n" + + "\bended_at\x18\b \x01(\x03R\aendedAt\"M\n" + + "\x13ListActionsResponse\x126\n" + + "\aactions\x18\x01 \x03(\v2\x1c.persys.agent.v1.AgentActionR\aactions\"\xbb\x01\n" + "\fWorkloadSpec\x12>\n" + "\tcontainer\x18\x01 \x01(\v2\x1e.persys.agent.v1.ContainerSpecH\x00R\tcontainer\x128\n" + "\acompose\x18\x02 \x01(\v2\x1c.persys.agent.v1.ComposeSpecH\x00R\acompose\x12)\n" + @@ -1783,13 +2026,14 @@ const file_agent_proto_rawDesc = "" + "\x14ACTUAL_STATE_RUNNING\x10\x02\x12\x18\n" + "\x14ACTUAL_STATE_STOPPED\x10\x03\x12\x17\n" + "\x13ACTUAL_STATE_FAILED\x10\x04\x12\x18\n" + - "\x14ACTUAL_STATE_UNKNOWN\x10\x052\xf7\x03\n" + + "\x14ACTUAL_STATE_UNKNOWN\x10\x052\xd1\x04\n" + "\fAgentService\x12^\n" + "\rApplyWorkload\x12%.persys.agent.v1.ApplyWorkloadRequest\x1a&.persys.agent.v1.ApplyWorkloadResponse\x12a\n" + "\x0eDeleteWorkload\x12&.persys.agent.v1.DeleteWorkloadRequest\x1a'.persys.agent.v1.DeleteWorkloadResponse\x12j\n" + "\x11GetWorkloadStatus\x12).persys.agent.v1.GetWorkloadStatusRequest\x1a*.persys.agent.v1.GetWorkloadStatusResponse\x12^\n" + "\rListWorkloads\x12%.persys.agent.v1.ListWorkloadsRequest\x1a&.persys.agent.v1.ListWorkloadsResponse\x12X\n" + - "\vHealthCheck\x12#.persys.agent.v1.HealthCheckRequest\x1a$.persys.agent.v1.HealthCheckResponseB/Z-github.com/persys/compute-agent/pkg/api/v1;v1b\x06proto3" + "\vHealthCheck\x12#.persys.agent.v1.HealthCheckRequest\x1a$.persys.agent.v1.HealthCheckResponse\x12X\n" + + "\vListActions\x12#.persys.agent.v1.ListActionsRequest\x1a$.persys.agent.v1.ListActionsResponseB/Z-github.com/persys/compute-agent/pkg/api/v1;v1b\x06proto3" var ( file_agent_proto_rawDescOnce sync.Once @@ -1804,7 +2048,7 @@ func file_agent_proto_rawDescGZIP() []byte { } var file_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 28) +var file_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 31) var file_agent_proto_goTypes = []any{ (WorkloadType)(0), // 0: persys.agent.v1.WorkloadType (DesiredState)(0), // 1: persys.agent.v1.DesiredState @@ -1819,67 +2063,73 @@ var file_agent_proto_goTypes = []any{ (*ListWorkloadsResponse)(nil), // 10: persys.agent.v1.ListWorkloadsResponse (*HealthCheckRequest)(nil), // 11: persys.agent.v1.HealthCheckRequest (*HealthCheckResponse)(nil), // 12: persys.agent.v1.HealthCheckResponse - (*WorkloadSpec)(nil), // 13: persys.agent.v1.WorkloadSpec - (*ContainerSpec)(nil), // 14: persys.agent.v1.ContainerSpec - (*ComposeSpec)(nil), // 15: persys.agent.v1.ComposeSpec - (*VMSpec)(nil), // 16: persys.agent.v1.VMSpec - (*CloudInitConfig)(nil), // 17: persys.agent.v1.CloudInitConfig - (*VolumeMount)(nil), // 18: persys.agent.v1.VolumeMount - (*PortMapping)(nil), // 19: persys.agent.v1.PortMapping - (*ResourceLimits)(nil), // 20: persys.agent.v1.ResourceLimits - (*RestartPolicy)(nil), // 21: persys.agent.v1.RestartPolicy - (*DiskConfig)(nil), // 22: persys.agent.v1.DiskConfig - (*NetworkConfig)(nil), // 23: persys.agent.v1.NetworkConfig - (*WorkloadStatus)(nil), // 24: persys.agent.v1.WorkloadStatus - nil, // 25: persys.agent.v1.HealthCheckResponse.RuntimeStatusEntry - nil, // 26: persys.agent.v1.ContainerSpec.EnvEntry - nil, // 27: persys.agent.v1.ContainerSpec.LabelsEntry - nil, // 28: persys.agent.v1.ComposeSpec.EnvEntry - nil, // 29: persys.agent.v1.VMSpec.MetadataEntry - nil, // 30: persys.agent.v1.WorkloadStatus.MetadataEntry + (*ListActionsRequest)(nil), // 13: persys.agent.v1.ListActionsRequest + (*AgentAction)(nil), // 14: persys.agent.v1.AgentAction + (*ListActionsResponse)(nil), // 15: persys.agent.v1.ListActionsResponse + (*WorkloadSpec)(nil), // 16: persys.agent.v1.WorkloadSpec + (*ContainerSpec)(nil), // 17: persys.agent.v1.ContainerSpec + (*ComposeSpec)(nil), // 18: persys.agent.v1.ComposeSpec + (*VMSpec)(nil), // 19: persys.agent.v1.VMSpec + (*CloudInitConfig)(nil), // 20: persys.agent.v1.CloudInitConfig + (*VolumeMount)(nil), // 21: persys.agent.v1.VolumeMount + (*PortMapping)(nil), // 22: persys.agent.v1.PortMapping + (*ResourceLimits)(nil), // 23: persys.agent.v1.ResourceLimits + (*RestartPolicy)(nil), // 24: persys.agent.v1.RestartPolicy + (*DiskConfig)(nil), // 25: persys.agent.v1.DiskConfig + (*NetworkConfig)(nil), // 26: persys.agent.v1.NetworkConfig + (*WorkloadStatus)(nil), // 27: persys.agent.v1.WorkloadStatus + nil, // 28: persys.agent.v1.HealthCheckResponse.RuntimeStatusEntry + nil, // 29: persys.agent.v1.ContainerSpec.EnvEntry + nil, // 30: persys.agent.v1.ContainerSpec.LabelsEntry + nil, // 31: persys.agent.v1.ComposeSpec.EnvEntry + nil, // 32: persys.agent.v1.VMSpec.MetadataEntry + nil, // 33: persys.agent.v1.WorkloadStatus.MetadataEntry } var file_agent_proto_depIdxs = []int32{ 0, // 0: persys.agent.v1.ApplyWorkloadRequest.type:type_name -> persys.agent.v1.WorkloadType 1, // 1: persys.agent.v1.ApplyWorkloadRequest.desired_state:type_name -> persys.agent.v1.DesiredState - 13, // 2: persys.agent.v1.ApplyWorkloadRequest.spec:type_name -> persys.agent.v1.WorkloadSpec - 24, // 3: persys.agent.v1.ApplyWorkloadResponse.status:type_name -> persys.agent.v1.WorkloadStatus - 24, // 4: persys.agent.v1.GetWorkloadStatusResponse.status:type_name -> persys.agent.v1.WorkloadStatus + 16, // 2: persys.agent.v1.ApplyWorkloadRequest.spec:type_name -> persys.agent.v1.WorkloadSpec + 27, // 3: persys.agent.v1.ApplyWorkloadResponse.status:type_name -> persys.agent.v1.WorkloadStatus + 27, // 4: persys.agent.v1.GetWorkloadStatusResponse.status:type_name -> persys.agent.v1.WorkloadStatus 0, // 5: persys.agent.v1.ListWorkloadsRequest.type:type_name -> persys.agent.v1.WorkloadType - 24, // 6: persys.agent.v1.ListWorkloadsResponse.workloads:type_name -> persys.agent.v1.WorkloadStatus - 25, // 7: persys.agent.v1.HealthCheckResponse.runtime_status:type_name -> persys.agent.v1.HealthCheckResponse.RuntimeStatusEntry - 14, // 8: persys.agent.v1.WorkloadSpec.container:type_name -> persys.agent.v1.ContainerSpec - 15, // 9: persys.agent.v1.WorkloadSpec.compose:type_name -> persys.agent.v1.ComposeSpec - 16, // 10: persys.agent.v1.WorkloadSpec.vm:type_name -> persys.agent.v1.VMSpec - 26, // 11: persys.agent.v1.ContainerSpec.env:type_name -> persys.agent.v1.ContainerSpec.EnvEntry - 18, // 12: persys.agent.v1.ContainerSpec.volumes:type_name -> persys.agent.v1.VolumeMount - 19, // 13: persys.agent.v1.ContainerSpec.ports:type_name -> persys.agent.v1.PortMapping - 20, // 14: persys.agent.v1.ContainerSpec.resources:type_name -> persys.agent.v1.ResourceLimits - 21, // 15: persys.agent.v1.ContainerSpec.restart_policy:type_name -> persys.agent.v1.RestartPolicy - 27, // 16: persys.agent.v1.ContainerSpec.labels:type_name -> persys.agent.v1.ContainerSpec.LabelsEntry - 28, // 17: persys.agent.v1.ComposeSpec.env:type_name -> persys.agent.v1.ComposeSpec.EnvEntry - 22, // 18: persys.agent.v1.VMSpec.disks:type_name -> persys.agent.v1.DiskConfig - 23, // 19: persys.agent.v1.VMSpec.networks:type_name -> persys.agent.v1.NetworkConfig - 29, // 20: persys.agent.v1.VMSpec.metadata:type_name -> persys.agent.v1.VMSpec.MetadataEntry - 17, // 21: persys.agent.v1.VMSpec.cloud_init_config:type_name -> persys.agent.v1.CloudInitConfig - 0, // 22: persys.agent.v1.WorkloadStatus.type:type_name -> persys.agent.v1.WorkloadType - 1, // 23: persys.agent.v1.WorkloadStatus.desired_state:type_name -> persys.agent.v1.DesiredState - 2, // 24: persys.agent.v1.WorkloadStatus.actual_state:type_name -> persys.agent.v1.ActualState - 30, // 25: persys.agent.v1.WorkloadStatus.metadata:type_name -> persys.agent.v1.WorkloadStatus.MetadataEntry - 3, // 26: persys.agent.v1.AgentService.ApplyWorkload:input_type -> persys.agent.v1.ApplyWorkloadRequest - 5, // 27: persys.agent.v1.AgentService.DeleteWorkload:input_type -> persys.agent.v1.DeleteWorkloadRequest - 7, // 28: persys.agent.v1.AgentService.GetWorkloadStatus:input_type -> persys.agent.v1.GetWorkloadStatusRequest - 9, // 29: persys.agent.v1.AgentService.ListWorkloads:input_type -> persys.agent.v1.ListWorkloadsRequest - 11, // 30: persys.agent.v1.AgentService.HealthCheck:input_type -> persys.agent.v1.HealthCheckRequest - 4, // 31: persys.agent.v1.AgentService.ApplyWorkload:output_type -> persys.agent.v1.ApplyWorkloadResponse - 6, // 32: persys.agent.v1.AgentService.DeleteWorkload:output_type -> persys.agent.v1.DeleteWorkloadResponse - 8, // 33: persys.agent.v1.AgentService.GetWorkloadStatus:output_type -> persys.agent.v1.GetWorkloadStatusResponse - 10, // 34: persys.agent.v1.AgentService.ListWorkloads:output_type -> persys.agent.v1.ListWorkloadsResponse - 12, // 35: persys.agent.v1.AgentService.HealthCheck:output_type -> persys.agent.v1.HealthCheckResponse - 31, // [31:36] is the sub-list for method output_type - 26, // [26:31] is the sub-list for method input_type - 26, // [26:26] is the sub-list for extension type_name - 26, // [26:26] is the sub-list for extension extendee - 0, // [0:26] is the sub-list for field type_name + 27, // 6: persys.agent.v1.ListWorkloadsResponse.workloads:type_name -> persys.agent.v1.WorkloadStatus + 28, // 7: persys.agent.v1.HealthCheckResponse.runtime_status:type_name -> persys.agent.v1.HealthCheckResponse.RuntimeStatusEntry + 14, // 8: persys.agent.v1.ListActionsResponse.actions:type_name -> persys.agent.v1.AgentAction + 17, // 9: persys.agent.v1.WorkloadSpec.container:type_name -> persys.agent.v1.ContainerSpec + 18, // 10: persys.agent.v1.WorkloadSpec.compose:type_name -> persys.agent.v1.ComposeSpec + 19, // 11: persys.agent.v1.WorkloadSpec.vm:type_name -> persys.agent.v1.VMSpec + 29, // 12: persys.agent.v1.ContainerSpec.env:type_name -> persys.agent.v1.ContainerSpec.EnvEntry + 21, // 13: persys.agent.v1.ContainerSpec.volumes:type_name -> persys.agent.v1.VolumeMount + 22, // 14: persys.agent.v1.ContainerSpec.ports:type_name -> persys.agent.v1.PortMapping + 23, // 15: persys.agent.v1.ContainerSpec.resources:type_name -> persys.agent.v1.ResourceLimits + 24, // 16: persys.agent.v1.ContainerSpec.restart_policy:type_name -> persys.agent.v1.RestartPolicy + 30, // 17: persys.agent.v1.ContainerSpec.labels:type_name -> persys.agent.v1.ContainerSpec.LabelsEntry + 31, // 18: persys.agent.v1.ComposeSpec.env:type_name -> persys.agent.v1.ComposeSpec.EnvEntry + 25, // 19: persys.agent.v1.VMSpec.disks:type_name -> persys.agent.v1.DiskConfig + 26, // 20: persys.agent.v1.VMSpec.networks:type_name -> persys.agent.v1.NetworkConfig + 32, // 21: persys.agent.v1.VMSpec.metadata:type_name -> persys.agent.v1.VMSpec.MetadataEntry + 20, // 22: persys.agent.v1.VMSpec.cloud_init_config:type_name -> persys.agent.v1.CloudInitConfig + 0, // 23: persys.agent.v1.WorkloadStatus.type:type_name -> persys.agent.v1.WorkloadType + 1, // 24: persys.agent.v1.WorkloadStatus.desired_state:type_name -> persys.agent.v1.DesiredState + 2, // 25: persys.agent.v1.WorkloadStatus.actual_state:type_name -> persys.agent.v1.ActualState + 33, // 26: persys.agent.v1.WorkloadStatus.metadata:type_name -> persys.agent.v1.WorkloadStatus.MetadataEntry + 3, // 27: persys.agent.v1.AgentService.ApplyWorkload:input_type -> persys.agent.v1.ApplyWorkloadRequest + 5, // 28: persys.agent.v1.AgentService.DeleteWorkload:input_type -> persys.agent.v1.DeleteWorkloadRequest + 7, // 29: persys.agent.v1.AgentService.GetWorkloadStatus:input_type -> persys.agent.v1.GetWorkloadStatusRequest + 9, // 30: persys.agent.v1.AgentService.ListWorkloads:input_type -> persys.agent.v1.ListWorkloadsRequest + 11, // 31: persys.agent.v1.AgentService.HealthCheck:input_type -> persys.agent.v1.HealthCheckRequest + 13, // 32: persys.agent.v1.AgentService.ListActions:input_type -> persys.agent.v1.ListActionsRequest + 4, // 33: persys.agent.v1.AgentService.ApplyWorkload:output_type -> persys.agent.v1.ApplyWorkloadResponse + 6, // 34: persys.agent.v1.AgentService.DeleteWorkload:output_type -> persys.agent.v1.DeleteWorkloadResponse + 8, // 35: persys.agent.v1.AgentService.GetWorkloadStatus:output_type -> persys.agent.v1.GetWorkloadStatusResponse + 10, // 36: persys.agent.v1.AgentService.ListWorkloads:output_type -> persys.agent.v1.ListWorkloadsResponse + 12, // 37: persys.agent.v1.AgentService.HealthCheck:output_type -> persys.agent.v1.HealthCheckResponse + 15, // 38: persys.agent.v1.AgentService.ListActions:output_type -> persys.agent.v1.ListActionsResponse + 33, // [33:39] is the sub-list for method output_type + 27, // [27:33] is the sub-list for method input_type + 27, // [27:27] is the sub-list for extension type_name + 27, // [27:27] is the sub-list for extension extendee + 0, // [0:27] is the sub-list for field type_name } func init() { file_agent_proto_init() } @@ -1887,7 +2137,7 @@ func file_agent_proto_init() { if File_agent_proto != nil { return } - file_agent_proto_msgTypes[10].OneofWrappers = []any{ + file_agent_proto_msgTypes[13].OneofWrappers = []any{ (*WorkloadSpec_Container)(nil), (*WorkloadSpec_Compose)(nil), (*WorkloadSpec_Vm)(nil), @@ -1898,7 +2148,7 @@ func file_agent_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_agent_proto_rawDesc), len(file_agent_proto_rawDesc)), NumEnums: 3, - NumMessages: 28, + NumMessages: 31, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/api/v1/agent_grpc.pb.go b/pkg/api/v1/agent_grpc.pb.go index 294eecd..2fb05bf 100644 --- a/pkg/api/v1/agent_grpc.pb.go +++ b/pkg/api/v1/agent_grpc.pb.go @@ -24,6 +24,7 @@ const ( AgentService_GetWorkloadStatus_FullMethodName = "/persys.agent.v1.AgentService/GetWorkloadStatus" AgentService_ListWorkloads_FullMethodName = "/persys.agent.v1.AgentService/ListWorkloads" AgentService_HealthCheck_FullMethodName = "/persys.agent.v1.AgentService/HealthCheck" + AgentService_ListActions_FullMethodName = "/persys.agent.v1.AgentService/ListActions" ) // AgentServiceClient is the client API for AgentService service. @@ -42,6 +43,8 @@ type AgentServiceClient interface { ListWorkloads(ctx context.Context, in *ListWorkloadsRequest, opts ...grpc.CallOption) (*ListWorkloadsResponse, error) // HealthCheck returns agent health status HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) + // ListActions returns action/task history tracked by the agent since startup + ListActions(ctx context.Context, in *ListActionsRequest, opts ...grpc.CallOption) (*ListActionsResponse, error) } type agentServiceClient struct { @@ -102,6 +105,16 @@ func (c *agentServiceClient) HealthCheck(ctx context.Context, in *HealthCheckReq return out, nil } +func (c *agentServiceClient) ListActions(ctx context.Context, in *ListActionsRequest, opts ...grpc.CallOption) (*ListActionsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListActionsResponse) + err := c.cc.Invoke(ctx, AgentService_ListActions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AgentServiceServer is the server API for AgentService service. // All implementations must embed UnimplementedAgentServiceServer // for forward compatibility. @@ -118,6 +131,8 @@ type AgentServiceServer interface { ListWorkloads(context.Context, *ListWorkloadsRequest) (*ListWorkloadsResponse, error) // HealthCheck returns agent health status HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) + // ListActions returns action/task history tracked by the agent since startup + ListActions(context.Context, *ListActionsRequest) (*ListActionsResponse, error) mustEmbedUnimplementedAgentServiceServer() } @@ -143,6 +158,9 @@ func (UnimplementedAgentServiceServer) ListWorkloads(context.Context, *ListWorkl func (UnimplementedAgentServiceServer) HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) { return nil, status.Error(codes.Unimplemented, "method HealthCheck not implemented") } +func (UnimplementedAgentServiceServer) ListActions(context.Context, *ListActionsRequest) (*ListActionsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListActions not implemented") +} func (UnimplementedAgentServiceServer) mustEmbedUnimplementedAgentServiceServer() {} func (UnimplementedAgentServiceServer) testEmbeddedByValue() {} @@ -254,6 +272,24 @@ func _AgentService_HealthCheck_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _AgentService_ListActions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListActionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).ListActions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_ListActions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).ListActions(ctx, req.(*ListActionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AgentService_ServiceDesc is the grpc.ServiceDesc for AgentService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -281,6 +317,10 @@ var AgentService_ServiceDesc = grpc.ServiceDesc{ MethodName: "HealthCheck", Handler: _AgentService_HealthCheck_Handler, }, + { + MethodName: "ListActions", + Handler: _AgentService_ListActions_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "agent.proto", From 220c40711d344d6cf6e497f0a46cbb9a6f975861 Mon Sep 17 00:00:00 2001 From: milx Date: Tue, 17 Feb 2026 21:28:04 +0330 Subject: [PATCH 02/25] Feat: Full Feauture Client Test with Spec file suppot for workload orchestration --- examples/client/main.go | 539 ++++++++++++++++++++++++++++++++++------ 1 file changed, 460 insertions(+), 79 deletions(-) diff --git a/examples/client/main.go b/examples/client/main.go index fc85951..f660c30 100644 --- a/examples/client/main.go +++ b/examples/client/main.go @@ -5,10 +5,13 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" + "encoding/json" "flag" "fmt" "log" "os" + "strconv" + "strings" "time" pb "github.com/persys/compute-agent/pkg/api/v1" @@ -23,9 +26,40 @@ func main() { keyFile := flag.String("key", "", "Client key file") caFile := flag.String("ca", "", "CA certificate file") insecure := flag.Bool("insecure", false, "Disable TLS") - action := flag.String("action", "health", "Action to perform: health, apply, apply-compose, apply-vm, status, list, delete") + action := flag.String("action", "health", "Action to perform: health, apply, apply-compose, apply-vm, status, list, delete, list-actions") workloadID := flag.String("id", "", "Workload ID") workloadType := flag.String("type", "container", "Workload type: container, compose, vm") + actionType := flag.String("action-type", "", "Filter list-actions by action type (apply_workload, delete_workload, ...)") + actionStatus := flag.String("action-status", "", "Filter list-actions by status (pending, running, completed, failed)") + actionLimit := flag.Int("action-limit", 0, "Limit list-actions results (0 = all)") + newestFirst := flag.Bool("newest-first", true, "Sort list-actions by newest first") + waitForResult := flag.Bool("wait", true, "For apply actions, poll workload status until terminal state") + waitTimeout := flag.Duration("wait-timeout", 45*time.Second, "Maximum time to wait for terminal workload state") + revisionID := flag.String("revision", "rev-1", "Workload revision ID") + desiredState := flag.String("desired-state", "running", "Desired state: running or stopped") + specFile := flag.String("spec-file", "", "Optional JSON file for workload spec (container/compose/vm)") + + containerImage := flag.String("container-image", "nginx:latest", "Container image") + containerCommand := flag.String("container-command", "", "Container command as comma-separated values") + containerArgs := flag.String("container-args", "", "Container args as comma-separated values") + containerEnv := flag.String("container-env", "", "Container env as comma-separated key=value pairs") + containerLabels := flag.String("container-labels", "", "Container labels as comma-separated key=value pairs") + containerPorts := flag.String("container-ports", "8080:80/tcp", "Container ports as comma-separated host:container/proto") + containerVolumes := flag.String("container-volumes", "", "Container volumes as comma-separated /host:/container[:ro|rw]") + containerCPUShares := flag.Int64("container-cpu-shares", 0, "Container CPU shares") + containerMemoryMB := flag.Int64("container-memory-mb", 0, "Container memory limit in MB") + containerMemorySwapMB := flag.Int64("container-memory-swap-mb", 0, "Container memory swap limit in MB") + containerRestartPolicy := flag.String("container-restart-policy", "unless-stopped", "Container restart policy") + containerRestartMaxRetry := flag.Int("container-restart-max-retry", 0, "Container restart max retry count") + + vmName := flag.String("vm-name", "", "VM name (defaults to workload ID)") + vmVCPUs := flag.Int("vm-vcpus", 2, "VM vCPU count") + vmMemoryMB := flag.Int64("vm-memory-mb", 2048, "VM memory in MB") + vmCloudInitFile := flag.String("vm-cloud-init-file", "", "Path to cloud-init user-data file") + vmCloudInitInline := flag.String("vm-cloud-init", "", "Inline cloud-init user-data") + vmDisks := flag.String("vm-disks", "", "VM disks as ';' entries: path=...,device=...,format=...,size_gb=...,type=...,boot=true|false") + vmNetworks := flag.String("vm-networks", "", "VM networks as ';' entries: network=...,mac=...,ip=...") + vmMetadata := flag.String("vm-metadata", "environment=test,owner=demo,created-by=test-client", "VM metadata as comma-separated key=value") flag.Parse() // Create gRPC connection @@ -60,29 +94,119 @@ func main() { case "apply": switch *workloadType { case "container": - applyWorkload(ctx, client, *workloadID) + applyWorkload(ctx, client, *workloadID, &applyOptions{ + revisionID: *revisionID, + desired: parseDesiredState(*desiredState), + wait: *waitForResult, + waitTime: *waitTimeout, + specFile: *specFile, + container: containerOptions{ + image: *containerImage, + command: *containerCommand, + args: *containerArgs, + env: *containerEnv, + labels: *containerLabels, + ports: *containerPorts, + volumes: *containerVolumes, + cpuShares: *containerCPUShares, + memoryMB: *containerMemoryMB, + memorySwapMB: *containerMemorySwapMB, + restartPolicy: *containerRestartPolicy, + restartMaxRetries: *containerRestartMaxRetry, + }, + }) case "compose": - applyComposeWorkload(ctx, client, *workloadID) + applyComposeWorkload(ctx, client, *workloadID, *waitForResult, *waitTimeout) case "vm": - applyVMWorkload(ctx, client, *workloadID) + applyVMWorkload(ctx, client, *workloadID, &applyOptions{ + revisionID: *revisionID, + desired: parseDesiredState(*desiredState), + wait: *waitForResult, + waitTime: *waitTimeout, + specFile: *specFile, + vm: vmOptions{ + name: *vmName, + vcpus: *vmVCPUs, + memoryMB: *vmMemoryMB, + cloudInitFile: *vmCloudInitFile, + cloudInitText: *vmCloudInitInline, + disks: *vmDisks, + networks: *vmNetworks, + metadata: *vmMetadata, + }, + }) default: log.Fatalf("Unknown workload type: %s", *workloadType) } case "apply-compose": - applyComposeWorkload(ctx, client, *workloadID) + applyComposeWorkload(ctx, client, *workloadID, *waitForResult, *waitTimeout) case "apply-vm": - applyVMWorkload(ctx, client, *workloadID) + applyVMWorkload(ctx, client, *workloadID, &applyOptions{ + revisionID: *revisionID, + desired: parseDesiredState(*desiredState), + wait: *waitForResult, + waitTime: *waitTimeout, + specFile: *specFile, + vm: vmOptions{ + name: *vmName, + vcpus: *vmVCPUs, + memoryMB: *vmMemoryMB, + cloudInitFile: *vmCloudInitFile, + cloudInitText: *vmCloudInitInline, + disks: *vmDisks, + networks: *vmNetworks, + metadata: *vmMetadata, + }, + }) case "status": getStatus(ctx, client, *workloadID) case "list": listWorkloads(ctx, client) case "delete": deleteWorkload(ctx, client, *workloadID) + case "list-actions": + listActions(ctx, client, *workloadID, *actionType, *actionStatus, *actionLimit, *newestFirst) default: log.Fatalf("Unknown action: %s", *action) } } +type applyOptions struct { + revisionID string + desired pb.DesiredState + wait bool + waitTime time.Duration + specFile string + container containerOptions + vm vmOptions +} + +type containerOptions struct { + image string + command string + args string + env string + labels string + ports string + volumes string + cpuShares int64 + memoryMB int64 + memorySwapMB int64 + restartPolicy string + restartMaxRetries int +} + +type vmOptions struct { + name string + vcpus int + memoryMB int64 + cloudInitFile string + cloudInitText string + disks string + networks string + metadata string +} + func loadTLSConfig(certFile, keyFile, caFile string) (*tls.Config, error) { // Load client certificate cert, err := tls.LoadX509KeyPair(certFile, keyFile) @@ -125,36 +249,44 @@ func healthCheck(ctx context.Context, client pb.AgentServiceClient) { } } -func applyWorkload(ctx context.Context, client pb.AgentServiceClient, workloadID string) { +func applyWorkload(ctx context.Context, client pb.AgentServiceClient, workloadID string, opts *applyOptions) { if workloadID == "" { workloadID = "example-nginx" } - // Example: Create an nginx container + containerSpec := &pb.ContainerSpec{ + Image: opts.container.image, + Env: parseKeyValueCSV(opts.container.env), + Labels: parseKeyValueCSV(opts.container.labels), + RestartPolicy: &pb.RestartPolicy{ + Policy: opts.container.restartPolicy, + MaxRetryCount: int32(opts.container.restartMaxRetries), + }, + } + containerSpec.Command = parseCSVList(opts.container.command) + containerSpec.Args = parseCSVList(opts.container.args) + containerSpec.Ports = parsePortMappings(opts.container.ports) + containerSpec.Volumes = parseVolumeMounts(opts.container.volumes) + if opts.container.cpuShares > 0 || opts.container.memoryMB > 0 || opts.container.memorySwapMB > 0 { + containerSpec.Resources = &pb.ResourceLimits{ + CpuShares: opts.container.cpuShares, + MemoryBytes: opts.container.memoryMB * 1024 * 1024, + MemorySwapBytes: opts.container.memorySwapMB * 1024 * 1024, + } + } + + if opts.specFile != "" { + loadSpecFromFile(opts.specFile, containerSpec) + } + req := &pb.ApplyWorkloadRequest{ Id: workloadID, Type: pb.WorkloadType_WORKLOAD_TYPE_CONTAINER, - RevisionId: "rev-1", - DesiredState: pb.DesiredState_DESIRED_STATE_RUNNING, + RevisionId: opts.revisionID, + DesiredState: opts.desired, Spec: &pb.WorkloadSpec{ Spec: &pb.WorkloadSpec_Container{ - Container: &pb.ContainerSpec{ - Image: "nginx:latest", - Ports: []*pb.PortMapping{ - { - HostPort: 8080, - ContainerPort: 80, - Protocol: "tcp", - }, - }, - Env: map[string]string{ - "NGINX_HOST": "localhost", - }, - RestartPolicy: &pb.RestartPolicy{ - Policy: "unless-stopped", - MaxRetryCount: 0, - }, - }, + Container: containerSpec, }, }, } @@ -170,10 +302,11 @@ func applyWorkload(ctx context.Context, client pb.AgentServiceClient, workloadID fmt.Printf(" Message: %s\n", resp.Message) if resp.Status != nil { printStatus(resp.Status) + maybeWaitForTerminalStatus(ctx, client, workloadID, resp.Status, opts.wait, opts.waitTime) } } -func applyComposeWorkload(ctx context.Context, client pb.AgentServiceClient, workloadID string) { +func applyComposeWorkload(ctx context.Context, client pb.AgentServiceClient, workloadID string, waitForResult bool, waitTimeout time.Duration) { // Example docker-compose.yml composeYAML := `version: '3.8' services: @@ -212,70 +345,55 @@ services: fmt.Printf("Compose Workload Applied:\n") fmt.Printf(" Applied: %v\n", resp.Applied) + fmt.Printf(" Skipped: %v\n", resp.Skipped) fmt.Printf(" Message: %s\n", resp.Message) + if resp.Status != nil { + printStatus(resp.Status) + maybeWaitForTerminalStatus(ctx, client, workloadID, resp.Status, waitForResult, waitTimeout) + } } -func applyVMWorkload(ctx context.Context, client pb.AgentServiceClient, workloadID string) { +func applyVMWorkload(ctx context.Context, client pb.AgentServiceClient, workloadID string, opts *applyOptions) { if workloadID == "" { workloadID = "example-vm" } - // Cloud-init script to configure the VM - cloudInitScript := `#!/bin/bash -set -e -echo "Cloud-init starting at $(date)" > /tmp/cloud-init.log -# Update system -apt-get update -apt-get install -y curl wget openssh-server -# Configure hostname -hostnamectl set-hostname ` + workloadID + ` -echo "Cloud-init completed at $(date)" >> /tmp/cloud-init.log -` + cloudInit := opts.vm.cloudInitText + if opts.vm.cloudInitFile != "" { + b, err := os.ReadFile(opts.vm.cloudInitFile) + if err != nil { + log.Fatalf("Failed to read vm cloud-init file: %v", err) + } + cloudInit = string(b) + } + if cloudInit == "" { + cloudInit = fmt.Sprintf("#!/bin/bash\necho 'vm %s initialized' > /tmp/cloud-init.log\n", workloadID) + } + + vmSpec := &pb.VMSpec{ + Name: workloadID, + Vcpus: int32(opts.vm.vcpus), + MemoryMb: opts.vm.memoryMB, + CloudInit: cloudInit, + Metadata: parseKeyValueCSV(opts.vm.metadata), + } + if opts.vm.name != "" { + vmSpec.Name = opts.vm.name + } + vmSpec.Disks = parseVMDiskConfigs(opts.vm.disks, workloadID) + vmSpec.Networks = parseVMNetworkConfigs(opts.vm.networks) + if opts.specFile != "" { + loadSpecFromFile(opts.specFile, vmSpec) + } - // Example: Create a VM workload with ISO boot and cloud-init req := &pb.ApplyWorkloadRequest{ Id: workloadID, Type: pb.WorkloadType_WORKLOAD_TYPE_VM, - RevisionId: "rev-1", - DesiredState: pb.DesiredState_DESIRED_STATE_RUNNING, + RevisionId: opts.revisionID, + DesiredState: opts.desired, Spec: &pb.WorkloadSpec{ Spec: &pb.WorkloadSpec_Vm{ - Vm: &pb.VMSpec{ - Name: workloadID, - Vcpus: 2, - MemoryMb: 2048, - CloudInit: cloudInitScript, - Disks: []*pb.DiskConfig{ - // Boot disk - { - Path: "/var/lib/libvirt/images/" + workloadID + ".qcow2", - Device: "vda", - Format: "qcow2", - SizeGb: 20, - Type: "disk", - Boot: false, - }, - // Installation ISO (optional - uncomment to use) - { - Path: "/home/milx/Desktop/fedora-coreos-42.20250803.3.0-live-iso.x86_64.iso", - Device: "hdc", - Format: "raw", - Type: "cdrom", - Boot: true, - }, - }, - Networks: []*pb.NetworkConfig{ - { - Network: "default", - MacAddress: "52:54:00:12:34:56", - }, - }, - Metadata: map[string]string{ - "environment": "test", - "owner": "demo", - "created-by": "test-client", - }, - }, + Vm: vmSpec, }, }, } @@ -291,6 +409,7 @@ echo "Cloud-init completed at $(date)" >> /tmp/cloud-init.log fmt.Printf(" Message: %s\n", resp.Message) if resp.Status != nil { printStatus(resp.Status) + maybeWaitForTerminalStatus(ctx, client, workloadID, resp.Status, opts.wait, opts.waitTime) } } @@ -309,6 +428,188 @@ func getStatus(ctx context.Context, client pb.AgentServiceClient, workloadID str printStatus(resp.Status) } +func parseDesiredState(s string) pb.DesiredState { + switch strings.ToLower(strings.TrimSpace(s)) { + case "stopped": + return pb.DesiredState_DESIRED_STATE_STOPPED + default: + return pb.DesiredState_DESIRED_STATE_RUNNING + } +} + +func parseCSVList(s string) []string { + if strings.TrimSpace(s) == "" { + return nil + } + parts := strings.Split(s, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + v := strings.TrimSpace(p) + if v != "" { + out = append(out, v) + } + } + return out +} + +func parseKeyValueCSV(s string) map[string]string { + result := map[string]string{} + for _, entry := range parseCSVList(s) { + kv := strings.SplitN(entry, "=", 2) + if len(kv) != 2 { + continue + } + key := strings.TrimSpace(kv[0]) + val := strings.TrimSpace(kv[1]) + if key != "" { + result[key] = val + } + } + return result +} + +func parsePortMappings(s string) []*pb.PortMapping { + var out []*pb.PortMapping + for _, item := range parseCSVList(s) { + parts := strings.SplitN(item, "/", 2) + portPart := parts[0] + proto := "tcp" + if len(parts) == 2 && strings.TrimSpace(parts[1]) != "" { + proto = strings.TrimSpace(parts[1]) + } + + hc := strings.SplitN(portPart, ":", 2) + if len(hc) != 2 { + continue + } + hostPort, err1 := strconv.Atoi(strings.TrimSpace(hc[0])) + containerPort, err2 := strconv.Atoi(strings.TrimSpace(hc[1])) + if err1 != nil || err2 != nil { + continue + } + out = append(out, &pb.PortMapping{ + HostPort: int32(hostPort), + ContainerPort: int32(containerPort), + Protocol: proto, + }) + } + return out +} + +func parseVolumeMounts(s string) []*pb.VolumeMount { + var out []*pb.VolumeMount + for _, item := range parseCSVList(s) { + parts := strings.Split(item, ":") + if len(parts) < 2 { + continue + } + hostPath := strings.TrimSpace(parts[0]) + containerPath := strings.TrimSpace(parts[1]) + readOnly := false + if len(parts) > 2 { + mode := strings.ToLower(strings.TrimSpace(parts[2])) + readOnly = mode == "ro" + } + out = append(out, &pb.VolumeMount{ + HostPath: hostPath, + ContainerPath: containerPath, + ReadOnly: readOnly, + }) + } + return out +} + +func parseVMDiskConfigs(disks string, workloadID string) []*pb.DiskConfig { + if strings.TrimSpace(disks) == "" { + return []*pb.DiskConfig{ + { + Path: "/var/lib/libvirt/images/" + workloadID + ".qcow2", + Device: "vda", + Format: "qcow2", + SizeGb: 20, + Type: "disk", + }, + } + } + + entries := strings.Split(disks, ";") + var out []*pb.DiskConfig + for _, raw := range entries { + raw = strings.TrimSpace(raw) + if raw == "" { + continue + } + kv := parseEntryKV(raw) + sizeGB, _ := strconv.ParseInt(kv["size_gb"], 10, 64) + boot, _ := strconv.ParseBool(kv["boot"]) + if kv["path"] == "" || kv["device"] == "" || kv["format"] == "" || kv["type"] == "" { + continue + } + out = append(out, &pb.DiskConfig{ + Path: kv["path"], + Device: kv["device"], + Format: kv["format"], + SizeGb: sizeGB, + Type: kv["type"], + Boot: boot, + }) + } + return out +} + +func parseVMNetworkConfigs(networks string) []*pb.NetworkConfig { + if strings.TrimSpace(networks) == "" { + return []*pb.NetworkConfig{ + {Network: "default"}, + } + } + + entries := strings.Split(networks, ";") + var out []*pb.NetworkConfig + for _, raw := range entries { + raw = strings.TrimSpace(raw) + if raw == "" { + continue + } + kv := parseEntryKV(raw) + if kv["network"] == "" { + continue + } + out = append(out, &pb.NetworkConfig{ + Network: kv["network"], + MacAddress: kv["mac"], + IpAddress: kv["ip"], + }) + } + return out +} + +func parseEntryKV(s string) map[string]string { + result := map[string]string{} + for _, token := range strings.Split(s, ",") { + kv := strings.SplitN(strings.TrimSpace(token), "=", 2) + if len(kv) != 2 { + continue + } + key := strings.TrimSpace(kv[0]) + val := strings.TrimSpace(kv[1]) + if key != "" { + result[key] = val + } + } + return result +} + +func loadSpecFromFile(path string, target interface{}) { + b, err := os.ReadFile(path) + if err != nil { + log.Fatalf("Failed to read spec file %s: %v", path, err) + } + if err := json.Unmarshal(b, target); err != nil { + log.Fatalf("Failed to parse spec file %s: %v", path, err) + } +} + func listWorkloads(ctx context.Context, client pb.AgentServiceClient) { resp, err := client.ListWorkloads(ctx, &pb.ListWorkloadsRequest{}) if err != nil { @@ -339,6 +640,39 @@ func deleteWorkload(ctx context.Context, client pb.AgentServiceClient, workloadI fmt.Printf(" Message: %s\n", resp.Message) } +func listActions(ctx context.Context, client pb.AgentServiceClient, workloadID, actionType, status string, limit int, newestFirst bool) { + resp, err := client.ListActions(ctx, &pb.ListActionsRequest{ + WorkloadId: workloadID, + ActionType: actionType, + Status: status, + Limit: int32(limit), + NewestFirst: newestFirst, + }) + if err != nil { + log.Fatalf("Failed to list actions: %v", err) + } + + fmt.Printf("Actions (%d total):\n", len(resp.Actions)) + for i, action := range resp.Actions { + fmt.Printf("\n[%d] %s\n", i+1, action.TaskId) + fmt.Printf(" Workload: %s\n", action.WorkloadId) + fmt.Printf(" Type: %s\n", action.ActionType) + fmt.Printf(" Status: %s\n", action.Status) + if action.Error != "" { + fmt.Printf(" Error: %s\n", action.Error) + } + if action.CreatedAt > 0 { + fmt.Printf(" Created: %s\n", time.Unix(action.CreatedAt, 0).Format(time.RFC3339)) + } + if action.StartedAt > 0 { + fmt.Printf(" Started: %s\n", time.Unix(action.StartedAt, 0).Format(time.RFC3339)) + } + if action.EndedAt > 0 { + fmt.Printf(" Ended: %s\n", time.Unix(action.EndedAt, 0).Format(time.RFC3339)) + } + } +} + func printStatus(status *pb.WorkloadStatus) { fmt.Printf(" ID: %s\n", status.Id) fmt.Printf(" Type: %s\n", status.Type) @@ -355,3 +689,50 @@ func printStatus(status *pb.WorkloadStatus) { } } } + +func maybeWaitForTerminalStatus(parentCtx context.Context, client pb.AgentServiceClient, workloadID string, status *pb.WorkloadStatus, waitForResult bool, waitTimeout time.Duration) { + if !waitForResult || status == nil { + return + } + + if status.GetActualState() != pb.ActualState_ACTUAL_STATE_PENDING { + return + } + + fmt.Printf("\nWaiting for workload %s to finish scheduling (timeout: %s)...\n", workloadID, waitTimeout) + if taskID := status.GetMetadata()["task_id"]; taskID != "" { + fmt.Printf(" Task ID: %s\n", taskID) + } + + waitCtx, cancel := context.WithTimeout(parentCtx, waitTimeout) + defer cancel() + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-waitCtx.Done(): + fmt.Printf(" Wait finished without terminal status: %v\n", waitCtx.Err()) + return + case <-ticker.C: + resp, err := client.GetWorkloadStatus(waitCtx, &pb.GetWorkloadStatusRequest{Id: workloadID}) + if err != nil { + fmt.Printf(" Status polling failed: %v\n", err) + return + } + + current := resp.GetStatus() + if current == nil { + fmt.Printf(" Status polling returned empty status\n") + return + } + + if current.ActualState != pb.ActualState_ACTUAL_STATE_PENDING { + fmt.Printf("\nFinal Workload Status:\n") + printStatus(current) + return + } + } + } +} From 145df140deb8f8f565a30bc834132364b89b41ca Mon Sep 17 00:00:00 2001 From: milx Date: Tue, 17 Feb 2026 21:28:41 +0330 Subject: [PATCH 03/25] Chore: Workload Spec-File Samples and how to use them --- examples/client/scripts/encode-compose.sh | 47 +++++ examples/client/scripts/quick-test.sh | 93 ++++++++++ examples/client/specs/README.md | 160 ++++++++++++++++++ examples/client/specs/cloud-init.yaml | 24 +++ examples/client/specs/compose-spec.json | 10 ++ examples/client/specs/compose.yaml | 18 ++ .../client/specs/container-spec-minimal.json | 16 ++ examples/client/specs/container-spec.json | 55 ++++++ examples/client/specs/vm-spec-minimal.json | 22 +++ examples/client/specs/vm-spec.json | 49 ++++++ 10 files changed, 494 insertions(+) create mode 100755 examples/client/scripts/encode-compose.sh create mode 100755 examples/client/scripts/quick-test.sh create mode 100644 examples/client/specs/README.md create mode 100644 examples/client/specs/cloud-init.yaml create mode 100644 examples/client/specs/compose-spec.json create mode 100644 examples/client/specs/compose.yaml create mode 100644 examples/client/specs/container-spec-minimal.json create mode 100644 examples/client/specs/container-spec.json create mode 100644 examples/client/specs/vm-spec-minimal.json create mode 100644 examples/client/specs/vm-spec.json diff --git a/examples/client/scripts/encode-compose.sh b/examples/client/scripts/encode-compose.sh new file mode 100755 index 0000000..0a55652 --- /dev/null +++ b/examples/client/scripts/encode-compose.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: +# ./examples/client/scripts/encode-compose.sh examples/client/specs/compose.yaml +# ./examples/client/scripts/encode-compose.sh examples/client/specs/compose.yaml examples/client/specs/compose-spec.json +# +# Output: +# - Prints base64 compose YAML to stdout. +# - If second arg is provided, updates compose_yaml in the target compose spec JSON. + +if [[ $# -lt 1 || $# -gt 2 ]]; then + echo "Usage: $0 [compose-spec-json-path]" >&2 + exit 1 +fi + +compose_yaml_path="$1" +compose_spec_json_path="${2:-}" + +if [[ ! -f "$compose_yaml_path" ]]; then + echo "Compose YAML not found: $compose_yaml_path" >&2 + exit 1 +fi + +encoded="$(base64 -w 0 "$compose_yaml_path")" +echo "$encoded" + +if [[ -n "$compose_spec_json_path" ]]; then + if [[ ! -f "$compose_spec_json_path" ]]; then + echo "Compose spec JSON not found: $compose_spec_json_path" >&2 + exit 1 + fi + + if command -v jq >/dev/null 2>&1; then + tmp="$(mktemp)" + jq --arg value "$encoded" '.compose_yaml = $value' "$compose_spec_json_path" > "$tmp" + mv "$tmp" "$compose_spec_json_path" + else + # Fallback without jq: replace first compose_yaml string value. + # This expects a standard JSON field like: "compose_yaml": "..." + escaped="$(printf '%s' "$encoded" | sed 's/[\/&]/\\&/g')" + sed -Ei "0,/\"compose_yaml\"[[:space:]]*:[[:space:]]*\"[^\"]*\"/s//\"compose_yaml\": \"$escaped\"/" "$compose_spec_json_path" + fi + + echo "Updated compose_yaml in: $compose_spec_json_path" >&2 +fi + diff --git a/examples/client/scripts/quick-test.sh b/examples/client/scripts/quick-test.sh new file mode 100755 index 0000000..dd2a5fc --- /dev/null +++ b/examples/client/scripts/quick-test.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Quick smoke test for example client + agent. +# - Applies container, compose, and (optionally) VM workloads using minimal specs. +# - Fetches status and action history. +# - Cleans up created workloads. +# +# Usage: +# ./examples/client/scripts/quick-test.sh [--skip-vm] [--client-arg "..."]... +# +# Examples: +# ./examples/client/scripts/quick-test.sh --client-arg -insecure +# ./examples/client/scripts/quick-test.sh --skip-vm --client-arg -insecure +# ./examples/client/scripts/quick-test.sh --client-arg -server --client-arg localhost:50051 --client-arg -insecure + +skip_vm="false" +client_args=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --skip-vm) + skip_vm="true" + shift + ;; + --client-arg) + if [[ $# -lt 2 ]]; then + echo "missing value for --client-arg" >&2 + exit 1 + fi + client_args+=("$2") + shift 2 + ;; + *) + echo "unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +if [[ ${#client_args[@]} -eq 0 ]]; then + client_args=(-insecure) +fi + +timestamp="$(date +%s)" +container_id="quick-container-${timestamp}" +compose_id="quick-compose-${timestamp}" +vm_id="quick-vm-${timestamp}" + +cleanup_ids=("$container_id" "$compose_id") +if [[ "$skip_vm" != "true" ]]; then + cleanup_ids+=("$vm_id") +fi + +cleanup() { + set +e + for wid in "${cleanup_ids[@]}"; do + go run ./examples/client -action delete -id "$wid" "${client_args[@]}" >/dev/null 2>&1 || true + done +} +trap cleanup EXIT + +run_client() { + go run ./examples/client "$@" "${client_args[@]}" +} + +echo "==> Health check" +run_client -action health + +echo "==> Apply container: $container_id" +run_client -action apply -type container -id "$container_id" -spec-file ./examples/client/specs/container-spec-minimal.json -wait-timeout 90s +run_client -action status -id "$container_id" +run_client -action list-actions -id "$container_id" + +echo "==> Apply compose: $compose_id" +run_client -action apply -type compose -id "$compose_id" -spec-file ./examples/client/specs/compose-spec-minimal.json -wait-timeout 90s +run_client -action status -id "$compose_id" +run_client -action list-actions -id "$compose_id" + +if [[ "$skip_vm" != "true" ]]; then + echo "==> Apply VM: $vm_id" + run_client -action apply -type vm -id "$vm_id" -spec-file ./examples/client/specs/vm-spec-minimal.json -wait-timeout 240s + run_client -action status -id "$vm_id" + run_client -action list-actions -id "$vm_id" +fi + +echo "==> Cleanup" +for wid in "${cleanup_ids[@]}"; do + run_client -action delete -id "$wid" +done + +echo "Quick test completed successfully." + diff --git a/examples/client/specs/README.md b/examples/client/specs/README.md new file mode 100644 index 0000000..906f9a1 --- /dev/null +++ b/examples/client/specs/README.md @@ -0,0 +1,160 @@ +# Example Client Specs + +These files are designed for `examples/client/main.go` and showcase all major fields for every workload type. + +## Included files + +Full specs: + +- `container-spec.json` +- `compose-spec.json` +- `vm-spec.json` + +Minimal specs: + +- `container-spec-minimal.json` +- `compose-spec-minimal.json` +- `vm-spec-minimal.json` + +Raw helper files: + +- `compose.yaml` +- `compose-minimal.yaml` +- `cloud-init.yaml` +- `cloud-init-minimal.yaml` + +Scripts: + +- `../scripts/encode-compose.sh` +- `../scripts/quick-test.sh` + +## How to run with full specs + +Container: + +```bash +go run ./examples/client \ + -insecure \ + -action apply \ + -type container \ + -id demo-container \ + -spec-file ./examples/client/specs/container-spec.json +``` + +Compose: + +```bash +go run ./examples/client \ + -insecure \ + -action apply \ + -type compose \ + -id demo-compose \ + -spec-file ./examples/client/specs/compose-spec.json +``` + +VM: + +```bash +go run ./examples/client \ + -insecure \ + -action apply \ + -type vm \ + -id demo-vm \ + -spec-file ./examples/client/specs/vm-spec.json +``` + +## How to run with minimal specs + +Container: + +```bash +go run ./examples/client \ + -insecure \ + -action apply \ + -type container \ + -id demo-container-min \ + -spec-file ./examples/client/specs/container-spec-minimal.json +``` + +Compose: + +```bash +go run ./examples/client \ + -insecure \ + -action apply \ + -type compose \ + -id demo-compose-min \ + -spec-file ./examples/client/specs/compose-spec-minimal.json +``` + +VM: + +```bash +go run ./examples/client \ + -insecure \ + -action apply \ + -type vm \ + -id demo-vm-min \ + -spec-file ./examples/client/specs/vm-spec-minimal.json +``` + +VM with raw cloud-init file: + +```bash +go run ./examples/client \ + -insecure \ + -action apply \ + -type vm \ + -id demo-vm \ + -vm-cloud-init-file ./examples/client/specs/cloud-init.yaml +``` + +## Compose base64 helper + +```bash +# Print base64 to stdout +./examples/client/scripts/encode-compose.sh ./examples/client/specs/compose.yaml + +# Update compose-spec.json in place +./examples/client/scripts/encode-compose.sh \ + ./examples/client/specs/compose.yaml \ + ./examples/client/specs/compose-spec.json + +# Update minimal compose spec from minimal yaml +./examples/client/scripts/encode-compose.sh \ + ./examples/client/specs/compose-minimal.yaml \ + ./examples/client/specs/compose-spec-minimal.json +``` + +## Quick smoke test + +Run container + compose + VM quick cycle: + +```bash +./examples/client/scripts/quick-test.sh --client-arg -insecure +``` + +Skip VM in environments without libvirt/KVM: + +```bash +./examples/client/scripts/quick-test.sh --skip-vm --client-arg -insecure +``` + +Pass custom client connection args (repeat `--client-arg`): + +```bash +./examples/client/scripts/quick-test.sh \ + --skip-vm \ + --client-arg -server \ + --client-arg localhost:50051 \ + --client-arg -insecure +``` + +## Notes + +- `-spec-file` is type-specific and should contain the spec object only: + - `container-spec*.json` maps to `ContainerSpec` + - `compose-spec*.json` maps to `ComposeSpec` + - `vm-spec*.json` maps to `VMSpec` +- The client still uses `-revision` and `-desired-state` from CLI flags for the top-level request. +- For compose, `compose_yaml` must be base64-encoded YAML content. diff --git a/examples/client/specs/cloud-init.yaml b/examples/client/specs/cloud-init.yaml new file mode 100644 index 0000000..68eb2cd --- /dev/null +++ b/examples/client/specs/cloud-init.yaml @@ -0,0 +1,24 @@ +#cloud-config +hostname: demo-vm +manage_etc_hosts: true + +users: + - name: core + groups: [wheel] + sudo: ["ALL=(ALL) NOPASSWD:ALL"] + +packages: + - qemu-guest-agent + - curl + - wget + +runcmd: + - systemctl enable --now qemu-guest-agent + - echo "cloud-init completed" > /tmp/cloud-init.done + +write_files: + - path: /etc/motd + permissions: "0644" + content: | + Managed by Persys test client + diff --git a/examples/client/specs/compose-spec.json b/examples/client/specs/compose-spec.json new file mode 100644 index 0000000..66956dd --- /dev/null +++ b/examples/client/specs/compose-spec.json @@ -0,0 +1,10 @@ +{ + "project_name": "demo-compose", + "compose_yaml": "dmVyc2lvbjogIjMuOCIKc2VydmljZXM6CiAgd2ViOgogICAgaW1hZ2U6IG5naW54OjEuMjcKICAgIHBvcnRzOgogICAgICAtICI4MDgwOjgwIgogICAgZW52aXJvbm1lbnQ6CiAgICAgIE5HSU5YX0hPU1Q6IGxvY2FsaG9zdAogICAgICBOR0lOWF9QT1JUOiA4MAogICAgdm9sdW1lczoKICAgICAgLSAvdG1wL25naW54LWRhdGE6L3Vzci9zaGFyZS9uZ2lueC9odG1sCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogIHJlZGlzOgogICAgaW1hZ2U6IHJlZGlzOjctYWxwaW5lCiAgICBjb21tYW5kOiBbInJlZGlzLXNlcnZlciIsICItLWFwcGVuZG9ubHkiLCAieWVzIl0KICAgIHZvbHVtZXM6CiAgICAgIC0gL3RtcC9yZWRpcy1kYXRhOi9kYXRhCg==", + "env": { + "COMPOSE_PROJECT_NAME": "demo-compose", + "WEB_PORT": "8080", + "REDIS_APPENDONLY": "yes" + } +} + diff --git a/examples/client/specs/compose.yaml b/examples/client/specs/compose.yaml new file mode 100644 index 0000000..03df720 --- /dev/null +++ b/examples/client/specs/compose.yaml @@ -0,0 +1,18 @@ +version: "3.8" +services: + web: + image: nginx:1.27 + ports: + - "8080:80" + environment: + NGINX_HOST: localhost + NGINX_PORT: 80 + volumes: + - /tmp/nginx-data:/usr/share/nginx/html + restart: unless-stopped + redis: + image: redis:7-alpine + command: ["redis-server", "--appendonly", "yes"] + volumes: + - /tmp/redis-data:/data + diff --git a/examples/client/specs/container-spec-minimal.json b/examples/client/specs/container-spec-minimal.json new file mode 100644 index 0000000..1e7fcf5 --- /dev/null +++ b/examples/client/specs/container-spec-minimal.json @@ -0,0 +1,16 @@ +{ + "image": "busybox:1.36", + "command": [ + "sh", + "-c", + "echo container-ok && sleep 300" + ], + "ports": [ + { + "host_port": 18080, + "container_port": 8080, + "protocol": "tcp" + } + ] +} + diff --git a/examples/client/specs/container-spec.json b/examples/client/specs/container-spec.json new file mode 100644 index 0000000..229208d --- /dev/null +++ b/examples/client/specs/container-spec.json @@ -0,0 +1,55 @@ +{ + "image": "nginx:1.27", + "command": [ + "/docker-entrypoint.sh" + ], + "args": [ + "nginx", + "-g", + "daemon off;" + ], + "env": { + "NGINX_HOST": "localhost", + "NGINX_PORT": "80", + "APP_ENV": "staging" + }, + "volumes": [ + { + "host_path": "/tmp/nginx-data", + "container_path": "/usr/share/nginx/html", + "read_only": false + }, + { + "host_path": "/tmp/nginx-config", + "container_path": "/etc/nginx/conf.d", + "read_only": true + } + ], + "ports": [ + { + "host_port": 8080, + "container_port": 80, + "protocol": "tcp" + }, + { + "host_port": 8443, + "container_port": 443, + "protocol": "tcp" + } + ], + "resources": { + "cpu_shares": 512, + "memory_bytes": 536870912, + "memory_swap_bytes": 1073741824 + }, + "restart_policy": { + "policy": "unless-stopped", + "max_retry_count": 0 + }, + "labels": { + "app": "demo-nginx", + "environment": "staging", + "managed-by": "persys-test-client" + } +} + diff --git a/examples/client/specs/vm-spec-minimal.json b/examples/client/specs/vm-spec-minimal.json new file mode 100644 index 0000000..54fe714 --- /dev/null +++ b/examples/client/specs/vm-spec-minimal.json @@ -0,0 +1,22 @@ +{ + "name": "demo-vm-minimal", + "vcpus": 1, + "memory_mb": 1024, + "disks": [ + { + "path": "/var/lib/libvirt/images/demo-vm-minimal.qcow2", + "device": "vda", + "format": "qcow2", + "size_gb": 10, + "type": "disk", + "boot": false + } + ], + "networks": [ + { + "network": "default" + } + ], + "cloud_init": "#cloud-config\nhostname: demo-vm-minimal\nruncmd:\n - echo vm-minimal > /tmp/cloud-init.done\n" +} + diff --git a/examples/client/specs/vm-spec.json b/examples/client/specs/vm-spec.json new file mode 100644 index 0000000..14ddfa4 --- /dev/null +++ b/examples/client/specs/vm-spec.json @@ -0,0 +1,49 @@ +{ + "name": "demo-vm", + "vcpus": 2, + "memory_mb": 4096, + "disks": [ + { + "path": "/var/lib/libvirt/images/demo-vm.qcow2", + "device": "vda", + "format": "qcow2", + "size_gb": 30, + "type": "disk", + "boot": false + }, + { + "path": "/var/lib/libvirt/images/fedora-coreos-live.iso", + "device": "hdc", + "format": "raw", + "size_gb": 0, + "type": "cdrom", + "boot": true + } + ], + "networks": [ + { + "network": "default", + "mac_address": "52:54:00:12:34:56", + "ip_address": "" + }, + { + "network": "default", + "mac_address": "52:54:00:12:34:57", + "ip_address": "" + } + ], + "cloud_init": "#cloud-config\nhostname: demo-vm\nmanage_etc_hosts: true\nusers:\n - name: core\n groups: [wheel]\n sudo: [\"ALL=(ALL) NOPASSWD:ALL\"]\npackages:\n - qemu-guest-agent\nruncmd:\n - systemctl enable --now qemu-guest-agent\n - echo \"cloud-init done\" > /tmp/cloud-init.done\n", + "metadata": { + "environment": "staging", + "owner": "platform-team", + "purpose": "integration-test", + "managed-by": "persys-test-client" + }, + "cloud_init_config": { + "user_data": "#cloud-config\nhostname: demo-vm-alt\nusers:\n - name: core\n", + "meta_data": "{\"instance-id\":\"demo-vm-instance\",\"local-hostname\":\"demo-vm\"}", + "network_config": "version: 2\nethernets:\n eth0:\n dhcp4: true\n", + "vendor_data": "#cloud-config\nwrite_files:\n - path: /etc/motd\n content: |\n Managed by Persys\n" + } +} + From 940d7e064af5ef6f1336e8d47e55ee58481295b6 Mon Sep 17 00:00:00 2001 From: milx Date: Tue, 17 Feb 2026 21:29:39 +0330 Subject: [PATCH 04/25] Feat: Add Action List Endpoint + Workload Tracking --- internal/grpc/server.go | 80 +++++++++++++++++++++++++++++++++--- internal/grpc/server_test.go | 42 +++++++++++++++++++ 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/internal/grpc/server.go b/internal/grpc/server.go index 9bd10dd..9fd8145 100644 --- a/internal/grpc/server.go +++ b/internal/grpc/server.go @@ -8,6 +8,8 @@ import ( "fmt" "net" "os" + "sort" + "strings" "time" "github.com/persys/compute-agent/internal/config" @@ -148,8 +150,9 @@ func (s *Server) ApplyWorkload(ctx context.Context, req *pb.ApplyWorkloadRequest if s.taskQueue != nil { taskID := fmt.Sprintf("apply-%s-%d", req.Id, time.Now().UnixNano()) t := &task.Task{ - ID: taskID, - Type: task.TaskTypeApplyWorkload, + ID: taskID, + WorkloadID: req.Id, + Type: task.TaskTypeApplyWorkload, } // Store workload in task for handler to access @@ -169,6 +172,10 @@ func (s *Server) ApplyWorkload(ctx context.Context, req *pb.ApplyWorkloadRequest ActualState: pb.ActualState_ACTUAL_STATE_PENDING, Message: "task pending execution", UpdatedAt: time.Now().Unix(), + Metadata: map[string]string{ + "task_id": taskID, + "task_status": string(task.TaskStatusPending), + }, }, }, nil } @@ -203,9 +210,10 @@ func (s *Server) DeleteWorkload(ctx context.Context, req *pb.DeleteWorkloadReque if s.taskQueue != nil { taskID := fmt.Sprintf("delete-%s-%d", req.Id, time.Now().UnixNano()) t := &task.Task{ - ID: taskID, - Type: task.TaskTypeDeleteWorkload, - Result: req.Id, + ID: taskID, + WorkloadID: req.Id, + Type: task.TaskTypeDeleteWorkload, + Result: req.Id, } err := s.taskQueue.Submit(t) @@ -309,6 +317,68 @@ func (s *Server) HealthCheck(ctx context.Context, req *pb.HealthCheckRequest) (* }, nil } +// ListActions returns task/action history tracked since agent startup. +func (s *Server) ListActions(ctx context.Context, req *pb.ListActionsRequest) (*pb.ListActionsResponse, error) { + s.logClientInfo(ctx, "ListActions", req.GetWorkloadId()) + + if s.taskQueue == nil { + return &pb.ListActionsResponse{Actions: []*pb.AgentAction{}}, nil + } + + snapshots := s.taskQueue.ListTaskSnapshots("") + + workloadFilter := strings.TrimSpace(req.GetWorkloadId()) + typeFilter := strings.TrimSpace(req.GetActionType()) + statusFilter := strings.TrimSpace(req.GetStatus()) + newestFirst := true + if !req.GetNewestFirst() { + newestFirst = false + } + + filtered := make([]task.TaskSnapshot, 0, len(snapshots)) + for _, item := range snapshots { + if workloadFilter != "" && item.WorkloadID != workloadFilter { + continue + } + if typeFilter != "" && string(item.Type) != typeFilter { + continue + } + if statusFilter != "" && string(item.Status) != statusFilter { + continue + } + filtered = append(filtered, item) + } + + sort.Slice(filtered, func(i, j int) bool { + ti := filtered[i].CreatedAt + tj := filtered[j].CreatedAt + if newestFirst { + return ti.After(tj) + } + return ti.Before(tj) + }) + + if req.GetLimit() > 0 && int(req.GetLimit()) < len(filtered) { + filtered = filtered[:req.GetLimit()] + } + + actions := make([]*pb.AgentAction, 0, len(filtered)) + for _, item := range filtered { + actions = append(actions, &pb.AgentAction{ + TaskId: item.ID, + WorkloadId: item.WorkloadID, + ActionType: string(item.Type), + Status: string(item.Status), + Error: item.Error, + CreatedAt: item.CreatedAt.Unix(), + StartedAt: item.StartedAt.Unix(), + EndedAt: item.EndedAt.Unix(), + }) + } + + return &pb.ListActionsResponse{Actions: actions}, nil +} + // Helper conversion functions func (s *Server) protoToWorkload(req *pb.ApplyWorkloadRequest) (*models.Workload, error) { diff --git a/internal/grpc/server_test.go b/internal/grpc/server_test.go index b079ce3..ffcefae 100644 --- a/internal/grpc/server_test.go +++ b/internal/grpc/server_test.go @@ -1,10 +1,14 @@ package grpc import ( + "context" "testing" "time" + "github.com/persys/compute-agent/internal/task" + pb "github.com/persys/compute-agent/pkg/api/v1" "github.com/persys/compute-agent/pkg/models" + "github.com/sirupsen/logrus" ) func TestStatusToProto_MetadataPreserved(t *testing.T) { @@ -27,3 +31,41 @@ func TestStatusToProto_MetadataPreserved(t *testing.T) { t.Fatal("expected metadata to be preserved") } } + +func TestListActions_FiltersByWorkloadID(t *testing.T) { + q := task.NewQueue(2, logrus.New()) + if err := q.Submit(&task.Task{ + ID: "apply-w1-1", + WorkloadID: "w1", + Type: task.TaskTypeApplyWorkload, + }); err != nil { + t.Fatalf("failed to submit task 1: %v", err) + } + if err := q.Submit(&task.Task{ + ID: "delete-w2-1", + WorkloadID: "w2", + Type: task.TaskTypeDeleteWorkload, + }); err != nil { + t.Fatalf("failed to submit task 2: %v", err) + } + + s := &Server{taskQueue: q} + resp, err := s.ListActions(context.Background(), &pb.ListActionsRequest{ + WorkloadId: "w1", + }) + if err != nil { + t.Fatalf("ListActions failed: %v", err) + } + if len(resp.Actions) != 1 { + t.Fatalf("expected 1 action, got %d", len(resp.Actions)) + } + if resp.Actions[0].TaskId != "apply-w1-1" { + t.Fatalf("unexpected task id: %s", resp.Actions[0].TaskId) + } + if resp.Actions[0].WorkloadId != "w1" { + t.Fatalf("unexpected workload id: %s", resp.Actions[0].WorkloadId) + } + if resp.Actions[0].ActionType != string(task.TaskTypeApplyWorkload) { + t.Fatalf("unexpected action type: %s", resp.Actions[0].ActionType) + } +} From cbd11a8b1d8fd1482456205acc44ab3d6647388f Mon Sep 17 00:00:00 2001 From: milx Date: Tue, 17 Feb 2026 21:30:44 +0330 Subject: [PATCH 05/25] Feat: Cleanup vm Disk After Workload Deletion --- internal/runtime/vm.go | 106 +++++++++++++++++++++++++++- internal/runtime/vm_cleanup_test.go | 74 +++++++++++++++++++ 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 internal/runtime/vm_cleanup_test.go diff --git a/internal/runtime/vm.go b/internal/runtime/vm.go index e65b43e..eea14b0 100644 --- a/internal/runtime/vm.go +++ b/internal/runtime/vm.go @@ -10,12 +10,15 @@ import ( "os/exec" "path/filepath" "strings" + "time" "github.com/digitalocean/go-libvirt" "github.com/persys/compute-agent/pkg/models" "github.com/sirupsen/logrus" ) +const managedDiskMarkerSuffix = ".persys-managed" + // VMRuntime manages KVM virtual machine workloads via libvirt type VMRuntime struct { conn *libvirt.Libvirt @@ -145,10 +148,24 @@ func (v *VMRuntime) Stop(ctx context.Context, id string) error { func (v *VMRuntime) Delete(ctx context.Context, id string) error { domain, err := v.conn.DomainLookupByName(id) if err != nil { - // Already deleted + // Domain may already be gone, but cleanup deterministic cloud-init artifacts. + v.cleanupDeterministicVMArtifacts(id) return nil } + diskPaths := []string{} + domainXML, xmlErr := v.conn.DomainGetXMLDesc(domain, libvirt.DomainXMLFlags(0)) + if xmlErr != nil { + v.logger.Warnf("Failed to fetch domain XML for %s during delete: %v", id, xmlErr) + } else { + paths, parseErr := parseDiskSourceFilePaths(domainXML) + if parseErr != nil { + v.logger.Warnf("Failed to parse domain XML for %s during delete: %v", id, parseErr) + } else { + diskPaths = paths + } + } + // Destroy if running state, _, err := v.conn.DomainGetState(domain, 0) if err == nil && state == int32(libvirt.DomainRunning) { @@ -160,6 +177,9 @@ func (v *VMRuntime) Delete(ctx context.Context, id string) error { return fmt.Errorf("failed to undefine domain: %w", err) } + v.cleanupDiskArtifacts(id, diskPaths) + v.cleanupDeterministicVMArtifacts(id) + v.logger.Infof("Deleted VM: %s", id) return nil } @@ -331,6 +351,11 @@ func (v *VMRuntime) createDisk(diskCfg *models.DiskConfig) error { } v.logger.Infof("Created QCOW2 disk: %s (%dGB)", diskCfg.Path, diskCfg.SizeGB) + + if err := os.WriteFile(managedDiskMarkerPath(diskCfg.Path), []byte(time.Now().Format(time.RFC3339)), 0644); err != nil { + v.logger.Warnf("Failed to create managed-disk marker for %s: %v", diskCfg.Path, err) + } + return nil } @@ -406,6 +431,85 @@ func (v *VMRuntime) createCloudInitISO(vmID string, spec *models.VMSpec) (string return isoPath, nil } +func managedDiskMarkerPath(diskPath string) string { + return diskPath + managedDiskMarkerSuffix +} + +func expectedCloudInitISOPaths(vmID string) []string { + return []string{ + filepath.Join("/var/lib/libvirt/images", fmt.Sprintf("%s-cloud-init.iso", vmID)), + filepath.Join("/tmp", fmt.Sprintf("%s-cloud-init.iso", vmID)), + } +} + +func isCloudInitISOForVM(vmID, path string) bool { + return filepath.Base(path) == fmt.Sprintf("%s-cloud-init.iso", vmID) +} + +func parseDiskSourceFilePaths(domainXML string) ([]string, error) { + type domainDiskSource struct { + File string `xml:"file,attr"` + } + type domainDisk struct { + Type string `xml:"type,attr"` + Source domainDiskSource `xml:"source"` + } + type domainDevices struct { + Disks []domainDisk `xml:"disk"` + } + type domainForCleanup struct { + Devices domainDevices `xml:"devices"` + } + + var parsed domainForCleanup + if err := xml.Unmarshal([]byte(domainXML), &parsed); err != nil { + return nil, fmt.Errorf("failed to unmarshal domain xml: %w", err) + } + + var paths []string + for _, disk := range parsed.Devices.Disks { + if disk.Type != "file" || disk.Source.File == "" { + continue + } + paths = append(paths, disk.Source.File) + } + return paths, nil +} + +func (v *VMRuntime) cleanupDeterministicVMArtifacts(vmID string) { + for _, path := range expectedCloudInitISOPaths(vmID) { + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + v.logger.Warnf("Failed to remove cloud-init ISO %s: %v", path, err) + } + } +} + +func (v *VMRuntime) cleanupDiskArtifacts(vmID string, diskPaths []string) { + for _, diskPath := range diskPaths { + remove := false + if isCloudInitISOForVM(vmID, diskPath) { + remove = true + } else { + if _, err := os.Stat(managedDiskMarkerPath(diskPath)); err == nil { + remove = true + } + } + + if !remove { + continue + } + + if err := os.Remove(diskPath); err != nil && !os.IsNotExist(err) { + v.logger.Warnf("Failed to remove managed VM disk %s: %v", diskPath, err) + continue + } + + if err := os.Remove(managedDiskMarkerPath(diskPath)); err != nil && !os.IsNotExist(err) { + v.logger.Warnf("Failed to remove managed-disk marker for %s: %v", diskPath, err) + } + } +} + // Helper functions type Domain struct { XMLName xml.Name `xml:"domain"` diff --git a/internal/runtime/vm_cleanup_test.go b/internal/runtime/vm_cleanup_test.go new file mode 100644 index 0000000..58f4e6b --- /dev/null +++ b/internal/runtime/vm_cleanup_test.go @@ -0,0 +1,74 @@ +package runtime + +import ( + "os" + "path/filepath" + "testing" + + "github.com/sirupsen/logrus" +) + +func TestParseDiskSourceFilePaths(t *testing.T) { + xmlData := ` + + + + + + + + + + + + +` + + paths, err := parseDiskSourceFilePaths(xmlData) + if err != nil { + t.Fatalf("unexpected parse error: %v", err) + } + if len(paths) != 2 { + t.Fatalf("expected 2 file-backed disk paths, got %d", len(paths)) + } + if paths[0] != "/var/lib/libvirt/images/vm-disk.qcow2" { + t.Fatalf("unexpected first path: %s", paths[0]) + } + if paths[1] != "/var/lib/libvirt/images/vm-cloud-init.iso" { + t.Fatalf("unexpected second path: %s", paths[1]) + } +} + +func TestCleanupDiskArtifacts_RemovesOnlyManagedAndCloudInit(t *testing.T) { + tmpDir := t.TempDir() + vmID := "vm-123" + + managedDisk := filepath.Join(tmpDir, "managed.qcow2") + externalDisk := filepath.Join(tmpDir, "external.qcow2") + cloudInitISO := filepath.Join(tmpDir, vmID+"-cloud-init.iso") + + for _, p := range []string{managedDisk, externalDisk, cloudInitISO} { + if err := os.WriteFile(p, []byte("x"), 0644); err != nil { + t.Fatalf("failed to create test file %s: %v", p, err) + } + } + if err := os.WriteFile(managedDiskMarkerPath(managedDisk), []byte("owned"), 0644); err != nil { + t.Fatalf("failed to create marker: %v", err) + } + + rt := &VMRuntime{logger: logrus.New().WithField("runtime", "vm-test")} + rt.cleanupDiskArtifacts(vmID, []string{managedDisk, externalDisk, cloudInitISO}) + + if _, err := os.Stat(managedDisk); !os.IsNotExist(err) { + t.Fatalf("expected managed disk to be removed, stat err=%v", err) + } + if _, err := os.Stat(managedDiskMarkerPath(managedDisk)); !os.IsNotExist(err) { + t.Fatalf("expected managed marker to be removed, stat err=%v", err) + } + if _, err := os.Stat(cloudInitISO); !os.IsNotExist(err) { + t.Fatalf("expected cloud-init ISO to be removed, stat err=%v", err) + } + if _, err := os.Stat(externalDisk); err != nil { + t.Fatalf("expected external disk to remain, stat err=%v", err) + } +} From bf655ed4ebd0017250dbe038876276f4b9782a67 Mon Sep 17 00:00:00 2001 From: milx Date: Tue, 17 Feb 2026 21:31:41 +0330 Subject: [PATCH 06/25] Update: Better Task Tracking + List Actions --- internal/task/queue.go | 63 +++++++++++++++++++++++++++++++------ internal/task/queue_test.go | 20 ++++++++++++ 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/internal/task/queue.go b/internal/task/queue.go index 7482dca..f844f28 100644 --- a/internal/task/queue.go +++ b/internal/task/queue.go @@ -30,15 +30,28 @@ const ( // Task represents an async operation type Task struct { - ID string - Type TaskType - Status TaskStatus - Error string - Result interface{} - CreatedAt time.Time - StartedAt time.Time - EndedAt time.Time - mu sync.RWMutex + ID string + WorkloadID string + Type TaskType + Status TaskStatus + Error string + Result interface{} + CreatedAt time.Time + StartedAt time.Time + EndedAt time.Time + mu sync.RWMutex +} + +// TaskSnapshot is an immutable view of a task for external consumers. +type TaskSnapshot struct { + ID string + WorkloadID string + Type TaskType + Status TaskStatus + Error string + CreatedAt time.Time + StartedAt time.Time + EndedAt time.Time } // Handler is a function that executes a task @@ -180,8 +193,14 @@ func (q *Queue) Submit(task *Task) error { q.logger.Debugf("Task %s submitted (type: %s)", task.ID, task.Type) return nil case <-q.stopCh: + q.mu.Lock() + delete(q.tasks, task.ID) + q.mu.Unlock() return fmt.Errorf("task queue is stopping") default: + q.mu.Lock() + delete(q.tasks, task.ID) + q.mu.Unlock() return fmt.Errorf("task queue is full, try again later") } } @@ -254,6 +273,32 @@ func (q *Queue) ListTasks(status TaskStatus) []*Task { return result } +// ListTaskSnapshots returns immutable task snapshots matching a status (or all if status is empty). +func (q *Queue) ListTaskSnapshots(status TaskStatus) []TaskSnapshot { + q.mu.RLock() + defer q.mu.RUnlock() + + var result []TaskSnapshot + for _, task := range q.tasks { + task.mu.RLock() + if status == "" || task.Status == status { + result = append(result, TaskSnapshot{ + ID: task.ID, + WorkloadID: task.WorkloadID, + Type: task.Type, + Status: task.Status, + Error: task.Error, + CreatedAt: task.CreatedAt, + StartedAt: task.StartedAt, + EndedAt: task.EndedAt, + }) + } + task.mu.RUnlock() + } + + return result +} + // CleanupOldTasks removes completed/failed tasks older than ttl func (q *Queue) CleanupOldTasks(ttl time.Duration) int { q.mu.Lock() diff --git a/internal/task/queue_test.go b/internal/task/queue_test.go index bcf8828..19f9b95 100644 --- a/internal/task/queue_test.go +++ b/internal/task/queue_test.go @@ -32,3 +32,23 @@ func TestQueue_SubmitAndCompleteTask(t *testing.T) { t.Fatalf("expected completed task, got %s", result.Status) } } + +func TestQueue_Submit_FullQueueDoesNotLeaveGhostTask(t *testing.T) { + q := NewQueue(1, logrus.New()) + + if err := q.Submit(&Task{ID: "t1", Type: TaskTypeApplyWorkload}); err != nil { + t.Fatalf("submit t1 failed: %v", err) + } + if err := q.Submit(&Task{ID: "t2", Type: TaskTypeApplyWorkload}); err != nil { + t.Fatalf("submit t2 failed: %v", err) + } + + err := q.Submit(&Task{ID: "t3", Type: TaskTypeApplyWorkload}) + if err == nil { + t.Fatal("expected submit to fail when queue is full") + } + + if got := q.GetTask("t3"); got != nil { + t.Fatal("expected failed submission task to be removed from task map") + } +} From 49dcfc93e012cb0687d852f19061f9fbec846955 Mon Sep 17 00:00:00 2001 From: milx Date: Tue, 17 Feb 2026 21:32:36 +0330 Subject: [PATCH 07/25] Feat: Add Failed Workload Tracking and management to avoid phantom workload situation --- internal/workload/manager.go | 140 ++++++++++++++++++++++-------- internal/workload/manager_test.go | 108 ++++++++++++++++++++++- 2 files changed, 208 insertions(+), 40 deletions(-) diff --git a/internal/workload/manager.go b/internal/workload/manager.go index c0968f7..52d35b5 100644 --- a/internal/workload/manager.go +++ b/internal/workload/manager.go @@ -4,6 +4,7 @@ import ( "context" stdErrors "errors" "fmt" + "strings" "sync" "time" @@ -84,6 +85,42 @@ func ensureMetadata(status *models.WorkloadStatus) { } } +func isRuntimeMissing(message string) bool { + msg := strings.ToLower(message) + return strings.Contains(msg, "not found") || + strings.Contains(msg, "no such") || + strings.Contains(msg, "does not exist") +} + +func (m *Manager) persistFailedStatus(workload *models.Workload, message string, err error) { + failedStatus, statusErr := m.store.GetStatus(workload.ID) + if statusErr != nil || failedStatus == nil { + failedStatus = &models.WorkloadStatus{ + ID: workload.ID, + Type: workload.Type, + RevisionID: workload.RevisionID, + DesiredState: workload.DesiredState, + CreatedAt: time.Now(), + } + } + + failedStatus.Type = workload.Type + failedStatus.RevisionID = workload.RevisionID + failedStatus.DesiredState = workload.DesiredState + failedStatus.ActualState = models.ActualStateFailed + failedStatus.Message = message + failedStatus.UpdatedAt = time.Now() + ensureMetadata(failedStatus) + failedStatus.Metadata["last_failure_at"] = time.Now().Format(time.RFC3339) + if err != nil { + failedStatus.Metadata["last_error"] = err.Error() + } + + if saveErr := m.store.SaveStatus(failedStatus); saveErr != nil { + m.logger.Warnf("Failed to persist failed status for %s: %v", workload.ID, saveErr) + } +} + func (m *Manager) enrichStatusWithRuntimeMetadata(ctx context.Context, rt runtime.Runtime, workloadID string, status *models.WorkloadStatus) { provider, ok := rt.(runtime.StatusMetadataProvider) if !ok || status == nil { @@ -149,6 +186,16 @@ func (m *Manager) ApplyWorkload(ctx context.Context, workload *models.Workload) m.metrics.RecordError(string(errors.ErrCodeResourceQuotaExceeded)) } + if err := m.store.SaveWorkload(workload); err != nil { + m.logger.Warnf("Failed to persist resource-constrained workload %s: %v", workload.ID, err) + } + + m.persistFailedStatus( + workload, + fmt.Sprintf("admission rejected due to resource constraints: %v", issues), + nil, + ) + workloadErr := errors.NewWorkloadError( errors.ErrCodeResourceQuotaExceeded, "Insufficient system resources to admit workload", @@ -197,18 +244,14 @@ func (m *Manager) ApplyWorkload(ctx context.Context, workload *models.Workload) // Create the workload in the runtime startTime := time.Now() if err := rt.Create(ctx, workload); err != nil { - // On creation failure, remove the workload from state to prevent ghost workloads - m.logger.Errorf("Failed to create workload %s: %v, removing from state", workload.ID, err) + m.logger.Errorf("Failed to create workload %s: %v", workload.ID, err) // Record failed workload metric if m.metrics != nil { m.metrics.RecordWorkloadFailed() } - // Best effort deletion from state - if delErr := m.store.DeleteWorkload(workload.ID); delErr != nil { - m.logger.Warnf("Failed to clean up failed workload %s from state: %v", workload.ID, delErr) - } + m.persistFailedStatus(workload, fmt.Sprintf("create failed: %v", err), err) // Create detailed error workloadErr := errors.NewWorkloadError( @@ -232,16 +275,13 @@ func (m *Manager) ApplyWorkload(ctx context.Context, workload *models.Workload) // Start if desired state is running if workload.DesiredState == models.DesiredStateRunning { if err := rt.Start(ctx, workload.ID); err != nil { - // On start failure, also remove the workload (delete what we created) - m.logger.Errorf("Failed to start workload %s: %v, cleaning up and removing from state", workload.ID, err) + m.logger.Errorf("Failed to start workload %s: %v, leaving desired state for reconciliation", workload.ID, err) if delErr := rt.Delete(ctx, workload.ID); delErr != nil { m.logger.Warnf("Failed to delete workload during cleanup: %v", delErr) } - if delErr := m.store.DeleteWorkload(workload.ID); delErr != nil { - m.logger.Warnf("Failed to clean up failed workload %s from state: %v", workload.ID, delErr) - } + m.persistFailedStatus(workload, fmt.Sprintf("start failed: %v", err), err) workloadErr := errors.NewWorkloadError( errors.ErrCodeStartFailed, @@ -282,6 +322,8 @@ func (m *Manager) ApplyWorkload(ctx context.Context, workload *models.Workload) return status, false, fmt.Errorf("failed to save status: %w", err) } + m.resetRetryTracker(workload.ID) + // Record successful workload creation if m.metrics != nil { m.metrics.RecordWorkloadCreated() @@ -398,7 +440,10 @@ func (m *Manager) ReconcileWorkload(ctx context.Context, id string) error { actualState, message, err := rt.Status(ctx, id) if err != nil { m.logger.Warnf("Failed to get status for %s: %v", id, err) - return err + status.ActualState = models.ActualStateFailed + status.Message = fmt.Sprintf("runtime status failed: %v", err) + status.UpdatedAt = time.Now() + return m.store.SaveStatus(status) } // Update status @@ -410,39 +455,54 @@ func (m *Manager) ReconcileWorkload(ctx context.Context, id string) error { needsAction := false // Don't reconcile if workload is in a transient state (unknown or pending indicates creation/deletion in progress) - if actualState == models.ActualStateUnknown || actualState == models.ActualStatePending { + missingFromRuntime := actualState == models.ActualStateUnknown && isRuntimeMissing(message) + if actualState == models.ActualStatePending { m.logger.Debugf("Skipping reconciliation for %s: workload is in transient state (%s)", id, actualState) needsAction = false - } else if actualState == models.ActualStateFailed { - tracker := m.getRetryTracker(id) - if tracker.GetAttemptCount() > 0 && !tracker.CanRetryNow() { + } else if actualState == models.ActualStateUnknown && !missingFromRuntime { + m.logger.Debugf("Skipping reconciliation for %s: workload state unknown (%s)", id, message) + needsAction = false + } else if actualState == models.ActualStateFailed || (missingFromRuntime && workload.DesiredState == models.DesiredStateRunning) { + if missingFromRuntime { + m.logger.Infof("Reconciling %s: workload missing from runtime, attempting recreate...", id) + status.ActualState = models.ActualStateFailed + if status.Message == "" { + status.Message = message + } ensureMetadata(status) - status.Metadata["retry_attempts"] = fmt.Sprintf("%d", tracker.GetAttemptCount()) - status.Metadata["next_retry_time"] = tracker.GetNextRetryTime().Format(time.RFC3339) - m.logger.Debugf("Skipping retry for %s until %s", id, tracker.GetNextRetryTime().Format(time.RFC3339)) - return m.store.SaveStatus(status) - } - - reason := retry.ClassifyError(stdErrors.New(status.Message)) - retryResult, recordErr := tracker.RecordFailure(reason, status.Message) - if recordErr != nil { - m.logger.Warnf("Failed to record retry metadata for %s: %v", id, recordErr) - } + status.Metadata["failure_reason"] = "RUNTIME_RESOURCE_MISSING" + status.Metadata["last_error"] = status.Message + } else { + tracker := m.getRetryTracker(id) + if tracker.GetAttemptCount() > 0 && !tracker.CanRetryNow() { + ensureMetadata(status) + status.Metadata["retry_attempts"] = fmt.Sprintf("%d", tracker.GetAttemptCount()) + status.Metadata["next_retry_time"] = tracker.GetNextRetryTime().Format(time.RFC3339) + m.logger.Debugf("Skipping retry for %s until %s", id, tracker.GetNextRetryTime().Format(time.RFC3339)) + return m.store.SaveStatus(status) + } - ensureMetadata(status) - status.Metadata["retry_attempts"] = fmt.Sprintf("%d", tracker.GetAttemptCount()) - status.Metadata["failure_reason"] = string(reason) - status.Metadata["last_error"] = status.Message + reason := retry.ClassifyError(stdErrors.New(status.Message)) + retryResult, recordErr := tracker.RecordFailure(reason, status.Message) + if recordErr != nil { + m.logger.Warnf("Failed to record retry metadata for %s: %v", id, recordErr) + } - if retryResult != nil && retryResult.Retryable { - status.Metadata["next_retry_time"] = retryResult.NextRetryTime.Format(time.RFC3339) - if !tracker.CanRetryNow() { - m.logger.Debugf("Skipping retry for %s until %s", id, retryResult.NextRetryTime.Format(time.RFC3339)) + ensureMetadata(status) + status.Metadata["retry_attempts"] = fmt.Sprintf("%d", tracker.GetAttemptCount()) + status.Metadata["failure_reason"] = string(reason) + status.Metadata["last_error"] = status.Message + + if retryResult != nil && retryResult.Retryable { + status.Metadata["next_retry_time"] = retryResult.NextRetryTime.Format(time.RFC3339) + if !tracker.CanRetryNow() { + m.logger.Debugf("Skipping retry for %s until %s", id, retryResult.NextRetryTime.Format(time.RFC3339)) + return m.store.SaveStatus(status) + } + } else { + m.logger.Infof("Not retrying failed workload %s (reason: %s)", id, reason) return m.store.SaveStatus(status) } - } else { - m.logger.Infof("Not retrying failed workload %s (reason: %s)", id, reason) - return m.store.SaveStatus(status) } // Retry failed workloads when policy allows @@ -475,6 +535,10 @@ func (m *Manager) ReconcileWorkload(ctx context.Context, id string) error { delete(status.Metadata, "failure_reason") delete(status.Metadata, "last_error") } + } else { + status.ActualState = models.ActualStateStopped + status.Message = "recreated and left stopped" + m.resetRetryTracker(id) } } needsAction = true diff --git a/internal/workload/manager_test.go b/internal/workload/manager_test.go index a3b6e11..0f01b16 100644 --- a/internal/workload/manager_test.go +++ b/internal/workload/manager_test.go @@ -410,6 +410,11 @@ func TestApplyWorkload_ResourceUnavailable_FailsBeforeCreate(t *testing.T) { // Configure existing lookup mockStore.On("GetWorkload", "test-workload").Return(nil, errors.New("not found")) + mockStore.On("SaveWorkload", mock.AnythingOfType("*models.Workload")).Return(nil).Once() + mockStore.On("GetStatus", "test-workload").Return(nil, errors.New("not found")).Once() + mockStore.On("SaveStatus", mock.MatchedBy(func(s *models.WorkloadStatus) bool { + return s.ID == "test-workload" && s.ActualState == models.ActualStateFailed + })).Return(nil).Once() // Attach a real monitor with impossible thresholds so it fails deterministically manager.SetResourceMonitor(resources.NewMonitor(&resources.Thresholds{ @@ -431,14 +436,58 @@ func TestApplyWorkload_ResourceUnavailable_FailsBeforeCreate(t *testing.T) { assert.ErrorAs(t, err, &workloadErr) assert.Equal(t, errors2.ErrCodeResourceQuotaExceeded, workloadErr.Code) - // Verify no create/start or save operations were called - mockStore.AssertNotCalled(t, "SaveWorkload", mock.Anything) + // Verify no runtime operations were called mockRuntime.AssertNotCalled(t, "Create", mock.Anything, mock.Anything) mockRuntime.AssertNotCalled(t, "Start", mock.Anything, mock.Anything) mockStore.AssertExpectations(t) } +func TestApplyWorkload_CreateFailure_PersistsFailedStatus(t *testing.T) { + mockStore := new(MockStore) + mockRuntime := new(MockRuntime) + + runtimeMgr := runtime.NewManager() + mockRuntime.On("Type").Return(models.WorkloadTypeContainer) + runtimeMgr.Register(mockRuntime) + + logger := logrus.New() + logger.SetLevel(logrus.FatalLevel) + manager := NewManager(mockStore, runtimeMgr, logger) + + workload := &models.Workload{ + ID: "failed-create", + Type: models.WorkloadTypeContainer, + RevisionID: "rev-1", + DesiredState: models.DesiredStateRunning, + Spec: map[string]interface{}{"image": "nginx:latest"}, + } + + mockStore.On("GetWorkload", "failed-create").Return(nil, errors.New("not found")) + mockStore.On("SaveWorkload", mock.AnythingOfType("*models.Workload")).Return(nil) + mockStore.On("SaveStatus", mock.MatchedBy(func(s *models.WorkloadStatus) bool { + return s.ID == "failed-create" && s.ActualState == models.ActualStatePending + })).Return(nil).Once() + mockRuntime.On("Create", mock.Anything, workload).Return(errors.New("image pull failed")).Once() + mockStore.On("GetStatus", "failed-create").Return(&models.WorkloadStatus{ + ID: "failed-create", + Type: models.WorkloadTypeContainer, + DesiredState: models.DesiredStateRunning, + ActualState: models.ActualStatePending, + }, nil).Once() + mockStore.On("SaveStatus", mock.MatchedBy(func(s *models.WorkloadStatus) bool { + return s.ID == "failed-create" && s.ActualState == models.ActualStateFailed + })).Return(nil).Once() + + status, skipped, err := manager.ApplyWorkload(context.Background(), workload) + assert.Error(t, err) + assert.Nil(t, status) + assert.False(t, skipped) + mockStore.AssertNotCalled(t, "DeleteWorkload", "failed-create") + mockStore.AssertExpectations(t) + mockRuntime.AssertExpectations(t) +} + func TestReconcileWorkload_FailedWorkload_RespectsRetryBackoff(t *testing.T) { mockStore := new(MockStore) mockRuntime := new(MockRuntime) @@ -554,6 +603,61 @@ func TestReconcileWorkload_FailedWorkload_DoesNotConsumeAttemptsBeforeBackoffWin mockStore.AssertExpectations(t) } +func TestReconcileWorkload_RecreatesMissingRuntimeWorkload(t *testing.T) { + mockStore := new(MockStore) + mockRuntime := new(MockRuntime) + + runtimeMgr := runtime.NewManager() + mockRuntime.On("Type").Return(models.WorkloadTypeContainer) + runtimeMgr.Register(mockRuntime) + + logger := logrus.New() + logger.SetLevel(logrus.FatalLevel) + + manager := NewManager(mockStore, runtimeMgr, logger) + manager.SetRetryPolicy(&retry.RetryPolicy{ + MaxAttempts: 3, + InitialDelay: 0, + MaxDelay: 0, + BackoffMultiplier: 1, + OnlyRetryTransient: false, + }) + + workload := &models.Workload{ + ID: "missing-workload", + Type: models.WorkloadTypeContainer, + DesiredState: models.DesiredStateRunning, + } + + status := &models.WorkloadStatus{ + ID: "missing-workload", + Type: models.WorkloadTypeContainer, + DesiredState: models.DesiredStateRunning, + ActualState: models.ActualStateUnknown, + Message: "container not found", + } + + mockStore.On("GetWorkload", "missing-workload").Return(workload, nil) + mockStore.On("GetStatus", "missing-workload").Return(status, nil) + mockRuntime.On("Status", mock.Anything, "missing-workload").Return( + models.ActualStateUnknown, "container not found", nil, + ).Once() + mockRuntime.On("Delete", mock.Anything, "missing-workload").Return(nil).Once() + mockRuntime.On("Create", mock.Anything, workload).Return(nil).Once() + mockRuntime.On("Start", mock.Anything, "missing-workload").Return(nil).Once() + mockRuntime.On("Status", mock.Anything, "missing-workload").Return( + models.ActualStateRunning, "running", nil, + ).Once() + mockStore.On("SaveStatus", mock.MatchedBy(func(s *models.WorkloadStatus) bool { + return s.ActualState == models.ActualStateRunning + })).Return(nil).Once() + + err := manager.ReconcileWorkload(context.Background(), "missing-workload") + assert.NoError(t, err) + mockStore.AssertExpectations(t) + mockRuntime.AssertExpectations(t) +} + func TestGetStatus_SurfacesRuntimeMetadata(t *testing.T) { mockStore := new(MockStore) baseRuntime := new(MockRuntime) From f36504bd2f14377e4511eaad14f0ddda5fe52115 Mon Sep 17 00:00:00 2001 From: milx Date: Tue, 17 Feb 2026 21:37:11 +0330 Subject: [PATCH 08/25] Chore: Optimize Docker image to be nimble --- Dockerfile | 63 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/Dockerfile b/Dockerfile index 291e67c..cab209b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,53 +1,44 @@ -# Build stage -FROM golang:1.21-alpine AS builder +# syntax=docker/dockerfile:1.7 -# Install build dependencies -RUN apk add --no-cache git make protobuf protobuf-dev +ARG GO_VERSION=1.24.4 +ARG VERSION=1.0.0 -WORKDIR /build +FROM golang:${GO_VERSION}-bookworm AS builder +WORKDIR /src -# Copy go mod files COPY go.mod go.sum ./ -RUN go mod download +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download -# Copy source code COPY . . -# Build the binary -RUN make build-linux +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" \ + -o /out/persys-agent ./cmd/agent -# Runtime stage -FROM ubuntu:22.04 - -# Install runtime dependencies -RUN apt-get update && apt-get install -y \ +FROM debian:bookworm-slim AS runtime-base +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ docker.io \ - docker-compose \ - qemu-kvm \ - libvirt-daemon-system \ + docker-compose-plugin \ + qemu-utils \ + genisoimage \ libvirt-clients \ - ca-certificates \ && rm -rf /var/lib/apt/lists/* -# Create persys user -RUN useradd -r -u 1000 -g 0 -m -s /bin/bash persys && \ +RUN useradd -r -u 1000 -g 0 -m -s /usr/sbin/nologin persys && \ mkdir -p /var/lib/persys /etc/persys/certs && \ chown -R persys:root /var/lib/persys /etc/persys -# Copy binary from builder -COPY --from=builder /build/bin/persys-agent-linux-amd64 /usr/local/bin/persys-agent -RUN chmod +x /usr/local/bin/persys-agent +COPY --from=builder /out/persys-agent /usr/local/bin/persys-agent -# Set up volumes VOLUME ["/var/lib/persys", "/etc/persys"] +EXPOSE 50051 8080 -# Expose gRPC port -EXPOSE 50051 - -# Run as persys user USER persys -# Set default environment variables ENV PERSYS_STATE_PATH=/var/lib/persys/state.db \ PERSYS_TLS_CERT=/etc/persys/certs/agent.crt \ PERSYS_TLS_KEY=/etc/persys/certs/agent.key \ @@ -55,3 +46,15 @@ ENV PERSYS_STATE_PATH=/var/lib/persys/state.db \ PERSYS_LOG_LEVEL=info ENTRYPOINT ["/usr/local/bin/persys-agent"] + +# Default optimized runtime image. +FROM runtime-base AS runtime + +# Optional full runtime image with local daemons/tools for all-in-one test environments. +FROM runtime-base AS full-runtime +USER root +RUN apt-get update && apt-get install -y --no-install-recommends \ + qemu-kvm \ + libvirt-daemon-system \ + && rm -rf /var/lib/apt/lists/* +USER persys From 614cede03b91bceb422f635c8840d589f685d477 Mon Sep 17 00:00:00 2001 From: milx Date: Tue, 17 Feb 2026 21:37:29 +0330 Subject: [PATCH 09/25] Feat: Better CI stages --- .github/workflows/ci.yml | 145 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 137 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a9a23f..4fbd22d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,9 +8,16 @@ on: - master - work +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + jobs: - unit-tests: - name: Unit Tests + lint-and-validate: + name: Lint And Validate runs-on: ubuntu-latest steps: - name: Checkout @@ -22,16 +29,89 @@ jobs: go-version-file: go.mod cache: true - - name: Download dependencies + - name: Go mod download run: go mod download - - name: Run unit tests + - name: Verify gofmt + run: | + files="$(gofmt -l .)" + if [ -n "$files" ]; then + echo "Unformatted files:" + echo "$files" + exit 1 + fi + + - name: Verify shell scripts syntax + run: | + if find . -type f -name '*.sh' | grep -q .; then + find . -type f -name '*.sh' -print0 | xargs -0 -n1 bash -n + fi + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: latest + args: --timeout=5m + + proto-drift-check: + name: Proto Drift Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Install protoc + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends protobuf-compiler + protoc --version + + - name: Install protobuf generators + run: | + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.1 + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: Regenerate proto + run: make proto + + - name: Verify generated code is committed + run: git diff --exit-code + + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + needs: [lint-and-validate, proto-drift-check] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Run unit tests with race and coverage run: make test-unit + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: unit-coverage + path: coverage.txt + if-no-files-found: error + e2e-tests: name: End-to-End Tests runs-on: ubuntu-latest - needs: unit-tests + needs: [unit-tests] steps: - name: Checkout uses: actions/checkout@v4 @@ -42,8 +122,57 @@ jobs: go-version-file: go.mod cache: true - - name: Download dependencies - run: go mod download - - name: Run end-to-end tests run: make test-e2e + + build-binaries: + name: Build Binaries + runs-on: ubuntu-latest + needs: [unit-tests] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Build agent and client + run: | + go build -v ./cmd/agent + go build -v ./examples/client + + docker-build: + name: Docker Build + runs-on: ubuntu-latest + needs: [unit-tests] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build optimized runtime image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + target: runtime + push: false + tags: persys/compute-agent:ci-runtime + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build full runtime image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + target: full-runtime + push: false + tags: persys/compute-agent:ci-full-runtime + cache-from: type=gha + cache-to: type=gha,mode=max From d3c8f14c7472769e4612e9c48e6c15bab7decbf4 Mon Sep 17 00:00:00 2001 From: milx Date: Tue, 17 Feb 2026 21:43:43 +0330 Subject: [PATCH 10/25] Chore: Ran Go FMT to fix CI error --- internal/retry/policy.go | 13 ++++++------- internal/state/store.go | 14 +++++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/internal/retry/policy.go b/internal/retry/policy.go index ad601a6..cbdd532 100644 --- a/internal/retry/policy.go +++ b/internal/retry/policy.go @@ -320,17 +320,16 @@ func contains(str string, keywords ...string) bool { str = strings.ToLower(str) for _, keyword := range keywords { if strings.Contains(str, strings.ToLower(keyword)) { - str = strings.ToLower(fmt.Sprintf(" %s ", str)) // Add padding for word boundaries - for _, keyword := range keywords { - if containsHelper(str, strings.ToLower(keyword)) { - return true + str = strings.ToLower(fmt.Sprintf(" %s ", str)) // Add padding for word boundaries + for _, keyword := range keywords { + if containsHelper(str, strings.ToLower(keyword)) { + return true + } } } } -} return false -} - +} func containsHelper(haystack, needle string) bool { return strings.Contains(haystack, needle) diff --git a/internal/state/store.go b/internal/state/store.go index 7fd7a1e..73d2265 100644 --- a/internal/state/store.go +++ b/internal/state/store.go @@ -65,7 +65,7 @@ func NewBoltStore(path string) (Store, error) { func (s *boltStore) SaveWorkload(workload *models.Workload) error { return s.db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(workloadBucket)) - + workload.UpdatedAt = time.Now() if workload.CreatedAt.IsZero() { workload.CreatedAt = workload.UpdatedAt @@ -86,7 +86,7 @@ func (s *boltStore) GetWorkload(id string) (*models.Workload, error) { err := s.db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(workloadBucket)) data := bucket.Get([]byte(id)) - + if data == nil { return fmt.Errorf("workload not found") } @@ -106,7 +106,7 @@ func (s *boltStore) DeleteWorkload(id string) error { return s.db.Update(func(tx *bolt.Tx) error { workloadBucket := tx.Bucket([]byte(workloadBucket)) statusBucket := tx.Bucket([]byte(statusBucket)) - + // Delete both workload and status if err := workloadBucket.Delete([]byte(id)); err != nil { return err @@ -120,7 +120,7 @@ func (s *boltStore) ListWorkloads() ([]*models.Workload, error) { err := s.db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(workloadBucket)) - + return bucket.ForEach(func(k, v []byte) error { var workload models.Workload if err := json.Unmarshal(v, &workload); err != nil { @@ -141,7 +141,7 @@ func (s *boltStore) ListWorkloads() ([]*models.Workload, error) { func (s *boltStore) SaveStatus(status *models.WorkloadStatus) error { return s.db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(statusBucket)) - + status.UpdatedAt = time.Now() if status.CreatedAt.IsZero() { status.CreatedAt = status.UpdatedAt @@ -162,7 +162,7 @@ func (s *boltStore) GetStatus(id string) (*models.WorkloadStatus, error) { err := s.db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(statusBucket)) data := bucket.Get([]byte(id)) - + if data == nil { return fmt.Errorf("status not found") } @@ -183,7 +183,7 @@ func (s *boltStore) ListStatuses() ([]*models.WorkloadStatus, error) { err := s.db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(statusBucket)) - + return bucket.ForEach(func(k, v []byte) error { var status models.WorkloadStatus if err := json.Unmarshal(v, &status); err != nil { From 677a98d908a9f35622c82704c9b9364c4d4ee54c Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 15:20:07 +0330 Subject: [PATCH 11/25] Chore: Disable Lint Stage temporarily --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fbd22d..75411b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,11 +47,11 @@ jobs: find . -type f -name '*.sh' -print0 | xargs -0 -n1 bash -n fi - - name: Run golangci-lint - uses: golangci/golangci-lint-action@v8 - with: - version: latest - args: --timeout=5m + # - name: Run golangci-lint + # uses: golangci/golangci-lint-action@v8 + # with: + # version: latest + # args: --timeout=5m proto-drift-check: name: Proto Drift Check From 4d3fa3e982e65ed434842d208751cb06e42f4fbf Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 16:25:12 +0330 Subject: [PATCH 12/25] Chore: Fix Dockerfile broken apt download --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index cab209b..c7ecd89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,6 @@ FROM debian:bookworm-slim AS runtime-base RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ docker.io \ - docker-compose-plugin \ qemu-utils \ genisoimage \ libvirt-clients \ From 6e4a06527a1a7c2437788455b1ded0e0d0cccd89 Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 17:51:02 +0330 Subject: [PATCH 13/25] Chore: Update Makefile to include build for control plane proto files --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile b/Makefile index 6d7a01f..8006c20 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ BUILD_DIR=bin DOCKER_IMAGE=persys/compute-agent PROTO_DIR=api/proto PKG_DIR=pkg/api/v1 +CONTROL_PKG_DIR=pkg/control/v1 # Go parameters GOCMD=go @@ -27,9 +28,13 @@ deps: proto: @echo "==> Generating protobuf code..." @mkdir -p $(PKG_DIR) + @mkdir -p $(CONTROL_PKG_DIR) cd api/proto && protoc --go_out=../../$(PKG_DIR) --go_opt=paths=source_relative \ --go-grpc_out=../../$(PKG_DIR) --go-grpc_opt=paths=source_relative \ agent.proto + cd api/proto && protoc --go_out=../../$(CONTROL_PKG_DIR) --go_opt=paths=source_relative \ + --go-grpc_out=../../$(CONTROL_PKG_DIR) --go-grpc_opt=paths=source_relative \ + control.proto build: @echo "==> Building $(BINARY_NAME)..." @@ -54,6 +59,7 @@ clean: $(GOCLEAN) rm -rf $(BUILD_DIR) rm -rf $(PKG_DIR) + rm -rf $(CONTROL_PKG_DIR) test: @echo "==> Running all tests..." From 4188db44e11a91caf71f90a1f0fc4fb1e967ab4e Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 17:51:12 +0330 Subject: [PATCH 14/25] Chore: Update Readme --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index c0458a2..a878b9c 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,17 @@ The agent is configured via environment variables: |----------|---------|-------------| | `PERSYS_NODE_ID` | hostname | Unique node identifier | | `PERSYS_VERSION` | `dev` | Agent version | +| `PERSYS_NODE_REGION` | `` | Node region label (ex: `us-east-1`) | +| `PERSYS_NODE_ENV` | `` | Node environment label (ex: `prod`, `staging`) | +| `PERSYS_NODE_LABELS` | `` | Extra labels as `key=value,key2=value2` | +| `PERSYS_AGENT_GRPC_ENDPOINT` | auto-derived | Scheduler-reachable agent endpoint (`host:port`) | + +### Scheduler Control Plane + +| Variable | Default | Description | +|----------|---------|-------------| +| `PERSYS_SCHEDULER_ADDR` | `127.0.0.1:8085` | Scheduler gRPC control endpoint (`host:port`) | +| `PERSYS_SCHEDULER_INSECURE` | `false` | Disable TLS for scheduler control client (testing only) | ## API Reference From 86ee79e696c1433085fae93a9d6e38902d6db00a Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 17:51:29 +0330 Subject: [PATCH 15/25] Feat: Add Control Plane Proto Contract --- api/proto/control.proto | 214 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 api/proto/control.proto diff --git a/api/proto/control.proto b/api/proto/control.proto new file mode 100644 index 0000000..bf4f4ad --- /dev/null +++ b/api/proto/control.proto @@ -0,0 +1,214 @@ +syntax = "proto3"; + +package persys.control.v1; + +option go_package = "github.com/persys/compute-agent/pkg/control/v1;controlv1"; + +import "google/protobuf/timestamp.proto"; + +service AgentControl { + // Registration + rpc RegisterNode(RegisterNodeRequest) returns (RegisterNodeResponse); + + // Heartbeat + rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse); + + // Workload lifecycle + rpc ApplyWorkload(ApplyWorkloadRequest) returns (ApplyWorkloadResponse); + rpc DeleteWorkload(DeleteWorkloadRequest) returns (DeleteWorkloadResponse); + + // Retry trigger + rpc RetryWorkload(RetryWorkloadRequest) returns (RetryWorkloadResponse); + + // Optional future streaming channel + rpc ControlStream(stream ControlMessage) returns (stream ControlMessage); +} + +message RegisterNodeRequest { + string node_id = 1; + NodeCapabilities capabilities = 2; + map labels = 3; + string agent_version = 4; + string grpc_endpoint = 5; + string cluster_id = 6; + google.protobuf.Timestamp timestamp = 7; +} + +message NodeCapabilities { + int64 cpu_total_millicores = 1; + int64 memory_total_mb = 2; + repeated StoragePool storage_pools = 3; + repeated string supported_workload_types = 4; // container, compose, vm +} + +message StoragePool { + string name = 1; + string type = 2; // local, nfs, iscsi + int64 total_gb = 3; +} + +message RegisterNodeResponse { + bool accepted = 1; + string reason = 2; + int32 heartbeat_interval_seconds = 3; + google.protobuf.Timestamp lease_expires_at = 4; +} + +message HeartbeatRequest { + string node_id = 1; + NodeUsage usage = 2; + repeated WorkloadStatus workload_statuses = 3; + google.protobuf.Timestamp timestamp = 4; +} + +message NodeUsage { + int64 cpu_allocated_millicores = 1; + int64 cpu_used_millicores = 2; + int64 memory_allocated_mb = 3; + int64 memory_used_mb = 4; + int64 disk_allocated_gb = 5; + int64 disk_used_gb = 6; +} + +message HeartbeatResponse { + bool acknowledged = 1; + bool drain_node = 2; + google.protobuf.Timestamp lease_expires_at = 3; +} + +message ApplyWorkloadRequest { + string workload_id = 1; + WorkloadSpec spec = 2; + + // Compatibility fields aligned with existing agent apply semantics. + string revision_id = 10; + string desired_state = 11; // Running | Stopped +} + +message ApplyWorkloadResponse { + bool success = 1; + FailureReason failure_reason = 2; + string error_message = 3; +} + +message DeleteWorkloadRequest { + string workload_id = 1; +} + +message DeleteWorkloadResponse { + bool success = 1; + string error_message = 2; +} + +message WorkloadSpec { + string type = 1; // container, compose, vm + ResourceRequirements resources = 2; + + oneof workload { + ContainerSpec container = 10; + ComposeSpec compose = 11; + VMSpec vm = 12; + } + + map metadata = 20; +} + +message ResourceRequirements { + int64 cpu_millicores = 1; + int64 memory_mb = 2; + int64 disk_gb = 3; +} + +message ContainerSpec { + string image = 1; + repeated string command = 2; + map env = 3; + repeated VolumeMount volumes = 4; + repeated Port ports = 5; + string restart_policy = 6; + bool privileged = 7; +} + +message VolumeMount { + string host_path = 1; + string container_path = 2; + bool read_only = 3; +} + +message Port { + int32 host_port = 1; + int32 container_port = 2; + string protocol = 3; // tcp or udp +} + +message ComposeSpec { + string source_type = 1; // git or inline + string git_repo = 2; + string git_ref = 3; + string inline_yaml = 4; + map env = 5; +} + +message VMSpec { + int32 vcpus = 1; + int64 memory_mb = 2; + repeated DiskConfig disks = 3; + repeated NetworkConfig networks = 4; + CloudInitConfig cloud_init = 5; + string os_image = 6; +} + +message DiskConfig { + string pool_name = 1; + int64 size_gb = 2; + string mount_point = 3; +} + +message NetworkConfig { + string bridge = 1; + bool dhcp = 2; + string static_ip = 3; +} + +message CloudInitConfig { + string user_data = 1; + string meta_data = 2; + string network_config = 3; +} + +message WorkloadStatus { + string workload_id = 1; + string state = 2; // Running, Stopped, Failed + FailureReason failure_reason = 3; + string message = 4; + google.protobuf.Timestamp last_transition = 5; +} + +enum FailureReason { + FAILURE_REASON_UNSPECIFIED = 0; + IMAGE_PULL_FAILED = 1; + IMAGE_NOT_FOUND = 2; + INSUFFICIENT_RESOURCES = 3; + INVALID_SPEC = 4; + RUNTIME_ERROR = 5; + NETWORK_ERROR = 6; + STORAGE_ERROR = 7; + VM_BOOT_FAILED = 8; +} + +message RetryWorkloadRequest { + string workload_id = 1; +} + +message RetryWorkloadResponse { + bool accepted = 1; +} + +message ControlMessage { + oneof message { + RegisterNodeRequest register = 1; + HeartbeatRequest heartbeat = 2; + ApplyWorkloadRequest apply = 3; + DeleteWorkloadRequest delete = 4; + } +} From f1f65589bde36ab3634f220d0152187f735d8068 Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 17:52:06 +0330 Subject: [PATCH 16/25] Feat: Add Agent Standalone Mode Flag to Binary --- cmd/agent/main.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/cmd/agent/main.go b/cmd/agent/main.go index b6d53ee..c6ff426 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -2,12 +2,14 @@ package main import ( "context" + "flag" "fmt" "os" "os/signal" "syscall" "github.com/persys/compute-agent/internal/config" + "github.com/persys/compute-agent/internal/control" "github.com/persys/compute-agent/internal/garbage" "github.com/persys/compute-agent/internal/grpc" "github.com/persys/compute-agent/internal/metrics" @@ -24,6 +26,9 @@ import ( const version = "1.0.0" func main() { + standaloneMode := flag.Bool("standalone", false, "run agent without scheduler registration/heartbeat") + flag.Parse() + // Initialize logger logger := logrus.New() logger.SetFormatter(&logrus.TextFormatter{ @@ -198,6 +203,18 @@ func main() { } }() + // Start scheduler control client loop in background unless standalone mode is enabled + var cancelControl context.CancelFunc + if *standaloneMode { + logger.Info("Standalone mode enabled: skipping scheduler registration and heartbeat loop") + } else { + controlClient := control.NewClient(cfg, workloadMgr, runtimeMgr, resourceMonitor, logger) + controlCtx, cancel := context.WithCancel(context.Background()) + cancelControl = cancel + defer cancelControl() + go controlClient.Run(controlCtx) + } + // Wait for shutdown signal sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) @@ -221,6 +238,10 @@ func main() { reconciler.Stop() } + if cancelControl != nil { + cancelControl() + } + grpcServer.Stop() // Stop metrics server From b04ddfe5389665c13d886c2e08e5d551f4fd03b4 Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 17:53:07 +0330 Subject: [PATCH 17/25] Chore: Add integration doc and update getting started --- docs/GETTING_STARTED.md | 9 + docs/SCHEDULER_INTEGRATION_GUIDE.md | 293 ++++++++++++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 docs/SCHEDULER_INTEGRATION_GUIDE.md diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index f9aad5b..c0de108 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -181,6 +181,10 @@ The agent is configured via environment variables. Here are the most important o # Server export PERSYS_GRPC_PORT=50051 +# Scheduler control plane +export PERSYS_SCHEDULER_ADDR=127.0.0.1:8085 +export PERSYS_SCHEDULER_INSECURE=true # use only for local testing + # TLS export PERSYS_TLS_ENABLED=true export PERSYS_TLS_CERT=/path/to/agent.crt @@ -192,6 +196,11 @@ export PERSYS_STATE_PATH=/var/lib/persys/state.db # Logging export PERSYS_LOG_LEVEL=info + +# Optional node metadata/labels advertised during RegisterNode +export PERSYS_NODE_REGION=us-east-1 +export PERSYS_NODE_ENV=staging +export PERSYS_NODE_LABELS=team=platform,zone=use1-az2 ``` ### Runtime Configuration diff --git a/docs/SCHEDULER_INTEGRATION_GUIDE.md b/docs/SCHEDULER_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..22b8407 --- /dev/null +++ b/docs/SCHEDULER_INTEGRATION_GUIDE.md @@ -0,0 +1,293 @@ +# Persys Scheduler Integration Guide (Compute Agent) + +## Purpose + +This document is a handover guide for implementing Persys Compute Scheduler integration with the compute agent. + +It is focused on: + +- API contract usage patterns +- state machine and reconciliation semantics +- retry/idempotency behavior +- observability and debugging +- production rollout checklist + +--- + +## 1. Integration Model + +The scheduler is the control plane. The agent is the execution plane. + +- Scheduler decides placement and desired state. +- Agent executes `ApplyWorkload` / `DeleteWorkload` and reports runtime state. +- Scheduler remains source of intent. +- Agent persists local workload/state for crash recovery and reconciliation. + +Key behavior: + +- `revision_id` provides idempotency for apply. +- Agent may accept async tasks and initially return `pending`. +- Agent reconciliation loop self-heals runtime drift (including recreate on missing runtime resources). + +--- + +## 2. Agent API Surface + +From `api/proto/agent.proto`: + +- `ApplyWorkload(ApplyWorkloadRequest) -> ApplyWorkloadResponse` +- `DeleteWorkload(DeleteWorkloadRequest) -> DeleteWorkloadResponse` +- `GetWorkloadStatus(GetWorkloadStatusRequest) -> GetWorkloadStatusResponse` +- `ListWorkloads(ListWorkloadsRequest) -> ListWorkloadsResponse` +- `HealthCheck(HealthCheckRequest) -> HealthCheckResponse` +- `ListActions(ListActionsRequest) -> ListActionsResponse` + +### 2.1 Recommended call ordering + +For create/update: + +1. Scheduler sends `ApplyWorkload`. +2. Scheduler stores request metadata (workload id, revision id, timestamp, node id). +3. If response status is `pending`, scheduler polls `GetWorkloadStatus`. +4. Scheduler optionally queries `ListActions` for traceability (`workload_id` + `newest_first=true`). +5. Scheduler commits final state in control-plane DB once terminal state is observed. + +For delete: + +1. Scheduler sends `DeleteWorkload`. +2. Scheduler polls `GetWorkloadStatus`. +3. Treat `status not found` as terminal deletion success. + +--- + +## 3. Workload Identity and Idempotency + +Use stable keys: + +- `id`: globally unique workload instance identifier (stable across retries). +- `revision_id`: increment or content-hash on any spec change. + +Agent behavior: + +- Same `id` + same `revision_id` => apply is skipped (`skipped=true`). +- Same `id` + different `revision_id` => recreate path. + +Scheduler rule: + +- Never generate a new `id` for transient retry of the same logical workload update. +- Only change `revision_id` when intent/spec changes. + +--- + +## 4. State Mapping (Scheduler View) + +Agent `ActualState`: + +- `PENDING` +- `RUNNING` +- `STOPPED` +- `FAILED` +- `UNKNOWN` + +Suggested scheduler interpretation: + +- `PENDING`: accepted by agent but not yet converged; continue polling. +- `RUNNING`: desired running converged. +- `STOPPED`: desired stopped converged (or runtime not started). +- `FAILED`: terminal for current attempt; scheduler policy decides retry/backoff/escalation. +- `UNKNOWN`: transitional/inspection state; continue polling and/or trigger `ListActions`. + +--- + +## 5. Async Execution and Action Traceability + +When task queue path is used, apply response returns a status with metadata: + +- `metadata["task_id"]` +- `metadata["task_status"]` (initially `pending`) + +Use `ListActions` to inspect execution history since agent startup. + +Recommended query: + +- `workload_id=` +- `limit=20` +- `newest_first=true` + +Store in scheduler event/audit log: + +- task id +- action type +- task status +- task error +- created/started/ended timestamps + +--- + +## 6. Failure Handling Strategy + +### 6.1 Apply failures + +Agent persists failed status and intent, so failed scheduling attempts are now observable and reconcilable. + +Scheduler should: + +1. Mark workload as `admitted_on_node=true` after successful RPC acceptance. +2. If state becomes `FAILED`, apply scheduler retry policy: + - transient infra/runtime errors: retry with backoff + - validation/spec errors: do not hot-loop; surface to user +3. Keep same `id`, same `revision_id` for retry unless intent changed. + +### 6.2 Missing runtime artifacts + +Agent reconciliation attempts recreation when workload exists in state but runtime artifact is missing (`not found` class). + +Scheduler implication: + +- Do not immediately re-place workload elsewhere on first missing-runtime signal. +- Allow local reconcile window before failover policy triggers. + +### 6.3 Delete failures + +Delete RPC may be accepted while async cleanup is ongoing. + +Scheduler should: + +- poll until status is gone +- if stale for too long, read `ListActions` + health checks and escalate + +--- + +## 7. Runtime-Specific Notes + +### 7.1 Compose + +- `compose_yaml` must be base64 encoded YAML. +- `project_name` should be stable and deterministic. + +### 7.2 VM + +- VM `disks` and `networks` must be explicit in spec. +- Cloud-init can be provided via `cloud_init` or `cloud_init_config`. +- VM delete cleanup is safe: + - deterministic cloud-init ISO artifacts are removed + - only agent-managed disks are auto-removed + - user-provided external disks are preserved + +--- + +## 8. Security and Connectivity + +Agent config defaults (`internal/config/config.go`): + +- gRPC: `PERSYS_GRPC_ADDR`, `PERSYS_GRPC_PORT` +- mTLS: `PERSYS_TLS_ENABLED`, `PERSYS_TLS_CERT`, `PERSYS_TLS_KEY`, `PERSYS_TLS_CA` + +Production guidance: + +- mTLS enabled (`PERSYS_TLS_ENABLED=true`) +- scheduler client cert rotation supported +- strict CA trust boundary per environment (dev/staging/prod) + +--- + +## 9. Polling and Timeouts (Recommended Defaults) + +Scheduler defaults: + +- apply status poll interval: `1s` +- initial apply timeout: `45s` (container/compose) +- VM apply timeout: `240s`+ depending on image/bootstrap +- delete timeout: `60s`+ + +Backoff: + +- exponential with jitter +- bounded max delay +- upper retry limit with escalation + +--- + +## 10. Suggested Scheduler Implementation Skeleton + +```text +schedule(workload): + request := buildApplyRequest(workload.id, workload.revision, desired, spec) + resp := agent.ApplyWorkload(request) + + if rpc error: + mark "dispatch_failed" + retry dispatch + + record acceptance + response metadata + + if resp.status.actual_state in {RUNNING, STOPPED}: + mark converged + return + + while timeout not exceeded: + st := agent.GetWorkloadStatus(workload.id) + if st in terminal: + persist terminal and return + sleep(poll_interval) + + actions := agent.ListActions(workload_id=workload.id, newest_first=true, limit=20) + mark timeout + attach actions for diagnostics +``` + +--- + +## 11. Operational Observability + +Use both: + +- `GetWorkloadStatus` for current truth +- `ListActions` for execution trace + +Minimum scheduler logs per operation: + +- node id +- workload id +- revision id +- desired state +- agent RPC request timestamp +- task id (if present) +- terminal state +- terminal message/error + +--- + +## 12. Rollout Plan (Recommended) + +1. Enable integration in staging with read-only shadow mode (scheduler sends apply; does not route user traffic yet). +2. Validate: + - revision idempotency + - failure traceability via `ListActions` + - reconcile recreate behavior +3. Turn on production for container workloads first. +4. Enable compose, then VM after storage/network validation. +5. Keep automated quick smoke tests in CI and pre-deploy checks. + +--- + +## 13. Implementation Checklist + +- [ ] gRPC client generated from `api/proto/agent.proto` +- [ ] scheduler workload model maps 1:1 to agent workload spec +- [ ] stable `id` and deterministic `revision_id` +- [ ] apply/delete orchestration with polling +- [ ] `ListActions` ingestion into scheduler audit/events +- [ ] retry/backoff policy implemented per failure class +- [ ] mTLS cert distribution and rotation wired +- [ ] production timeouts set by workload type +- [ ] dashboards/alerts for failed/pending timeout workloads + +--- + +## 14. Reference Files + +- API contract: `api/proto/agent.proto` +- Agent configuration: `internal/config/config.go` +- Architecture details: `docs/ARCHITECTURE.md` +- Example client and specs: `examples/client/main.go`, `examples/client/specs/` + From f54302b88413638af15df45968d92524fb36de05 Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 17:53:49 +0330 Subject: [PATCH 18/25] Chore: Fix Health request to show proper output --- examples/client/main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/client/main.go b/examples/client/main.go index f660c30..822f00a 100644 --- a/examples/client/main.go +++ b/examples/client/main.go @@ -247,6 +247,10 @@ func healthCheck(ctx context.Context, client pb.AgentServiceClient) { for runtime, status := range resp.RuntimeStatus { fmt.Printf(" %s: %s\n", runtime, status) } + + fmt.Printf(" CPU Utilization: %v\n", resp.CpuUtilization) + fmt.Printf(" Memory Utilization: %v\n", resp.MemoryUtilization) + fmt.Printf(" Disk Utilization: %v\n", resp.DiskUtilization) } func applyWorkload(ctx context.Context, client pb.AgentServiceClient, workloadID string, opts *applyOptions) { From d3a12ee4344da878b0d479b0d40f80da89b4e146 Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 17:55:06 +0330 Subject: [PATCH 19/25] Chore: Add Spec Files for workload apply test via smoke client --- .../client/specs/container-spec-minimal.json | 7 +------ examples/client/specs/container-spec.json | 16 ++-------------- examples/client/specs/vm-spec.json | 15 +++++---------- 3 files changed, 8 insertions(+), 30 deletions(-) diff --git a/examples/client/specs/container-spec-minimal.json b/examples/client/specs/container-spec-minimal.json index 1e7fcf5..a44eb5a 100644 --- a/examples/client/specs/container-spec-minimal.json +++ b/examples/client/specs/container-spec-minimal.json @@ -1,10 +1,5 @@ { - "image": "busybox:1.36", - "command": [ - "sh", - "-c", - "echo container-ok && sleep 300" - ], + "image": "nginx:latest", "ports": [ { "host_port": 18080, diff --git a/examples/client/specs/container-spec.json b/examples/client/specs/container-spec.json index 229208d..b02ecf9 100644 --- a/examples/client/specs/container-spec.json +++ b/examples/client/specs/container-spec.json @@ -1,5 +1,5 @@ { - "image": "nginx:1.27", + "image": "nginx:latest", "command": [ "/docker-entrypoint.sh" ], @@ -13,21 +13,9 @@ "NGINX_PORT": "80", "APP_ENV": "staging" }, - "volumes": [ - { - "host_path": "/tmp/nginx-data", - "container_path": "/usr/share/nginx/html", - "read_only": false - }, - { - "host_path": "/tmp/nginx-config", - "container_path": "/etc/nginx/conf.d", - "read_only": true - } - ], "ports": [ { - "host_port": 8080, + "host_port": 18080, "container_port": 80, "protocol": "tcp" }, diff --git a/examples/client/specs/vm-spec.json b/examples/client/specs/vm-spec.json index 14ddfa4..edcc56f 100644 --- a/examples/client/specs/vm-spec.json +++ b/examples/client/specs/vm-spec.json @@ -1,10 +1,10 @@ { - "name": "demo-vm", + "name": "persys-vm", "vcpus": 2, - "memory_mb": 4096, + "memory_mb": 2048, "disks": [ { - "path": "/var/lib/libvirt/images/demo-vm.qcow2", + "path": "/var/lib/libvirt/images/persys-vm.qcow2", "device": "vda", "format": "qcow2", "size_gb": 30, @@ -12,7 +12,7 @@ "boot": false }, { - "path": "/var/lib/libvirt/images/fedora-coreos-live.iso", + "path": "/var/lib/libvirt/images/iso/fedora-coreos-42.20250803.3.0-live-iso.x86_64.iso", "device": "hdc", "format": "raw", "size_gb": 0, @@ -25,14 +25,9 @@ "network": "default", "mac_address": "52:54:00:12:34:56", "ip_address": "" - }, - { - "network": "default", - "mac_address": "52:54:00:12:34:57", - "ip_address": "" } ], - "cloud_init": "#cloud-config\nhostname: demo-vm\nmanage_etc_hosts: true\nusers:\n - name: core\n groups: [wheel]\n sudo: [\"ALL=(ALL) NOPASSWD:ALL\"]\npackages:\n - qemu-guest-agent\nruncmd:\n - systemctl enable --now qemu-guest-agent\n - echo \"cloud-init done\" > /tmp/cloud-init.done\n", + "cloud_init": "#cloud-config\nhostname: persys-vm\nmanage_etc_hosts: true\nusers:\n - name: core\n groups: [wheel]\n sudo: [\"ALL=(ALL) NOPASSWD:ALL\"]\npackages:\n - qemu-guest-agent\nruncmd:\n - systemctl enable --now qemu-guest-agent\n - echo \"cloud-init done\" > /tmp/cloud-init.done\n", "metadata": { "environment": "staging", "owner": "platform-team", From d6b52a8725869d83cfebb107d06e297655205144 Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 17:55:47 +0330 Subject: [PATCH 20/25] Chore: Add new envs and labels to agent config --- internal/config/config.go | 66 +++++++++++++++++++++++++++++-- internal/config/config_test.go | 72 +++++++++++++++++++++++++++++++++- 2 files changed, 133 insertions(+), 5 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 977aad3..ef717ba 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strconv" + "strings" "time" "github.com/persys/compute-agent/internal/node" @@ -40,8 +41,17 @@ type Config struct { LogLevel string // Agent metadata - NodeID string - Version string + NodeID string + Version string + NodeRegion string + NodeEnv string + NodeLabels map[string]string + + // Scheduler control-plane configuration + SchedulerAddr string + SchedulerInsecure bool + SchedulerTLSEnabled bool + AgentGRPCEndpoint string } // Load reads configuration from environment variables with sensible defaults @@ -76,10 +86,24 @@ func Load() (*Config, error) { LogLevel: getEnv("PERSYS_LOG_LEVEL", "info"), // Metadata - NodeID: generateNodeID(), - Version: getEnv("PERSYS_VERSION", "dev"), + NodeID: generateNodeID(), + Version: getEnv("PERSYS_VERSION", "dev"), + NodeRegion: getEnv("PERSYS_NODE_REGION", ""), + NodeEnv: getEnv("PERSYS_NODE_ENV", ""), + + // Scheduler defaults + SchedulerAddr: getEnv("PERSYS_SCHEDULER_ADDR", "127.0.0.1:8085"), + SchedulerInsecure: getEnvAsBool("PERSYS_SCHEDULER_INSECURE", false), + SchedulerTLSEnabled: !getEnvAsBool("PERSYS_SCHEDULER_INSECURE", false), + AgentGRPCEndpoint: getEnv("PERSYS_AGENT_GRPC_ENDPOINT", ""), } + cfg.NodeLabels = parseNodeLabels( + cfg.NodeRegion, + cfg.NodeEnv, + getEnv("PERSYS_NODE_LABELS", ""), + ) + if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("invalid configuration: %w", err) } @@ -107,6 +131,10 @@ func (c *Config) Validate() error { return fmt.Errorf("at least one runtime must be enabled") } + if c.SchedulerAddr == "" { + return fmt.Errorf("scheduler address cannot be empty") + } + return nil } @@ -145,6 +173,36 @@ func getEnvAsDuration(key string, defaultValue time.Duration) time.Duration { return defaultValue } +func parseNodeLabels(region, env, labelsRaw string) map[string]string { + labels := make(map[string]string) + + if region != "" { + labels["region"] = region + } + if env != "" { + labels["env"] = env + } + + for _, pair := range strings.Split(labelsRaw, ",") { + pair = strings.TrimSpace(pair) + if pair == "" { + continue + } + kv := strings.SplitN(pair, "=", 2) + if len(kv) != 2 { + continue + } + key := strings.TrimSpace(kv[0]) + value := strings.TrimSpace(kv[1]) + if key == "" || value == "" { + continue + } + labels[key] = value + } + + return labels +} + func getHostname() string { hostname, err := os.Hostname() if err != nil { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0055564..5ab6c48 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,6 +1,9 @@ package config -import "testing" +import ( + "os" + "testing" +) func TestValidate_InvalidPort(t *testing.T) { cfg := &Config{GRPCPort: 70000, StateStorePath: "/tmp/state.db", DockerEnabled: true} @@ -22,3 +25,70 @@ func TestValidate_AtLeastOneRuntime(t *testing.T) { t.Fatal("expected runtime validation error") } } + +func TestLoad_NodeLabelsFromEnv(t *testing.T) { + t.Setenv("PERSYS_GRPC_PORT", "50051") + t.Setenv("PERSYS_STATE_PATH", "/tmp/state.db") + t.Setenv("PERSYS_DOCKER_ENABLED", "true") + t.Setenv("PERSYS_COMPOSE_ENABLED", "false") + t.Setenv("PERSYS_VM_ENABLED", "false") + t.Setenv("PERSYS_TLS_ENABLED", "false") + t.Setenv("PERSYS_NODE_REGION", "us-east-1") + t.Setenv("PERSYS_NODE_ENV", "prod") + t.Setenv("PERSYS_NODE_LABELS", "team=platform,zone=use1-az2,invalid,noequal=,=novalue") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if got := cfg.NodeLabels["region"]; got != "us-east-1" { + t.Fatalf("expected region label, got %q", got) + } + if got := cfg.NodeLabels["env"]; got != "prod" { + t.Fatalf("expected env label, got %q", got) + } + if got := cfg.NodeLabels["team"]; got != "platform" { + t.Fatalf("expected team label, got %q", got) + } + if got := cfg.NodeLabels["zone"]; got != "use1-az2" { + t.Fatalf("expected zone label, got %q", got) + } + if _, ok := cfg.NodeLabels["invalid"]; ok { + t.Fatal("did not expect invalid label key") + } +} + +func TestLoad_SchedulerAddrFromEnv(t *testing.T) { + for _, k := range []string{ + "PERSYS_GRPC_PORT", + "PERSYS_STATE_PATH", + "PERSYS_DOCKER_ENABLED", + "PERSYS_COMPOSE_ENABLED", + "PERSYS_VM_ENABLED", + "PERSYS_TLS_ENABLED", + "PERSYS_SCHEDULER_ADDR", + "PERSYS_SCHEDULER_INSECURE", + } { + _ = os.Unsetenv(k) + } + t.Setenv("PERSYS_GRPC_PORT", "50051") + t.Setenv("PERSYS_STATE_PATH", "/tmp/state.db") + t.Setenv("PERSYS_DOCKER_ENABLED", "true") + t.Setenv("PERSYS_COMPOSE_ENABLED", "false") + t.Setenv("PERSYS_VM_ENABLED", "false") + t.Setenv("PERSYS_TLS_ENABLED", "false") + t.Setenv("PERSYS_SCHEDULER_ADDR", "10.0.0.9:8085") + t.Setenv("PERSYS_SCHEDULER_INSECURE", "true") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if cfg.SchedulerAddr != "10.0.0.9:8085" { + t.Fatalf("expected scheduler addr from env, got %q", cfg.SchedulerAddr) + } + if !cfg.SchedulerInsecure { + t.Fatal("expected scheduler insecure mode to be true") + } +} From 23284635fb95af7581a43d5a59be9d46bd2a39bd Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 17:56:10 +0330 Subject: [PATCH 21/25] Feat: Add Control Plane gRPC Client --- internal/control/client.go | 545 +++++++++++++++++++++++++++++++++++++ 1 file changed, 545 insertions(+) create mode 100644 internal/control/client.go diff --git a/internal/control/client.go b/internal/control/client.go new file mode 100644 index 0000000..798440c --- /dev/null +++ b/internal/control/client.go @@ -0,0 +1,545 @@ +package control + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "math" + "net" + "os" + "strconv" + "strings" + "time" + + "github.com/persys/compute-agent/internal/config" + "github.com/persys/compute-agent/internal/resources" + "github.com/persys/compute-agent/internal/runtime" + "github.com/persys/compute-agent/internal/workload" + controlv1 "github.com/persys/compute-agent/pkg/control/v1" + "github.com/persys/compute-agent/pkg/models" + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/mem" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + defaultRPCTimeout = 10 * time.Second + defaultHeartbeatSeconds = 10 +) + +// Client manages scheduler control-plane communication. +type Client struct { + cfg *config.Config + workloadManager *workload.Manager + runtimeMgr *runtime.Manager + resourceMonitor *resources.Monitor + logger *logrus.Entry +} + +func NewClient(cfg *config.Config, workloadManager *workload.Manager, runtimeMgr *runtime.Manager, resourceMonitor *resources.Monitor, logger *logrus.Logger) *Client { + return &Client{ + cfg: cfg, + workloadManager: workloadManager, + runtimeMgr: runtimeMgr, + resourceMonitor: resourceMonitor, + logger: logger.WithField("component", "scheduler-control-client"), + } +} + +// ApplyWorkload forwards workload intent to the scheduler control plane. +func (c *Client) ApplyWorkload(ctx context.Context, req *controlv1.ApplyWorkloadRequest) (*controlv1.ApplyWorkloadResponse, error) { + conn, client, err := c.dial(ctx) + if err != nil { + return nil, err + } + defer conn.Close() + + rpcCtx, cancel := context.WithTimeout(ctx, defaultRPCTimeout) + defer cancel() + return client.ApplyWorkload(rpcCtx, req) +} + +// DeleteWorkload forwards workload deletion intent to the scheduler control plane. +func (c *Client) DeleteWorkload(ctx context.Context, req *controlv1.DeleteWorkloadRequest) (*controlv1.DeleteWorkloadResponse, error) { + conn, client, err := c.dial(ctx) + if err != nil { + return nil, err + } + defer conn.Close() + + rpcCtx, cancel := context.WithTimeout(ctx, defaultRPCTimeout) + defer cancel() + return client.DeleteWorkload(rpcCtx, req) +} + +// RetryWorkload forwards retry intent to the scheduler control plane. +func (c *Client) RetryWorkload(ctx context.Context, req *controlv1.RetryWorkloadRequest) (*controlv1.RetryWorkloadResponse, error) { + conn, client, err := c.dial(ctx) + if err != nil { + return nil, err + } + defer conn.Close() + + rpcCtx, cancel := context.WithTimeout(ctx, defaultRPCTimeout) + defer cancel() + return client.RetryWorkload(rpcCtx, req) +} + +// Run starts registration/heartbeat loop and blocks until ctx is canceled. +func (c *Client) Run(ctx context.Context) { + backoff := time.Second + + for { + if ctx.Err() != nil { + return + } + + conn, client, err := c.dial(ctx) + if err != nil { + c.logger.WithError(err).Warn("Failed to connect to scheduler control endpoint") + if !sleepWithContext(ctx, backoff) { + return + } + backoff = nextBackoff(backoff) + continue + } + + regResp, err := c.register(ctx, client) + if err != nil { + c.logger.WithError(err).Warn("Node registration failed") + _ = conn.Close() + if !sleepWithContext(ctx, backoff) { + return + } + backoff = nextBackoff(backoff) + continue + } + if !regResp.GetAccepted() { + c.logger.WithField("reason", regResp.GetReason()).Warn("Scheduler rejected node registration") + _ = conn.Close() + if !sleepWithContext(ctx, backoff) { + return + } + backoff = nextBackoff(backoff) + continue + } + + backoff = time.Second + interval := time.Duration(regResp.GetHeartbeatIntervalSeconds()) * time.Second + if interval <= 0 { + interval = defaultHeartbeatSeconds * time.Second + } + leaseExpiry := timestampToTime(regResp.GetLeaseExpiresAt()) + + c.logger.WithFields(logrus.Fields{ + "heartbeat_interval": interval, + "lease_expires_at": leaseExpiry.Format(time.RFC3339), + }).Info("Successfully registered node with scheduler") + + if _, err := c.heartbeat(ctx, client); err != nil { + c.logger.WithError(err).Warn("Initial heartbeat failed after registration") + _ = conn.Close() + continue + } + + ticker := time.NewTicker(interval) + reconnect := false + for !reconnect { + select { + case <-ctx.Done(): + ticker.Stop() + _ = conn.Close() + return + case <-ticker.C: + if !leaseExpiry.IsZero() && time.Now().After(leaseExpiry) { + c.logger.Warn("Node lease expired; re-registering") + reconnect = true + continue + } + + hbResp, err := c.heartbeat(ctx, client) + if err != nil { + c.logger.WithError(err).Warn("Heartbeat failed; reconnecting") + reconnect = true + continue + } + if hbResp.GetLeaseExpiresAt() != nil { + leaseExpiry = timestampToTime(hbResp.GetLeaseExpiresAt()) + } + if hbResp.GetDrainNode() { + c.logger.Warn("Scheduler requested drain mode for this node") + } + if !hbResp.GetAcknowledged() { + c.logger.Warn("Heartbeat was not acknowledged") + } + } + } + + ticker.Stop() + _ = conn.Close() + } +} + +func (c *Client) register(ctx context.Context, client controlv1.AgentControlClient) (*controlv1.RegisterNodeResponse, error) { + capabilities, err := c.nodeCapabilities() + if err != nil { + return nil, err + } + + rpcCtx, cancel := context.WithTimeout(ctx, defaultRPCTimeout) + defer cancel() + + supported := c.supportedWorkloadTypes() + c.logger.WithFields(logrus.Fields{ + "node_id": c.cfg.NodeID, + "grpc_endpoint": c.agentEndpoint(), + "supported_workload_types": strings.Join(supported, ","), + }).Info("Registering node with scheduler") + + return client.RegisterNode(rpcCtx, &controlv1.RegisterNodeRequest{ + NodeId: c.cfg.NodeID, + Capabilities: capabilities, + Labels: c.cfg.NodeLabels, + AgentVersion: c.cfg.Version, + GrpcEndpoint: c.agentEndpoint(), + Timestamp: timestamppb.Now(), + }) +} + +func (c *Client) heartbeat(ctx context.Context, client controlv1.AgentControlClient) (*controlv1.HeartbeatResponse, error) { + usage := c.nodeUsage() + workloadStatuses := c.workloadStatuses(ctx) + + rpcCtx, cancel := context.WithTimeout(ctx, defaultRPCTimeout) + defer cancel() + + return client.Heartbeat(rpcCtx, &controlv1.HeartbeatRequest{ + NodeId: c.cfg.NodeID, + Usage: usage, + WorkloadStatuses: workloadStatuses, + Timestamp: timestamppb.Now(), + }) +} + +func (c *Client) dial(ctx context.Context) (*grpc.ClientConn, controlv1.AgentControlClient, error) { + dialCtx, cancel := context.WithTimeout(ctx, defaultRPCTimeout) + defer cancel() + + opts, err := c.dialOptions() + if err != nil { + return nil, nil, err + } + + conn, err := grpc.DialContext(dialCtx, c.cfg.SchedulerAddr, opts...) + if err != nil { + return nil, nil, err + } + + return conn, controlv1.NewAgentControlClient(conn), nil +} + +func (c *Client) dialOptions() ([]grpc.DialOption, error) { + if c.cfg.SchedulerInsecure || !c.cfg.SchedulerTLSEnabled { + return []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}, nil + } + + cert, err := tls.LoadX509KeyPair(c.cfg.TLSCertPath, c.cfg.TLSKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to load client cert/key: %w", err) + } + + caCert, err := os.ReadFile(c.cfg.TLSCAPath) + if err != nil { + return nil, fmt.Errorf("failed to read CA certificate: %w", err) + } + + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("failed to parse CA certificate") + } + + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: pool, + Certificates: []tls.Certificate{cert}, + } + + return []grpc.DialOption{grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))}, nil +} + +func (c *Client) nodeCapabilities() (*controlv1.NodeCapabilities, error) { + cpuCount, err := cpu.Counts(true) + if err != nil { + return nil, fmt.Errorf("failed to get CPU count: %w", err) + } + memStats, err := mem.VirtualMemory() + if err != nil { + return nil, fmt.Errorf("failed to get memory stats: %w", err) + } + storagePools := []*controlv1.StoragePool{} + if diskStats, err := disk.Usage("/"); err == nil { + storagePools = append(storagePools, &controlv1.StoragePool{ + Name: "root", + Type: "local", + TotalGb: int64(diskStats.Total / 1024 / 1024 / 1024), + }) + } + + return &controlv1.NodeCapabilities{ + CpuTotalMillicores: int64(cpuCount * 1000), + MemoryTotalMb: int64(memStats.Total / 1024 / 1024), + StoragePools: storagePools, + SupportedWorkloadTypes: c.supportedWorkloadTypes(), + }, nil +} + +func (c *Client) nodeUsage() *controlv1.NodeUsage { + var cpuTotalMillicores int64 + if count, err := cpu.Counts(true); err == nil { + cpuTotalMillicores = int64(count * 1000) + } + + var cpuUsedMillicores int64 + var memoryUsedMB int64 + var diskUsedGB int64 + + if c.resourceMonitor != nil { + if util, err := c.resourceMonitor.GetUtilization([]string{"/"}); err == nil { + cpuUsedMillicores = int64(math.Round(float64(cpuTotalMillicores) * (util.CPUPercent / 100.0))) + if usage, ok := util.DiskPercent["/"]; ok { + if diskStats, err := disk.Usage("/"); err == nil { + diskUsedGB = int64(math.Round(float64(diskStats.Total/1024/1024/1024) * (usage / 100.0))) + } + } + } + } + + if vm, err := mem.VirtualMemory(); err == nil { + memoryUsedMB = int64(vm.Used / 1024 / 1024) + } + if diskStats, err := disk.Usage("/"); err == nil && diskUsedGB == 0 { + diskUsedGB = int64(diskStats.Used / 1024 / 1024 / 1024) + } + + return &controlv1.NodeUsage{ + CpuAllocatedMillicores: 0, + CpuUsedMillicores: cpuUsedMillicores, + MemoryAllocatedMb: 0, + MemoryUsedMb: memoryUsedMB, + DiskAllocatedGb: 0, + DiskUsedGb: diskUsedGB, + } +} + +func (c *Client) workloadStatuses(ctx context.Context) []*controlv1.WorkloadStatus { + if c.workloadManager == nil { + return nil + } + statuses, err := c.workloadManager.ListWorkloads(ctx, nil) + if err != nil { + c.logger.WithError(err).Warn("Failed to list workload statuses for heartbeat") + return nil + } + + out := make([]*controlv1.WorkloadStatus, 0, len(statuses)) + for _, status := range statuses { + if status == nil { + continue + } + + out = append(out, &controlv1.WorkloadStatus{ + WorkloadId: status.ID, + State: normalizedState(string(status.ActualState)), + FailureReason: mapFailureReason(status), + Message: status.Message, + LastTransition: timestamppb.New(nonZeroTime(status.UpdatedAt)), + }) + } + + return out +} + +func (c *Client) supportedWorkloadTypes() []string { + types := make([]string, 0, 3) + if c.runtimeMgr != nil && c.runtimeMgr.IsEnabled(models.WorkloadTypeContainer) { + types = append(types, "container") + } + if c.runtimeMgr != nil && c.runtimeMgr.IsEnabled(models.WorkloadTypeCompose) { + types = append(types, "compose") + } + if c.runtimeMgr != nil && c.runtimeMgr.IsEnabled(models.WorkloadTypeVM) { + types = append(types, "vm") + } + + // Fallback to config flags if runtime manager is unavailable. + if c.runtimeMgr == nil { + if c.cfg.DockerEnabled { + types = append(types, "container") + } + if c.cfg.ComposeEnabled { + types = append(types, "compose") + } + if c.cfg.VMEnabled { + types = append(types, "vm") + } + } + return types +} + +func (c *Client) agentEndpoint() string { + if c.cfg.AgentGRPCEndpoint != "" { + return c.cfg.AgentGRPCEndpoint + } + + host := c.cfg.GRPCAddr + if isWildcardHost(host) || strings.EqualFold(host, "localhost") { + if ip, err := outboundLocalIP(c.cfg.SchedulerAddr); err == nil && ip != "" { + host = ip + } else if ip, err := firstNonLoopbackIP(); err == nil && ip != "" { + host = ip + } else { + host = "127.0.0.1" + } + } + + return net.JoinHostPort(host, strconv.Itoa(c.cfg.GRPCPort)) +} + +func normalizedState(in string) string { + if in == "" { + return "Unknown" + } + s := strings.ToLower(strings.TrimSpace(in)) + switch s { + case "running": + return "Running" + case "stopped": + return "Stopped" + case "failed": + return "Failed" + case "pending": + return "Pending" + default: + return "Unknown" + } +} + +func mapFailureReason(status *models.WorkloadStatus) controlv1.FailureReason { + if status == nil { + return controlv1.FailureReason_FAILURE_REASON_UNSPECIFIED + } + if strings.EqualFold(string(status.ActualState), "failed") { + return controlv1.FailureReason_RUNTIME_ERROR + } + return controlv1.FailureReason_FAILURE_REASON_UNSPECIFIED +} + +func nonZeroTime(t time.Time) time.Time { + if t.IsZero() { + return time.Now() + } + return t +} + +func timestampToTime(ts *timestamppb.Timestamp) time.Time { + if ts == nil { + return time.Time{} + } + return ts.AsTime() +} + +func sleepWithContext(ctx context.Context, d time.Duration) bool { + t := time.NewTimer(d) + defer t.Stop() + + select { + case <-ctx.Done(): + return false + case <-t.C: + return true + } +} + +func nextBackoff(current time.Duration) time.Duration { + next := current * 2 + if next > 30*time.Second { + return 30 * time.Second + } + return next +} + +func isWildcardHost(host string) bool { + h := strings.TrimSpace(strings.ToLower(host)) + return h == "" || h == "0.0.0.0" || h == "::" || h == "[::]" +} + +// outboundLocalIP picks the local source IP for traffic toward the scheduler. +// This avoids advertising hostnames that are not resolvable from scheduler. +func outboundLocalIP(schedulerAddr string) (string, error) { + target := schedulerAddr + if _, _, err := net.SplitHostPort(target); err != nil { + // Handle bare host/IP values without explicit port. + if strings.Contains(err.Error(), "missing port in address") { + target = net.JoinHostPort(target, "80") + } else { + return "", err + } + } + + conn, err := net.Dial("udp", target) + if err != nil { + return "", err + } + defer conn.Close() + + udpAddr, ok := conn.LocalAddr().(*net.UDPAddr) + if !ok || udpAddr.IP == nil { + return "", fmt.Errorf("could not determine local UDP address") + } + if !udpAddr.IP.IsGlobalUnicast() { + return "", fmt.Errorf("local IP is not global unicast: %s", udpAddr.IP.String()) + } + return udpAddr.IP.String(), nil +} + +func firstNonLoopbackIP() (string, error) { + ifaces, err := net.Interfaces() + if err != nil { + return "", err + } + + var ipv6Candidate string + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, addr := range addrs { + ipNet, ok := addr.(*net.IPNet) + if !ok || ipNet.IP == nil || !ipNet.IP.IsGlobalUnicast() { + continue + } + if v4 := ipNet.IP.To4(); v4 != nil { + return v4.String(), nil + } + if ipv6Candidate == "" { + ipv6Candidate = ipNet.IP.String() + } + } + } + + if ipv6Candidate != "" { + return ipv6Candidate, nil + } + return "", fmt.Errorf("no non-loopback IP address found") +} From c5362f72260e0e021909e098cf5040081bc908c7 Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 17:56:55 +0330 Subject: [PATCH 22/25] Fix: Garbage Collector Deleting non persys managed resources --- internal/garbage/collector.go | 11 ++++ internal/garbage/collector_test.go | 83 +++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/internal/garbage/collector.go b/internal/garbage/collector.go index a5093b7..87c58af 100644 --- a/internal/garbage/collector.go +++ b/internal/garbage/collector.go @@ -144,6 +144,13 @@ func (c *Collector) collectOrphanedResources(ctx context.Context) int { models.WorkloadTypeCompose, models.WorkloadTypeVM, } { + // VM runtime lists all domains on the host; without an ownership marker query, + // deleting "orphaned" VMs is unsafe and may remove non-agent resources. + if workloadType == models.WorkloadTypeVM { + c.logger.Debug("Skipping VM orphaned-resource GC due to unknown ownership scope") + continue + } + rt, err := c.runtimeMgr.GetRuntime(workloadType) if err != nil { continue // Runtime not available @@ -289,6 +296,10 @@ func (c *Collector) GetStats(ctx context.Context) *Stats { models.WorkloadTypeCompose, models.WorkloadTypeVM, } { + if workloadType == models.WorkloadTypeVM { + continue + } + rt, err := c.runtimeMgr.GetRuntime(workloadType) if err != nil { continue diff --git a/internal/garbage/collector_test.go b/internal/garbage/collector_test.go index fd60bc1..3c67b77 100644 --- a/internal/garbage/collector_test.go +++ b/internal/garbage/collector_test.go @@ -1,6 +1,14 @@ package garbage -import "testing" +import ( + "context" + "testing" + "time" + + "github.com/persys/compute-agent/internal/runtime" + "github.com/persys/compute-agent/pkg/models" + "github.com/sirupsen/logrus" +) func TestDefaultConfig(t *testing.T) { cfg := DefaultConfig() @@ -11,3 +19,76 @@ func TestDefaultConfig(t *testing.T) { t.Fatal("expected positive gc intervals") } } + +func TestCollectOrphanedResources_SkipsVMRuntimeDeletion(t *testing.T) { + rtMgr := runtime.NewManager() + vmStub := &gcRuntimeStub{ + typ: models.WorkloadTypeVM, + items: []string{"external-vm"}, + } + rtMgr.Register(vmStub) + + store := &gcStoreStub{} + logger := logrus.New() + logger.SetLevel(logrus.DebugLevel) + + collector := NewCollector(&CollectorConfig{ + Enabled: true, + Interval: time.Minute, + FailedWorkloadTTL: time.Hour, + MaxOrphanedResourceAge: time.Hour, + }, store, rtMgr, logger) + + deleted := collector.collectOrphanedResources(context.Background()) + if deleted != 0 { + t.Fatalf("expected no orphaned deletions for VM runtime, got %d", deleted) + } + if len(vmStub.deletedIDs) != 0 { + t.Fatalf("expected VM delete not called, got deletions: %v", vmStub.deletedIDs) + } +} + +type gcStoreStub struct{} + +func (s *gcStoreStub) SaveWorkload(workload *models.Workload) error { return nil } +func (s *gcStoreStub) GetWorkload(id string) (*models.Workload, error) { + return nil, stateErrNotFound +} +func (s *gcStoreStub) DeleteWorkload(id string) error { return nil } +func (s *gcStoreStub) ListWorkloads() ([]*models.Workload, error) { + return []*models.Workload{}, nil +} +func (s *gcStoreStub) SaveStatus(status *models.WorkloadStatus) error { return nil } +func (s *gcStoreStub) GetStatus(id string) (*models.WorkloadStatus, error) { + return nil, stateErrNotFound +} +func (s *gcStoreStub) ListStatuses() ([]*models.WorkloadStatus, error) { + return []*models.WorkloadStatus{}, nil +} +func (s *gcStoreStub) Close() error { return nil } + +var stateErrNotFound = stateNotFoundError("not found") + +type stateNotFoundError string + +func (e stateNotFoundError) Error() string { return string(e) } + +type gcRuntimeStub struct { + typ models.WorkloadType + items []string + deletedIDs []string +} + +func (r *gcRuntimeStub) Create(ctx context.Context, workload *models.Workload) error { return nil } +func (r *gcRuntimeStub) Start(ctx context.Context, id string) error { return nil } +func (r *gcRuntimeStub) Stop(ctx context.Context, id string) error { return nil } +func (r *gcRuntimeStub) Delete(ctx context.Context, id string) error { + r.deletedIDs = append(r.deletedIDs, id) + return nil +} +func (r *gcRuntimeStub) Status(ctx context.Context, id string) (models.ActualState, string, error) { + return models.ActualStateRunning, "running", nil +} +func (r *gcRuntimeStub) List(ctx context.Context) ([]string, error) { return r.items, nil } +func (r *gcRuntimeStub) Type() models.WorkloadType { return r.typ } +func (r *gcRuntimeStub) Healthy(ctx context.Context) error { return nil } From 78e8030dbe73310f0d62124e612717f76a595e5a Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 17:57:49 +0330 Subject: [PATCH 23/25] Feat: Add Label Markers to persys workloads for GC --- internal/runtime/docker.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/internal/runtime/docker.go b/internal/runtime/docker.go index 2d1ae7d..ec18598 100644 --- a/internal/runtime/docker.go +++ b/internal/runtime/docker.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" @@ -23,6 +24,11 @@ type DockerRuntime struct { logger *logrus.Entry } +const ( + managedLabelKey = "persys.managed" + managedWorkloadIDKey = "persys.workload_id" +) + // NewDockerRuntime creates a new Docker runtime func NewDockerRuntime(endpoint string, logger *logrus.Logger) (*DockerRuntime, error) { var cli *client.Client @@ -66,11 +72,19 @@ func (d *DockerRuntime) Create(ctx context.Context, workload *models.Workload) e d.logger.Infof("Successfully pulled image: %s", spec.Image) // Build container config + labels := make(map[string]string, len(spec.Labels)+2) + for k, v := range spec.Labels { + labels[k] = v + } + // Force ownership labels so GC/reconciliation only targets agent-managed containers. + labels[managedLabelKey] = "true" + labels[managedWorkloadIDKey] = workload.ID + containerConfig := &container.Config{ Image: spec.Image, Cmd: spec.Command, Env: d.buildEnv(spec.Env), - Labels: spec.Labels, + Labels: labels, } if len(spec.Args) > 0 { @@ -180,7 +194,13 @@ func (d *DockerRuntime) Status(ctx context.Context, id string) (models.ActualSta } func (d *DockerRuntime) List(ctx context.Context) ([]string, error) { - containers, err := d.client.ContainerList(ctx, container.ListOptions{All: true}) + filter := filters.NewArgs() + filter.Add("label", managedLabelKey+"=true") + + containers, err := d.client.ContainerList(ctx, container.ListOptions{ + All: true, + Filters: filter, + }) if err != nil { return nil, fmt.Errorf("failed to list containers: %w", err) } From 5007dc366946968f2d11ad0189a6afb5f739fbe7 Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 17:58:18 +0330 Subject: [PATCH 24/25] Feat: proto for communicating to scheduler (control plane) --- pkg/control/v1/control.pb.go | 2133 +++++++++++++++++++++++++++++ pkg/control/v1/control_grpc.pb.go | 316 +++++ 2 files changed, 2449 insertions(+) create mode 100644 pkg/control/v1/control.pb.go create mode 100644 pkg/control/v1/control_grpc.pb.go diff --git a/pkg/control/v1/control.pb.go b/pkg/control/v1/control.pb.go new file mode 100644 index 0000000..7161d91 --- /dev/null +++ b/pkg/control/v1/control.pb.go @@ -0,0 +1,2133 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.21.12 +// source: control.proto + +package controlv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type FailureReason int32 + +const ( + FailureReason_FAILURE_REASON_UNSPECIFIED FailureReason = 0 + FailureReason_IMAGE_PULL_FAILED FailureReason = 1 + FailureReason_IMAGE_NOT_FOUND FailureReason = 2 + FailureReason_INSUFFICIENT_RESOURCES FailureReason = 3 + FailureReason_INVALID_SPEC FailureReason = 4 + FailureReason_RUNTIME_ERROR FailureReason = 5 + FailureReason_NETWORK_ERROR FailureReason = 6 + FailureReason_STORAGE_ERROR FailureReason = 7 + FailureReason_VM_BOOT_FAILED FailureReason = 8 +) + +// Enum value maps for FailureReason. +var ( + FailureReason_name = map[int32]string{ + 0: "FAILURE_REASON_UNSPECIFIED", + 1: "IMAGE_PULL_FAILED", + 2: "IMAGE_NOT_FOUND", + 3: "INSUFFICIENT_RESOURCES", + 4: "INVALID_SPEC", + 5: "RUNTIME_ERROR", + 6: "NETWORK_ERROR", + 7: "STORAGE_ERROR", + 8: "VM_BOOT_FAILED", + } + FailureReason_value = map[string]int32{ + "FAILURE_REASON_UNSPECIFIED": 0, + "IMAGE_PULL_FAILED": 1, + "IMAGE_NOT_FOUND": 2, + "INSUFFICIENT_RESOURCES": 3, + "INVALID_SPEC": 4, + "RUNTIME_ERROR": 5, + "NETWORK_ERROR": 6, + "STORAGE_ERROR": 7, + "VM_BOOT_FAILED": 8, + } +) + +func (x FailureReason) Enum() *FailureReason { + p := new(FailureReason) + *p = x + return p +} + +func (x FailureReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (FailureReason) Descriptor() protoreflect.EnumDescriptor { + return file_control_proto_enumTypes[0].Descriptor() +} + +func (FailureReason) Type() protoreflect.EnumType { + return &file_control_proto_enumTypes[0] +} + +func (x FailureReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use FailureReason.Descriptor instead. +func (FailureReason) EnumDescriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{0} +} + +type RegisterNodeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Capabilities *NodeCapabilities `protobuf:"bytes,2,opt,name=capabilities,proto3" json:"capabilities,omitempty"` + Labels map[string]string `protobuf:"bytes,3,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + AgentVersion string `protobuf:"bytes,4,opt,name=agent_version,json=agentVersion,proto3" json:"agent_version,omitempty"` + GrpcEndpoint string `protobuf:"bytes,5,opt,name=grpc_endpoint,json=grpcEndpoint,proto3" json:"grpc_endpoint,omitempty"` + ClusterId string `protobuf:"bytes,6,opt,name=cluster_id,json=clusterId,proto3" json:"cluster_id,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterNodeRequest) Reset() { + *x = RegisterNodeRequest{} + mi := &file_control_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterNodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterNodeRequest) ProtoMessage() {} + +func (x *RegisterNodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[0] + 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 RegisterNodeRequest.ProtoReflect.Descriptor instead. +func (*RegisterNodeRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{0} +} + +func (x *RegisterNodeRequest) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *RegisterNodeRequest) GetCapabilities() *NodeCapabilities { + if x != nil { + return x.Capabilities + } + return nil +} + +func (x *RegisterNodeRequest) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +func (x *RegisterNodeRequest) GetAgentVersion() string { + if x != nil { + return x.AgentVersion + } + return "" +} + +func (x *RegisterNodeRequest) GetGrpcEndpoint() string { + if x != nil { + return x.GrpcEndpoint + } + return "" +} + +func (x *RegisterNodeRequest) GetClusterId() string { + if x != nil { + return x.ClusterId + } + return "" +} + +func (x *RegisterNodeRequest) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +type NodeCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + CpuTotalMillicores int64 `protobuf:"varint,1,opt,name=cpu_total_millicores,json=cpuTotalMillicores,proto3" json:"cpu_total_millicores,omitempty"` + MemoryTotalMb int64 `protobuf:"varint,2,opt,name=memory_total_mb,json=memoryTotalMb,proto3" json:"memory_total_mb,omitempty"` + StoragePools []*StoragePool `protobuf:"bytes,3,rep,name=storage_pools,json=storagePools,proto3" json:"storage_pools,omitempty"` + SupportedWorkloadTypes []string `protobuf:"bytes,4,rep,name=supported_workload_types,json=supportedWorkloadTypes,proto3" json:"supported_workload_types,omitempty"` // container, compose, vm + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NodeCapabilities) Reset() { + *x = NodeCapabilities{} + mi := &file_control_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NodeCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeCapabilities) ProtoMessage() {} + +func (x *NodeCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[1] + 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 NodeCapabilities.ProtoReflect.Descriptor instead. +func (*NodeCapabilities) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{1} +} + +func (x *NodeCapabilities) GetCpuTotalMillicores() int64 { + if x != nil { + return x.CpuTotalMillicores + } + return 0 +} + +func (x *NodeCapabilities) GetMemoryTotalMb() int64 { + if x != nil { + return x.MemoryTotalMb + } + return 0 +} + +func (x *NodeCapabilities) GetStoragePools() []*StoragePool { + if x != nil { + return x.StoragePools + } + return nil +} + +func (x *NodeCapabilities) GetSupportedWorkloadTypes() []string { + if x != nil { + return x.SupportedWorkloadTypes + } + return nil +} + +type StoragePool struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` // local, nfs, iscsi + TotalGb int64 `protobuf:"varint,3,opt,name=total_gb,json=totalGb,proto3" json:"total_gb,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StoragePool) Reset() { + *x = StoragePool{} + mi := &file_control_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StoragePool) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StoragePool) ProtoMessage() {} + +func (x *StoragePool) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[2] + 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 StoragePool.ProtoReflect.Descriptor instead. +func (*StoragePool) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{2} +} + +func (x *StoragePool) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *StoragePool) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *StoragePool) GetTotalGb() int64 { + if x != nil { + return x.TotalGb + } + return 0 +} + +type RegisterNodeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"` + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + HeartbeatIntervalSeconds int32 `protobuf:"varint,3,opt,name=heartbeat_interval_seconds,json=heartbeatIntervalSeconds,proto3" json:"heartbeat_interval_seconds,omitempty"` + LeaseExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=lease_expires_at,json=leaseExpiresAt,proto3" json:"lease_expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterNodeResponse) Reset() { + *x = RegisterNodeResponse{} + mi := &file_control_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterNodeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterNodeResponse) ProtoMessage() {} + +func (x *RegisterNodeResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[3] + 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 RegisterNodeResponse.ProtoReflect.Descriptor instead. +func (*RegisterNodeResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{3} +} + +func (x *RegisterNodeResponse) GetAccepted() bool { + if x != nil { + return x.Accepted + } + return false +} + +func (x *RegisterNodeResponse) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *RegisterNodeResponse) GetHeartbeatIntervalSeconds() int32 { + if x != nil { + return x.HeartbeatIntervalSeconds + } + return 0 +} + +func (x *RegisterNodeResponse) GetLeaseExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.LeaseExpiresAt + } + return nil +} + +type HeartbeatRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Usage *NodeUsage `protobuf:"bytes,2,opt,name=usage,proto3" json:"usage,omitempty"` + WorkloadStatuses []*WorkloadStatus `protobuf:"bytes,3,rep,name=workload_statuses,json=workloadStatuses,proto3" json:"workload_statuses,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeartbeatRequest) Reset() { + *x = HeartbeatRequest{} + mi := &file_control_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeartbeatRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatRequest) ProtoMessage() {} + +func (x *HeartbeatRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[4] + 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 HeartbeatRequest.ProtoReflect.Descriptor instead. +func (*HeartbeatRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{4} +} + +func (x *HeartbeatRequest) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *HeartbeatRequest) GetUsage() *NodeUsage { + if x != nil { + return x.Usage + } + return nil +} + +func (x *HeartbeatRequest) GetWorkloadStatuses() []*WorkloadStatus { + if x != nil { + return x.WorkloadStatuses + } + return nil +} + +func (x *HeartbeatRequest) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +type NodeUsage struct { + state protoimpl.MessageState `protogen:"open.v1"` + CpuAllocatedMillicores int64 `protobuf:"varint,1,opt,name=cpu_allocated_millicores,json=cpuAllocatedMillicores,proto3" json:"cpu_allocated_millicores,omitempty"` + CpuUsedMillicores int64 `protobuf:"varint,2,opt,name=cpu_used_millicores,json=cpuUsedMillicores,proto3" json:"cpu_used_millicores,omitempty"` + MemoryAllocatedMb int64 `protobuf:"varint,3,opt,name=memory_allocated_mb,json=memoryAllocatedMb,proto3" json:"memory_allocated_mb,omitempty"` + MemoryUsedMb int64 `protobuf:"varint,4,opt,name=memory_used_mb,json=memoryUsedMb,proto3" json:"memory_used_mb,omitempty"` + DiskAllocatedGb int64 `protobuf:"varint,5,opt,name=disk_allocated_gb,json=diskAllocatedGb,proto3" json:"disk_allocated_gb,omitempty"` + DiskUsedGb int64 `protobuf:"varint,6,opt,name=disk_used_gb,json=diskUsedGb,proto3" json:"disk_used_gb,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NodeUsage) Reset() { + *x = NodeUsage{} + mi := &file_control_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NodeUsage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeUsage) ProtoMessage() {} + +func (x *NodeUsage) ProtoReflect() protoreflect.Message { + mi := &file_control_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 NodeUsage.ProtoReflect.Descriptor instead. +func (*NodeUsage) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{5} +} + +func (x *NodeUsage) GetCpuAllocatedMillicores() int64 { + if x != nil { + return x.CpuAllocatedMillicores + } + return 0 +} + +func (x *NodeUsage) GetCpuUsedMillicores() int64 { + if x != nil { + return x.CpuUsedMillicores + } + return 0 +} + +func (x *NodeUsage) GetMemoryAllocatedMb() int64 { + if x != nil { + return x.MemoryAllocatedMb + } + return 0 +} + +func (x *NodeUsage) GetMemoryUsedMb() int64 { + if x != nil { + return x.MemoryUsedMb + } + return 0 +} + +func (x *NodeUsage) GetDiskAllocatedGb() int64 { + if x != nil { + return x.DiskAllocatedGb + } + return 0 +} + +func (x *NodeUsage) GetDiskUsedGb() int64 { + if x != nil { + return x.DiskUsedGb + } + return 0 +} + +type HeartbeatResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Acknowledged bool `protobuf:"varint,1,opt,name=acknowledged,proto3" json:"acknowledged,omitempty"` + DrainNode bool `protobuf:"varint,2,opt,name=drain_node,json=drainNode,proto3" json:"drain_node,omitempty"` + LeaseExpiresAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=lease_expires_at,json=leaseExpiresAt,proto3" json:"lease_expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeartbeatResponse) Reset() { + *x = HeartbeatResponse{} + mi := &file_control_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeartbeatResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatResponse) ProtoMessage() {} + +func (x *HeartbeatResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[6] + 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 HeartbeatResponse.ProtoReflect.Descriptor instead. +func (*HeartbeatResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{6} +} + +func (x *HeartbeatResponse) GetAcknowledged() bool { + if x != nil { + return x.Acknowledged + } + return false +} + +func (x *HeartbeatResponse) GetDrainNode() bool { + if x != nil { + return x.DrainNode + } + return false +} + +func (x *HeartbeatResponse) GetLeaseExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.LeaseExpiresAt + } + return nil +} + +type ApplyWorkloadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkloadId string `protobuf:"bytes,1,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` + Spec *WorkloadSpec `protobuf:"bytes,2,opt,name=spec,proto3" json:"spec,omitempty"` + // Compatibility fields aligned with existing agent apply semantics. + RevisionId string `protobuf:"bytes,10,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + DesiredState string `protobuf:"bytes,11,opt,name=desired_state,json=desiredState,proto3" json:"desired_state,omitempty"` // Running | Stopped + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyWorkloadRequest) Reset() { + *x = ApplyWorkloadRequest{} + mi := &file_control_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyWorkloadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyWorkloadRequest) ProtoMessage() {} + +func (x *ApplyWorkloadRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[7] + 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 ApplyWorkloadRequest.ProtoReflect.Descriptor instead. +func (*ApplyWorkloadRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{7} +} + +func (x *ApplyWorkloadRequest) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +func (x *ApplyWorkloadRequest) GetSpec() *WorkloadSpec { + if x != nil { + return x.Spec + } + return nil +} + +func (x *ApplyWorkloadRequest) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *ApplyWorkloadRequest) GetDesiredState() string { + if x != nil { + return x.DesiredState + } + return "" +} + +type ApplyWorkloadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + FailureReason FailureReason `protobuf:"varint,2,opt,name=failure_reason,json=failureReason,proto3,enum=persys.control.v1.FailureReason" json:"failure_reason,omitempty"` + ErrorMessage string `protobuf:"bytes,3,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyWorkloadResponse) Reset() { + *x = ApplyWorkloadResponse{} + mi := &file_control_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyWorkloadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyWorkloadResponse) ProtoMessage() {} + +func (x *ApplyWorkloadResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[8] + 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 ApplyWorkloadResponse.ProtoReflect.Descriptor instead. +func (*ApplyWorkloadResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{8} +} + +func (x *ApplyWorkloadResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *ApplyWorkloadResponse) GetFailureReason() FailureReason { + if x != nil { + return x.FailureReason + } + return FailureReason_FAILURE_REASON_UNSPECIFIED +} + +func (x *ApplyWorkloadResponse) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +type DeleteWorkloadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkloadId string `protobuf:"bytes,1,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteWorkloadRequest) Reset() { + *x = DeleteWorkloadRequest{} + mi := &file_control_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteWorkloadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWorkloadRequest) ProtoMessage() {} + +func (x *DeleteWorkloadRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[9] + 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 DeleteWorkloadRequest.ProtoReflect.Descriptor instead. +func (*DeleteWorkloadRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{9} +} + +func (x *DeleteWorkloadRequest) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +type DeleteWorkloadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteWorkloadResponse) Reset() { + *x = DeleteWorkloadResponse{} + mi := &file_control_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteWorkloadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWorkloadResponse) ProtoMessage() {} + +func (x *DeleteWorkloadResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[10] + 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 DeleteWorkloadResponse.ProtoReflect.Descriptor instead. +func (*DeleteWorkloadResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{10} +} + +func (x *DeleteWorkloadResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *DeleteWorkloadResponse) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +type WorkloadSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // container, compose, vm + Resources *ResourceRequirements `protobuf:"bytes,2,opt,name=resources,proto3" json:"resources,omitempty"` + // Types that are valid to be assigned to Workload: + // + // *WorkloadSpec_Container + // *WorkloadSpec_Compose + // *WorkloadSpec_Vm + Workload isWorkloadSpec_Workload `protobuf_oneof:"workload"` + Metadata map[string]string `protobuf:"bytes,20,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkloadSpec) Reset() { + *x = WorkloadSpec{} + mi := &file_control_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkloadSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkloadSpec) ProtoMessage() {} + +func (x *WorkloadSpec) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[11] + 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 WorkloadSpec.ProtoReflect.Descriptor instead. +func (*WorkloadSpec) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{11} +} + +func (x *WorkloadSpec) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *WorkloadSpec) GetResources() *ResourceRequirements { + if x != nil { + return x.Resources + } + return nil +} + +func (x *WorkloadSpec) GetWorkload() isWorkloadSpec_Workload { + if x != nil { + return x.Workload + } + return nil +} + +func (x *WorkloadSpec) GetContainer() *ContainerSpec { + if x != nil { + if x, ok := x.Workload.(*WorkloadSpec_Container); ok { + return x.Container + } + } + return nil +} + +func (x *WorkloadSpec) GetCompose() *ComposeSpec { + if x != nil { + if x, ok := x.Workload.(*WorkloadSpec_Compose); ok { + return x.Compose + } + } + return nil +} + +func (x *WorkloadSpec) GetVm() *VMSpec { + if x != nil { + if x, ok := x.Workload.(*WorkloadSpec_Vm); ok { + return x.Vm + } + } + return nil +} + +func (x *WorkloadSpec) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +type isWorkloadSpec_Workload interface { + isWorkloadSpec_Workload() +} + +type WorkloadSpec_Container struct { + Container *ContainerSpec `protobuf:"bytes,10,opt,name=container,proto3,oneof"` +} + +type WorkloadSpec_Compose struct { + Compose *ComposeSpec `protobuf:"bytes,11,opt,name=compose,proto3,oneof"` +} + +type WorkloadSpec_Vm struct { + Vm *VMSpec `protobuf:"bytes,12,opt,name=vm,proto3,oneof"` +} + +func (*WorkloadSpec_Container) isWorkloadSpec_Workload() {} + +func (*WorkloadSpec_Compose) isWorkloadSpec_Workload() {} + +func (*WorkloadSpec_Vm) isWorkloadSpec_Workload() {} + +type ResourceRequirements struct { + state protoimpl.MessageState `protogen:"open.v1"` + CpuMillicores int64 `protobuf:"varint,1,opt,name=cpu_millicores,json=cpuMillicores,proto3" json:"cpu_millicores,omitempty"` + MemoryMb int64 `protobuf:"varint,2,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"` + DiskGb int64 `protobuf:"varint,3,opt,name=disk_gb,json=diskGb,proto3" json:"disk_gb,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceRequirements) Reset() { + *x = ResourceRequirements{} + mi := &file_control_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceRequirements) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceRequirements) ProtoMessage() {} + +func (x *ResourceRequirements) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[12] + 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 ResourceRequirements.ProtoReflect.Descriptor instead. +func (*ResourceRequirements) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{12} +} + +func (x *ResourceRequirements) GetCpuMillicores() int64 { + if x != nil { + return x.CpuMillicores + } + return 0 +} + +func (x *ResourceRequirements) GetMemoryMb() int64 { + if x != nil { + return x.MemoryMb + } + return 0 +} + +func (x *ResourceRequirements) GetDiskGb() int64 { + if x != nil { + return x.DiskGb + } + return 0 +} + +type ContainerSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + Image string `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"` + Command []string `protobuf:"bytes,2,rep,name=command,proto3" json:"command,omitempty"` + Env map[string]string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Volumes []*VolumeMount `protobuf:"bytes,4,rep,name=volumes,proto3" json:"volumes,omitempty"` + Ports []*Port `protobuf:"bytes,5,rep,name=ports,proto3" json:"ports,omitempty"` + RestartPolicy string `protobuf:"bytes,6,opt,name=restart_policy,json=restartPolicy,proto3" json:"restart_policy,omitempty"` + Privileged bool `protobuf:"varint,7,opt,name=privileged,proto3" json:"privileged,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContainerSpec) Reset() { + *x = ContainerSpec{} + mi := &file_control_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContainerSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContainerSpec) ProtoMessage() {} + +func (x *ContainerSpec) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[13] + 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 ContainerSpec.ProtoReflect.Descriptor instead. +func (*ContainerSpec) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{13} +} + +func (x *ContainerSpec) GetImage() string { + if x != nil { + return x.Image + } + return "" +} + +func (x *ContainerSpec) GetCommand() []string { + if x != nil { + return x.Command + } + return nil +} + +func (x *ContainerSpec) GetEnv() map[string]string { + if x != nil { + return x.Env + } + return nil +} + +func (x *ContainerSpec) GetVolumes() []*VolumeMount { + if x != nil { + return x.Volumes + } + return nil +} + +func (x *ContainerSpec) GetPorts() []*Port { + if x != nil { + return x.Ports + } + return nil +} + +func (x *ContainerSpec) GetRestartPolicy() string { + if x != nil { + return x.RestartPolicy + } + return "" +} + +func (x *ContainerSpec) GetPrivileged() bool { + if x != nil { + return x.Privileged + } + return false +} + +type VolumeMount struct { + state protoimpl.MessageState `protogen:"open.v1"` + HostPath string `protobuf:"bytes,1,opt,name=host_path,json=hostPath,proto3" json:"host_path,omitempty"` + ContainerPath string `protobuf:"bytes,2,opt,name=container_path,json=containerPath,proto3" json:"container_path,omitempty"` + ReadOnly bool `protobuf:"varint,3,opt,name=read_only,json=readOnly,proto3" json:"read_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VolumeMount) Reset() { + *x = VolumeMount{} + mi := &file_control_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VolumeMount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VolumeMount) ProtoMessage() {} + +func (x *VolumeMount) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[14] + 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 VolumeMount.ProtoReflect.Descriptor instead. +func (*VolumeMount) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{14} +} + +func (x *VolumeMount) GetHostPath() string { + if x != nil { + return x.HostPath + } + return "" +} + +func (x *VolumeMount) GetContainerPath() string { + if x != nil { + return x.ContainerPath + } + return "" +} + +func (x *VolumeMount) GetReadOnly() bool { + if x != nil { + return x.ReadOnly + } + return false +} + +type Port struct { + state protoimpl.MessageState `protogen:"open.v1"` + HostPort int32 `protobuf:"varint,1,opt,name=host_port,json=hostPort,proto3" json:"host_port,omitempty"` + ContainerPort int32 `protobuf:"varint,2,opt,name=container_port,json=containerPort,proto3" json:"container_port,omitempty"` + Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` // tcp or udp + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Port) Reset() { + *x = Port{} + mi := &file_control_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Port) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Port) ProtoMessage() {} + +func (x *Port) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[15] + 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 Port.ProtoReflect.Descriptor instead. +func (*Port) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{15} +} + +func (x *Port) GetHostPort() int32 { + if x != nil { + return x.HostPort + } + return 0 +} + +func (x *Port) GetContainerPort() int32 { + if x != nil { + return x.ContainerPort + } + return 0 +} + +func (x *Port) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +type ComposeSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceType string `protobuf:"bytes,1,opt,name=source_type,json=sourceType,proto3" json:"source_type,omitempty"` // git or inline + GitRepo string `protobuf:"bytes,2,opt,name=git_repo,json=gitRepo,proto3" json:"git_repo,omitempty"` + GitRef string `protobuf:"bytes,3,opt,name=git_ref,json=gitRef,proto3" json:"git_ref,omitempty"` + InlineYaml string `protobuf:"bytes,4,opt,name=inline_yaml,json=inlineYaml,proto3" json:"inline_yaml,omitempty"` + Env map[string]string `protobuf:"bytes,5,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ComposeSpec) Reset() { + *x = ComposeSpec{} + mi := &file_control_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ComposeSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ComposeSpec) ProtoMessage() {} + +func (x *ComposeSpec) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[16] + 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 ComposeSpec.ProtoReflect.Descriptor instead. +func (*ComposeSpec) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{16} +} + +func (x *ComposeSpec) GetSourceType() string { + if x != nil { + return x.SourceType + } + return "" +} + +func (x *ComposeSpec) GetGitRepo() string { + if x != nil { + return x.GitRepo + } + return "" +} + +func (x *ComposeSpec) GetGitRef() string { + if x != nil { + return x.GitRef + } + return "" +} + +func (x *ComposeSpec) GetInlineYaml() string { + if x != nil { + return x.InlineYaml + } + return "" +} + +func (x *ComposeSpec) GetEnv() map[string]string { + if x != nil { + return x.Env + } + return nil +} + +type VMSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + Vcpus int32 `protobuf:"varint,1,opt,name=vcpus,proto3" json:"vcpus,omitempty"` + MemoryMb int64 `protobuf:"varint,2,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"` + Disks []*DiskConfig `protobuf:"bytes,3,rep,name=disks,proto3" json:"disks,omitempty"` + Networks []*NetworkConfig `protobuf:"bytes,4,rep,name=networks,proto3" json:"networks,omitempty"` + CloudInit *CloudInitConfig `protobuf:"bytes,5,opt,name=cloud_init,json=cloudInit,proto3" json:"cloud_init,omitempty"` + OsImage string `protobuf:"bytes,6,opt,name=os_image,json=osImage,proto3" json:"os_image,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VMSpec) Reset() { + *x = VMSpec{} + mi := &file_control_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VMSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMSpec) ProtoMessage() {} + +func (x *VMSpec) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[17] + 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 VMSpec.ProtoReflect.Descriptor instead. +func (*VMSpec) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{17} +} + +func (x *VMSpec) GetVcpus() int32 { + if x != nil { + return x.Vcpus + } + return 0 +} + +func (x *VMSpec) GetMemoryMb() int64 { + if x != nil { + return x.MemoryMb + } + return 0 +} + +func (x *VMSpec) GetDisks() []*DiskConfig { + if x != nil { + return x.Disks + } + return nil +} + +func (x *VMSpec) GetNetworks() []*NetworkConfig { + if x != nil { + return x.Networks + } + return nil +} + +func (x *VMSpec) GetCloudInit() *CloudInitConfig { + if x != nil { + return x.CloudInit + } + return nil +} + +func (x *VMSpec) GetOsImage() string { + if x != nil { + return x.OsImage + } + return "" +} + +type DiskConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + PoolName string `protobuf:"bytes,1,opt,name=pool_name,json=poolName,proto3" json:"pool_name,omitempty"` + SizeGb int64 `protobuf:"varint,2,opt,name=size_gb,json=sizeGb,proto3" json:"size_gb,omitempty"` + MountPoint string `protobuf:"bytes,3,opt,name=mount_point,json=mountPoint,proto3" json:"mount_point,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiskConfig) Reset() { + *x = DiskConfig{} + mi := &file_control_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiskConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiskConfig) ProtoMessage() {} + +func (x *DiskConfig) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[18] + 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 DiskConfig.ProtoReflect.Descriptor instead. +func (*DiskConfig) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{18} +} + +func (x *DiskConfig) GetPoolName() string { + if x != nil { + return x.PoolName + } + return "" +} + +func (x *DiskConfig) GetSizeGb() int64 { + if x != nil { + return x.SizeGb + } + return 0 +} + +func (x *DiskConfig) GetMountPoint() string { + if x != nil { + return x.MountPoint + } + return "" +} + +type NetworkConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Bridge string `protobuf:"bytes,1,opt,name=bridge,proto3" json:"bridge,omitempty"` + Dhcp bool `protobuf:"varint,2,opt,name=dhcp,proto3" json:"dhcp,omitempty"` + StaticIp string `protobuf:"bytes,3,opt,name=static_ip,json=staticIp,proto3" json:"static_ip,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkConfig) Reset() { + *x = NetworkConfig{} + mi := &file_control_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkConfig) ProtoMessage() {} + +func (x *NetworkConfig) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[19] + 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 NetworkConfig.ProtoReflect.Descriptor instead. +func (*NetworkConfig) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{19} +} + +func (x *NetworkConfig) GetBridge() string { + if x != nil { + return x.Bridge + } + return "" +} + +func (x *NetworkConfig) GetDhcp() bool { + if x != nil { + return x.Dhcp + } + return false +} + +func (x *NetworkConfig) GetStaticIp() string { + if x != nil { + return x.StaticIp + } + return "" +} + +type CloudInitConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserData string `protobuf:"bytes,1,opt,name=user_data,json=userData,proto3" json:"user_data,omitempty"` + MetaData string `protobuf:"bytes,2,opt,name=meta_data,json=metaData,proto3" json:"meta_data,omitempty"` + NetworkConfig string `protobuf:"bytes,3,opt,name=network_config,json=networkConfig,proto3" json:"network_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloudInitConfig) Reset() { + *x = CloudInitConfig{} + mi := &file_control_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloudInitConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloudInitConfig) ProtoMessage() {} + +func (x *CloudInitConfig) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[20] + 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 CloudInitConfig.ProtoReflect.Descriptor instead. +func (*CloudInitConfig) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{20} +} + +func (x *CloudInitConfig) GetUserData() string { + if x != nil { + return x.UserData + } + return "" +} + +func (x *CloudInitConfig) GetMetaData() string { + if x != nil { + return x.MetaData + } + return "" +} + +func (x *CloudInitConfig) GetNetworkConfig() string { + if x != nil { + return x.NetworkConfig + } + return "" +} + +type WorkloadStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkloadId string `protobuf:"bytes,1,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` + State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` // Running, Stopped, Failed + FailureReason FailureReason `protobuf:"varint,3,opt,name=failure_reason,json=failureReason,proto3,enum=persys.control.v1.FailureReason" json:"failure_reason,omitempty"` + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + LastTransition *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_transition,json=lastTransition,proto3" json:"last_transition,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkloadStatus) Reset() { + *x = WorkloadStatus{} + mi := &file_control_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkloadStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkloadStatus) ProtoMessage() {} + +func (x *WorkloadStatus) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[21] + 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 WorkloadStatus.ProtoReflect.Descriptor instead. +func (*WorkloadStatus) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{21} +} + +func (x *WorkloadStatus) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +func (x *WorkloadStatus) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *WorkloadStatus) GetFailureReason() FailureReason { + if x != nil { + return x.FailureReason + } + return FailureReason_FAILURE_REASON_UNSPECIFIED +} + +func (x *WorkloadStatus) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *WorkloadStatus) GetLastTransition() *timestamppb.Timestamp { + if x != nil { + return x.LastTransition + } + return nil +} + +type RetryWorkloadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkloadId string `protobuf:"bytes,1,opt,name=workload_id,json=workloadId,proto3" json:"workload_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RetryWorkloadRequest) Reset() { + *x = RetryWorkloadRequest{} + mi := &file_control_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetryWorkloadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetryWorkloadRequest) ProtoMessage() {} + +func (x *RetryWorkloadRequest) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[22] + 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 RetryWorkloadRequest.ProtoReflect.Descriptor instead. +func (*RetryWorkloadRequest) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{22} +} + +func (x *RetryWorkloadRequest) GetWorkloadId() string { + if x != nil { + return x.WorkloadId + } + return "" +} + +type RetryWorkloadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RetryWorkloadResponse) Reset() { + *x = RetryWorkloadResponse{} + mi := &file_control_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetryWorkloadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetryWorkloadResponse) ProtoMessage() {} + +func (x *RetryWorkloadResponse) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[23] + 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 RetryWorkloadResponse.ProtoReflect.Descriptor instead. +func (*RetryWorkloadResponse) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{23} +} + +func (x *RetryWorkloadResponse) GetAccepted() bool { + if x != nil { + return x.Accepted + } + return false +} + +type ControlMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Message: + // + // *ControlMessage_Register + // *ControlMessage_Heartbeat + // *ControlMessage_Apply + // *ControlMessage_Delete + Message isControlMessage_Message `protobuf_oneof:"message"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ControlMessage) Reset() { + *x = ControlMessage{} + mi := &file_control_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ControlMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ControlMessage) ProtoMessage() {} + +func (x *ControlMessage) ProtoReflect() protoreflect.Message { + mi := &file_control_proto_msgTypes[24] + 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 ControlMessage.ProtoReflect.Descriptor instead. +func (*ControlMessage) Descriptor() ([]byte, []int) { + return file_control_proto_rawDescGZIP(), []int{24} +} + +func (x *ControlMessage) GetMessage() isControlMessage_Message { + if x != nil { + return x.Message + } + return nil +} + +func (x *ControlMessage) GetRegister() *RegisterNodeRequest { + if x != nil { + if x, ok := x.Message.(*ControlMessage_Register); ok { + return x.Register + } + } + return nil +} + +func (x *ControlMessage) GetHeartbeat() *HeartbeatRequest { + if x != nil { + if x, ok := x.Message.(*ControlMessage_Heartbeat); ok { + return x.Heartbeat + } + } + return nil +} + +func (x *ControlMessage) GetApply() *ApplyWorkloadRequest { + if x != nil { + if x, ok := x.Message.(*ControlMessage_Apply); ok { + return x.Apply + } + } + return nil +} + +func (x *ControlMessage) GetDelete() *DeleteWorkloadRequest { + if x != nil { + if x, ok := x.Message.(*ControlMessage_Delete); ok { + return x.Delete + } + } + return nil +} + +type isControlMessage_Message interface { + isControlMessage_Message() +} + +type ControlMessage_Register struct { + Register *RegisterNodeRequest `protobuf:"bytes,1,opt,name=register,proto3,oneof"` +} + +type ControlMessage_Heartbeat struct { + Heartbeat *HeartbeatRequest `protobuf:"bytes,2,opt,name=heartbeat,proto3,oneof"` +} + +type ControlMessage_Apply struct { + Apply *ApplyWorkloadRequest `protobuf:"bytes,3,opt,name=apply,proto3,oneof"` +} + +type ControlMessage_Delete struct { + Delete *DeleteWorkloadRequest `protobuf:"bytes,4,opt,name=delete,proto3,oneof"` +} + +func (*ControlMessage_Register) isControlMessage_Message() {} + +func (*ControlMessage_Heartbeat) isControlMessage_Message() {} + +func (*ControlMessage_Apply) isControlMessage_Message() {} + +func (*ControlMessage_Delete) isControlMessage_Message() {} + +var File_control_proto protoreflect.FileDescriptor + +const file_control_proto_rawDesc = "" + + "\n" + + "\rcontrol.proto\x12\x11persys.control.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa1\x03\n" + + "\x13RegisterNodeRequest\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12G\n" + + "\fcapabilities\x18\x02 \x01(\v2#.persys.control.v1.NodeCapabilitiesR\fcapabilities\x12J\n" + + "\x06labels\x18\x03 \x03(\v22.persys.control.v1.RegisterNodeRequest.LabelsEntryR\x06labels\x12#\n" + + "\ragent_version\x18\x04 \x01(\tR\fagentVersion\x12#\n" + + "\rgrpc_endpoint\x18\x05 \x01(\tR\fgrpcEndpoint\x12\x1d\n" + + "\n" + + "cluster_id\x18\x06 \x01(\tR\tclusterId\x128\n" + + "\ttimestamp\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xeb\x01\n" + + "\x10NodeCapabilities\x120\n" + + "\x14cpu_total_millicores\x18\x01 \x01(\x03R\x12cpuTotalMillicores\x12&\n" + + "\x0fmemory_total_mb\x18\x02 \x01(\x03R\rmemoryTotalMb\x12C\n" + + "\rstorage_pools\x18\x03 \x03(\v2\x1e.persys.control.v1.StoragePoolR\fstoragePools\x128\n" + + "\x18supported_workload_types\x18\x04 \x03(\tR\x16supportedWorkloadTypes\"P\n" + + "\vStoragePool\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x19\n" + + "\btotal_gb\x18\x03 \x01(\x03R\atotalGb\"\xce\x01\n" + + "\x14RegisterNodeResponse\x12\x1a\n" + + "\baccepted\x18\x01 \x01(\bR\baccepted\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\x12<\n" + + "\x1aheartbeat_interval_seconds\x18\x03 \x01(\x05R\x18heartbeatIntervalSeconds\x12D\n" + + "\x10lease_expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x0eleaseExpiresAt\"\xe9\x01\n" + + "\x10HeartbeatRequest\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x122\n" + + "\x05usage\x18\x02 \x01(\v2\x1c.persys.control.v1.NodeUsageR\x05usage\x12N\n" + + "\x11workload_statuses\x18\x03 \x03(\v2!.persys.control.v1.WorkloadStatusR\x10workloadStatuses\x128\n" + + "\ttimestamp\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\"\x99\x02\n" + + "\tNodeUsage\x128\n" + + "\x18cpu_allocated_millicores\x18\x01 \x01(\x03R\x16cpuAllocatedMillicores\x12.\n" + + "\x13cpu_used_millicores\x18\x02 \x01(\x03R\x11cpuUsedMillicores\x12.\n" + + "\x13memory_allocated_mb\x18\x03 \x01(\x03R\x11memoryAllocatedMb\x12$\n" + + "\x0ememory_used_mb\x18\x04 \x01(\x03R\fmemoryUsedMb\x12*\n" + + "\x11disk_allocated_gb\x18\x05 \x01(\x03R\x0fdiskAllocatedGb\x12 \n" + + "\fdisk_used_gb\x18\x06 \x01(\x03R\n" + + "diskUsedGb\"\x9c\x01\n" + + "\x11HeartbeatResponse\x12\"\n" + + "\facknowledged\x18\x01 \x01(\bR\facknowledged\x12\x1d\n" + + "\n" + + "drain_node\x18\x02 \x01(\bR\tdrainNode\x12D\n" + + "\x10lease_expires_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x0eleaseExpiresAt\"\xb2\x01\n" + + "\x14ApplyWorkloadRequest\x12\x1f\n" + + "\vworkload_id\x18\x01 \x01(\tR\n" + + "workloadId\x123\n" + + "\x04spec\x18\x02 \x01(\v2\x1f.persys.control.v1.WorkloadSpecR\x04spec\x12\x1f\n" + + "\vrevision_id\x18\n" + + " \x01(\tR\n" + + "revisionId\x12#\n" + + "\rdesired_state\x18\v \x01(\tR\fdesiredState\"\x9f\x01\n" + + "\x15ApplyWorkloadResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12G\n" + + "\x0efailure_reason\x18\x02 \x01(\x0e2 .persys.control.v1.FailureReasonR\rfailureReason\x12#\n" + + "\rerror_message\x18\x03 \x01(\tR\ferrorMessage\"8\n" + + "\x15DeleteWorkloadRequest\x12\x1f\n" + + "\vworkload_id\x18\x01 \x01(\tR\n" + + "workloadId\"W\n" + + "\x16DeleteWorkloadResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12#\n" + + "\rerror_message\x18\x02 \x01(\tR\ferrorMessage\"\xa8\x03\n" + + "\fWorkloadSpec\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12E\n" + + "\tresources\x18\x02 \x01(\v2'.persys.control.v1.ResourceRequirementsR\tresources\x12@\n" + + "\tcontainer\x18\n" + + " \x01(\v2 .persys.control.v1.ContainerSpecH\x00R\tcontainer\x12:\n" + + "\acompose\x18\v \x01(\v2\x1e.persys.control.v1.ComposeSpecH\x00R\acompose\x12+\n" + + "\x02vm\x18\f \x01(\v2\x19.persys.control.v1.VMSpecH\x00R\x02vm\x12I\n" + + "\bmetadata\x18\x14 \x03(\v2-.persys.control.v1.WorkloadSpec.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\n" + + "\n" + + "\bworkload\"s\n" + + "\x14ResourceRequirements\x12%\n" + + "\x0ecpu_millicores\x18\x01 \x01(\x03R\rcpuMillicores\x12\x1b\n" + + "\tmemory_mb\x18\x02 \x01(\x03R\bmemoryMb\x12\x17\n" + + "\adisk_gb\x18\x03 \x01(\x03R\x06diskGb\"\xe4\x02\n" + + "\rContainerSpec\x12\x14\n" + + "\x05image\x18\x01 \x01(\tR\x05image\x12\x18\n" + + "\acommand\x18\x02 \x03(\tR\acommand\x12;\n" + + "\x03env\x18\x03 \x03(\v2).persys.control.v1.ContainerSpec.EnvEntryR\x03env\x128\n" + + "\avolumes\x18\x04 \x03(\v2\x1e.persys.control.v1.VolumeMountR\avolumes\x12-\n" + + "\x05ports\x18\x05 \x03(\v2\x17.persys.control.v1.PortR\x05ports\x12%\n" + + "\x0erestart_policy\x18\x06 \x01(\tR\rrestartPolicy\x12\x1e\n" + + "\n" + + "privileged\x18\a \x01(\bR\n" + + "privileged\x1a6\n" + + "\bEnvEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"n\n" + + "\vVolumeMount\x12\x1b\n" + + "\thost_path\x18\x01 \x01(\tR\bhostPath\x12%\n" + + "\x0econtainer_path\x18\x02 \x01(\tR\rcontainerPath\x12\x1b\n" + + "\tread_only\x18\x03 \x01(\bR\breadOnly\"f\n" + + "\x04Port\x12\x1b\n" + + "\thost_port\x18\x01 \x01(\x05R\bhostPort\x12%\n" + + "\x0econtainer_port\x18\x02 \x01(\x05R\rcontainerPort\x12\x1a\n" + + "\bprotocol\x18\x03 \x01(\tR\bprotocol\"\xf6\x01\n" + + "\vComposeSpec\x12\x1f\n" + + "\vsource_type\x18\x01 \x01(\tR\n" + + "sourceType\x12\x19\n" + + "\bgit_repo\x18\x02 \x01(\tR\agitRepo\x12\x17\n" + + "\agit_ref\x18\x03 \x01(\tR\x06gitRef\x12\x1f\n" + + "\vinline_yaml\x18\x04 \x01(\tR\n" + + "inlineYaml\x129\n" + + "\x03env\x18\x05 \x03(\v2'.persys.control.v1.ComposeSpec.EnvEntryR\x03env\x1a6\n" + + "\bEnvEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x8c\x02\n" + + "\x06VMSpec\x12\x14\n" + + "\x05vcpus\x18\x01 \x01(\x05R\x05vcpus\x12\x1b\n" + + "\tmemory_mb\x18\x02 \x01(\x03R\bmemoryMb\x123\n" + + "\x05disks\x18\x03 \x03(\v2\x1d.persys.control.v1.DiskConfigR\x05disks\x12<\n" + + "\bnetworks\x18\x04 \x03(\v2 .persys.control.v1.NetworkConfigR\bnetworks\x12A\n" + + "\n" + + "cloud_init\x18\x05 \x01(\v2\".persys.control.v1.CloudInitConfigR\tcloudInit\x12\x19\n" + + "\bos_image\x18\x06 \x01(\tR\aosImage\"c\n" + + "\n" + + "DiskConfig\x12\x1b\n" + + "\tpool_name\x18\x01 \x01(\tR\bpoolName\x12\x17\n" + + "\asize_gb\x18\x02 \x01(\x03R\x06sizeGb\x12\x1f\n" + + "\vmount_point\x18\x03 \x01(\tR\n" + + "mountPoint\"X\n" + + "\rNetworkConfig\x12\x16\n" + + "\x06bridge\x18\x01 \x01(\tR\x06bridge\x12\x12\n" + + "\x04dhcp\x18\x02 \x01(\bR\x04dhcp\x12\x1b\n" + + "\tstatic_ip\x18\x03 \x01(\tR\bstaticIp\"r\n" + + "\x0fCloudInitConfig\x12\x1b\n" + + "\tuser_data\x18\x01 \x01(\tR\buserData\x12\x1b\n" + + "\tmeta_data\x18\x02 \x01(\tR\bmetaData\x12%\n" + + "\x0enetwork_config\x18\x03 \x01(\tR\rnetworkConfig\"\xef\x01\n" + + "\x0eWorkloadStatus\x12\x1f\n" + + "\vworkload_id\x18\x01 \x01(\tR\n" + + "workloadId\x12\x14\n" + + "\x05state\x18\x02 \x01(\tR\x05state\x12G\n" + + "\x0efailure_reason\x18\x03 \x01(\x0e2 .persys.control.v1.FailureReasonR\rfailureReason\x12\x18\n" + + "\amessage\x18\x04 \x01(\tR\amessage\x12C\n" + + "\x0flast_transition\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\x0elastTransition\"7\n" + + "\x14RetryWorkloadRequest\x12\x1f\n" + + "\vworkload_id\x18\x01 \x01(\tR\n" + + "workloadId\"3\n" + + "\x15RetryWorkloadResponse\x12\x1a\n" + + "\baccepted\x18\x01 \x01(\bR\baccepted\"\xab\x02\n" + + "\x0eControlMessage\x12D\n" + + "\bregister\x18\x01 \x01(\v2&.persys.control.v1.RegisterNodeRequestH\x00R\bregister\x12C\n" + + "\theartbeat\x18\x02 \x01(\v2#.persys.control.v1.HeartbeatRequestH\x00R\theartbeat\x12?\n" + + "\x05apply\x18\x03 \x01(\v2'.persys.control.v1.ApplyWorkloadRequestH\x00R\x05apply\x12B\n" + + "\x06delete\x18\x04 \x01(\v2(.persys.control.v1.DeleteWorkloadRequestH\x00R\x06deleteB\t\n" + + "\amessage*\xd6\x01\n" + + "\rFailureReason\x12\x1e\n" + + "\x1aFAILURE_REASON_UNSPECIFIED\x10\x00\x12\x15\n" + + "\x11IMAGE_PULL_FAILED\x10\x01\x12\x13\n" + + "\x0fIMAGE_NOT_FOUND\x10\x02\x12\x1a\n" + + "\x16INSUFFICIENT_RESOURCES\x10\x03\x12\x10\n" + + "\fINVALID_SPEC\x10\x04\x12\x11\n" + + "\rRUNTIME_ERROR\x10\x05\x12\x11\n" + + "\rNETWORK_ERROR\x10\x06\x12\x11\n" + + "\rSTORAGE_ERROR\x10\a\x12\x12\n" + + "\x0eVM_BOOT_FAILED\x10\b2\xd1\x04\n" + + "\fAgentControl\x12_\n" + + "\fRegisterNode\x12&.persys.control.v1.RegisterNodeRequest\x1a'.persys.control.v1.RegisterNodeResponse\x12V\n" + + "\tHeartbeat\x12#.persys.control.v1.HeartbeatRequest\x1a$.persys.control.v1.HeartbeatResponse\x12b\n" + + "\rApplyWorkload\x12'.persys.control.v1.ApplyWorkloadRequest\x1a(.persys.control.v1.ApplyWorkloadResponse\x12e\n" + + "\x0eDeleteWorkload\x12(.persys.control.v1.DeleteWorkloadRequest\x1a).persys.control.v1.DeleteWorkloadResponse\x12b\n" + + "\rRetryWorkload\x12'.persys.control.v1.RetryWorkloadRequest\x1a(.persys.control.v1.RetryWorkloadResponse\x12Y\n" + + "\rControlStream\x12!.persys.control.v1.ControlMessage\x1a!.persys.control.v1.ControlMessage(\x010\x01B:Z8github.com/persys/compute-agent/pkg/control/v1;controlv1b\x06proto3" + +var ( + file_control_proto_rawDescOnce sync.Once + file_control_proto_rawDescData []byte +) + +func file_control_proto_rawDescGZIP() []byte { + file_control_proto_rawDescOnce.Do(func() { + file_control_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_control_proto_rawDesc), len(file_control_proto_rawDesc))) + }) + return file_control_proto_rawDescData +} + +var file_control_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_control_proto_msgTypes = make([]protoimpl.MessageInfo, 29) +var file_control_proto_goTypes = []any{ + (FailureReason)(0), // 0: persys.control.v1.FailureReason + (*RegisterNodeRequest)(nil), // 1: persys.control.v1.RegisterNodeRequest + (*NodeCapabilities)(nil), // 2: persys.control.v1.NodeCapabilities + (*StoragePool)(nil), // 3: persys.control.v1.StoragePool + (*RegisterNodeResponse)(nil), // 4: persys.control.v1.RegisterNodeResponse + (*HeartbeatRequest)(nil), // 5: persys.control.v1.HeartbeatRequest + (*NodeUsage)(nil), // 6: persys.control.v1.NodeUsage + (*HeartbeatResponse)(nil), // 7: persys.control.v1.HeartbeatResponse + (*ApplyWorkloadRequest)(nil), // 8: persys.control.v1.ApplyWorkloadRequest + (*ApplyWorkloadResponse)(nil), // 9: persys.control.v1.ApplyWorkloadResponse + (*DeleteWorkloadRequest)(nil), // 10: persys.control.v1.DeleteWorkloadRequest + (*DeleteWorkloadResponse)(nil), // 11: persys.control.v1.DeleteWorkloadResponse + (*WorkloadSpec)(nil), // 12: persys.control.v1.WorkloadSpec + (*ResourceRequirements)(nil), // 13: persys.control.v1.ResourceRequirements + (*ContainerSpec)(nil), // 14: persys.control.v1.ContainerSpec + (*VolumeMount)(nil), // 15: persys.control.v1.VolumeMount + (*Port)(nil), // 16: persys.control.v1.Port + (*ComposeSpec)(nil), // 17: persys.control.v1.ComposeSpec + (*VMSpec)(nil), // 18: persys.control.v1.VMSpec + (*DiskConfig)(nil), // 19: persys.control.v1.DiskConfig + (*NetworkConfig)(nil), // 20: persys.control.v1.NetworkConfig + (*CloudInitConfig)(nil), // 21: persys.control.v1.CloudInitConfig + (*WorkloadStatus)(nil), // 22: persys.control.v1.WorkloadStatus + (*RetryWorkloadRequest)(nil), // 23: persys.control.v1.RetryWorkloadRequest + (*RetryWorkloadResponse)(nil), // 24: persys.control.v1.RetryWorkloadResponse + (*ControlMessage)(nil), // 25: persys.control.v1.ControlMessage + nil, // 26: persys.control.v1.RegisterNodeRequest.LabelsEntry + nil, // 27: persys.control.v1.WorkloadSpec.MetadataEntry + nil, // 28: persys.control.v1.ContainerSpec.EnvEntry + nil, // 29: persys.control.v1.ComposeSpec.EnvEntry + (*timestamppb.Timestamp)(nil), // 30: google.protobuf.Timestamp +} +var file_control_proto_depIdxs = []int32{ + 2, // 0: persys.control.v1.RegisterNodeRequest.capabilities:type_name -> persys.control.v1.NodeCapabilities + 26, // 1: persys.control.v1.RegisterNodeRequest.labels:type_name -> persys.control.v1.RegisterNodeRequest.LabelsEntry + 30, // 2: persys.control.v1.RegisterNodeRequest.timestamp:type_name -> google.protobuf.Timestamp + 3, // 3: persys.control.v1.NodeCapabilities.storage_pools:type_name -> persys.control.v1.StoragePool + 30, // 4: persys.control.v1.RegisterNodeResponse.lease_expires_at:type_name -> google.protobuf.Timestamp + 6, // 5: persys.control.v1.HeartbeatRequest.usage:type_name -> persys.control.v1.NodeUsage + 22, // 6: persys.control.v1.HeartbeatRequest.workload_statuses:type_name -> persys.control.v1.WorkloadStatus + 30, // 7: persys.control.v1.HeartbeatRequest.timestamp:type_name -> google.protobuf.Timestamp + 30, // 8: persys.control.v1.HeartbeatResponse.lease_expires_at:type_name -> google.protobuf.Timestamp + 12, // 9: persys.control.v1.ApplyWorkloadRequest.spec:type_name -> persys.control.v1.WorkloadSpec + 0, // 10: persys.control.v1.ApplyWorkloadResponse.failure_reason:type_name -> persys.control.v1.FailureReason + 13, // 11: persys.control.v1.WorkloadSpec.resources:type_name -> persys.control.v1.ResourceRequirements + 14, // 12: persys.control.v1.WorkloadSpec.container:type_name -> persys.control.v1.ContainerSpec + 17, // 13: persys.control.v1.WorkloadSpec.compose:type_name -> persys.control.v1.ComposeSpec + 18, // 14: persys.control.v1.WorkloadSpec.vm:type_name -> persys.control.v1.VMSpec + 27, // 15: persys.control.v1.WorkloadSpec.metadata:type_name -> persys.control.v1.WorkloadSpec.MetadataEntry + 28, // 16: persys.control.v1.ContainerSpec.env:type_name -> persys.control.v1.ContainerSpec.EnvEntry + 15, // 17: persys.control.v1.ContainerSpec.volumes:type_name -> persys.control.v1.VolumeMount + 16, // 18: persys.control.v1.ContainerSpec.ports:type_name -> persys.control.v1.Port + 29, // 19: persys.control.v1.ComposeSpec.env:type_name -> persys.control.v1.ComposeSpec.EnvEntry + 19, // 20: persys.control.v1.VMSpec.disks:type_name -> persys.control.v1.DiskConfig + 20, // 21: persys.control.v1.VMSpec.networks:type_name -> persys.control.v1.NetworkConfig + 21, // 22: persys.control.v1.VMSpec.cloud_init:type_name -> persys.control.v1.CloudInitConfig + 0, // 23: persys.control.v1.WorkloadStatus.failure_reason:type_name -> persys.control.v1.FailureReason + 30, // 24: persys.control.v1.WorkloadStatus.last_transition:type_name -> google.protobuf.Timestamp + 1, // 25: persys.control.v1.ControlMessage.register:type_name -> persys.control.v1.RegisterNodeRequest + 5, // 26: persys.control.v1.ControlMessage.heartbeat:type_name -> persys.control.v1.HeartbeatRequest + 8, // 27: persys.control.v1.ControlMessage.apply:type_name -> persys.control.v1.ApplyWorkloadRequest + 10, // 28: persys.control.v1.ControlMessage.delete:type_name -> persys.control.v1.DeleteWorkloadRequest + 1, // 29: persys.control.v1.AgentControl.RegisterNode:input_type -> persys.control.v1.RegisterNodeRequest + 5, // 30: persys.control.v1.AgentControl.Heartbeat:input_type -> persys.control.v1.HeartbeatRequest + 8, // 31: persys.control.v1.AgentControl.ApplyWorkload:input_type -> persys.control.v1.ApplyWorkloadRequest + 10, // 32: persys.control.v1.AgentControl.DeleteWorkload:input_type -> persys.control.v1.DeleteWorkloadRequest + 23, // 33: persys.control.v1.AgentControl.RetryWorkload:input_type -> persys.control.v1.RetryWorkloadRequest + 25, // 34: persys.control.v1.AgentControl.ControlStream:input_type -> persys.control.v1.ControlMessage + 4, // 35: persys.control.v1.AgentControl.RegisterNode:output_type -> persys.control.v1.RegisterNodeResponse + 7, // 36: persys.control.v1.AgentControl.Heartbeat:output_type -> persys.control.v1.HeartbeatResponse + 9, // 37: persys.control.v1.AgentControl.ApplyWorkload:output_type -> persys.control.v1.ApplyWorkloadResponse + 11, // 38: persys.control.v1.AgentControl.DeleteWorkload:output_type -> persys.control.v1.DeleteWorkloadResponse + 24, // 39: persys.control.v1.AgentControl.RetryWorkload:output_type -> persys.control.v1.RetryWorkloadResponse + 25, // 40: persys.control.v1.AgentControl.ControlStream:output_type -> persys.control.v1.ControlMessage + 35, // [35:41] is the sub-list for method output_type + 29, // [29:35] is the sub-list for method input_type + 29, // [29:29] is the sub-list for extension type_name + 29, // [29:29] is the sub-list for extension extendee + 0, // [0:29] is the sub-list for field type_name +} + +func init() { file_control_proto_init() } +func file_control_proto_init() { + if File_control_proto != nil { + return + } + file_control_proto_msgTypes[11].OneofWrappers = []any{ + (*WorkloadSpec_Container)(nil), + (*WorkloadSpec_Compose)(nil), + (*WorkloadSpec_Vm)(nil), + } + file_control_proto_msgTypes[24].OneofWrappers = []any{ + (*ControlMessage_Register)(nil), + (*ControlMessage_Heartbeat)(nil), + (*ControlMessage_Apply)(nil), + (*ControlMessage_Delete)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_control_proto_rawDesc), len(file_control_proto_rawDesc)), + NumEnums: 1, + NumMessages: 29, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_control_proto_goTypes, + DependencyIndexes: file_control_proto_depIdxs, + EnumInfos: file_control_proto_enumTypes, + MessageInfos: file_control_proto_msgTypes, + }.Build() + File_control_proto = out.File + file_control_proto_goTypes = nil + file_control_proto_depIdxs = nil +} diff --git a/pkg/control/v1/control_grpc.pb.go b/pkg/control/v1/control_grpc.pb.go new file mode 100644 index 0000000..cc6235a --- /dev/null +++ b/pkg/control/v1/control_grpc.pb.go @@ -0,0 +1,316 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v3.21.12 +// source: control.proto + +package controlv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + AgentControl_RegisterNode_FullMethodName = "/persys.control.v1.AgentControl/RegisterNode" + AgentControl_Heartbeat_FullMethodName = "/persys.control.v1.AgentControl/Heartbeat" + AgentControl_ApplyWorkload_FullMethodName = "/persys.control.v1.AgentControl/ApplyWorkload" + AgentControl_DeleteWorkload_FullMethodName = "/persys.control.v1.AgentControl/DeleteWorkload" + AgentControl_RetryWorkload_FullMethodName = "/persys.control.v1.AgentControl/RetryWorkload" + AgentControl_ControlStream_FullMethodName = "/persys.control.v1.AgentControl/ControlStream" +) + +// AgentControlClient is the client API for AgentControl service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AgentControlClient interface { + // Registration + RegisterNode(ctx context.Context, in *RegisterNodeRequest, opts ...grpc.CallOption) (*RegisterNodeResponse, error) + // Heartbeat + Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error) + // Workload lifecycle + ApplyWorkload(ctx context.Context, in *ApplyWorkloadRequest, opts ...grpc.CallOption) (*ApplyWorkloadResponse, error) + DeleteWorkload(ctx context.Context, in *DeleteWorkloadRequest, opts ...grpc.CallOption) (*DeleteWorkloadResponse, error) + // Retry trigger + RetryWorkload(ctx context.Context, in *RetryWorkloadRequest, opts ...grpc.CallOption) (*RetryWorkloadResponse, error) + // Optional future streaming channel + ControlStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ControlMessage, ControlMessage], error) +} + +type agentControlClient struct { + cc grpc.ClientConnInterface +} + +func NewAgentControlClient(cc grpc.ClientConnInterface) AgentControlClient { + return &agentControlClient{cc} +} + +func (c *agentControlClient) RegisterNode(ctx context.Context, in *RegisterNodeRequest, opts ...grpc.CallOption) (*RegisterNodeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RegisterNodeResponse) + err := c.cc.Invoke(ctx, AgentControl_RegisterNode_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HeartbeatResponse) + err := c.cc.Invoke(ctx, AgentControl_Heartbeat_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) ApplyWorkload(ctx context.Context, in *ApplyWorkloadRequest, opts ...grpc.CallOption) (*ApplyWorkloadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApplyWorkloadResponse) + err := c.cc.Invoke(ctx, AgentControl_ApplyWorkload_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) DeleteWorkload(ctx context.Context, in *DeleteWorkloadRequest, opts ...grpc.CallOption) (*DeleteWorkloadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteWorkloadResponse) + err := c.cc.Invoke(ctx, AgentControl_DeleteWorkload_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) RetryWorkload(ctx context.Context, in *RetryWorkloadRequest, opts ...grpc.CallOption) (*RetryWorkloadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RetryWorkloadResponse) + err := c.cc.Invoke(ctx, AgentControl_RetryWorkload_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentControlClient) ControlStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ControlMessage, ControlMessage], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &AgentControl_ServiceDesc.Streams[0], AgentControl_ControlStream_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[ControlMessage, ControlMessage]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type AgentControl_ControlStreamClient = grpc.BidiStreamingClient[ControlMessage, ControlMessage] + +// AgentControlServer is the server API for AgentControl service. +// All implementations must embed UnimplementedAgentControlServer +// for forward compatibility. +type AgentControlServer interface { + // Registration + RegisterNode(context.Context, *RegisterNodeRequest) (*RegisterNodeResponse, error) + // Heartbeat + Heartbeat(context.Context, *HeartbeatRequest) (*HeartbeatResponse, error) + // Workload lifecycle + ApplyWorkload(context.Context, *ApplyWorkloadRequest) (*ApplyWorkloadResponse, error) + DeleteWorkload(context.Context, *DeleteWorkloadRequest) (*DeleteWorkloadResponse, error) + // Retry trigger + RetryWorkload(context.Context, *RetryWorkloadRequest) (*RetryWorkloadResponse, error) + // Optional future streaming channel + ControlStream(grpc.BidiStreamingServer[ControlMessage, ControlMessage]) error + mustEmbedUnimplementedAgentControlServer() +} + +// UnimplementedAgentControlServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAgentControlServer struct{} + +func (UnimplementedAgentControlServer) RegisterNode(context.Context, *RegisterNodeRequest) (*RegisterNodeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RegisterNode not implemented") +} +func (UnimplementedAgentControlServer) Heartbeat(context.Context, *HeartbeatRequest) (*HeartbeatResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Heartbeat not implemented") +} +func (UnimplementedAgentControlServer) ApplyWorkload(context.Context, *ApplyWorkloadRequest) (*ApplyWorkloadResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ApplyWorkload not implemented") +} +func (UnimplementedAgentControlServer) DeleteWorkload(context.Context, *DeleteWorkloadRequest) (*DeleteWorkloadResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteWorkload not implemented") +} +func (UnimplementedAgentControlServer) RetryWorkload(context.Context, *RetryWorkloadRequest) (*RetryWorkloadResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RetryWorkload not implemented") +} +func (UnimplementedAgentControlServer) ControlStream(grpc.BidiStreamingServer[ControlMessage, ControlMessage]) error { + return status.Error(codes.Unimplemented, "method ControlStream not implemented") +} +func (UnimplementedAgentControlServer) mustEmbedUnimplementedAgentControlServer() {} +func (UnimplementedAgentControlServer) testEmbeddedByValue() {} + +// UnsafeAgentControlServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AgentControlServer will +// result in compilation errors. +type UnsafeAgentControlServer interface { + mustEmbedUnimplementedAgentControlServer() +} + +func RegisterAgentControlServer(s grpc.ServiceRegistrar, srv AgentControlServer) { + // If the following call panics, it indicates UnimplementedAgentControlServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&AgentControl_ServiceDesc, srv) +} + +func _AgentControl_RegisterNode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterNodeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).RegisterNode(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_RegisterNode_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).RegisterNode(ctx, req.(*RegisterNodeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_Heartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HeartbeatRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).Heartbeat(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_Heartbeat_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).Heartbeat(ctx, req.(*HeartbeatRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_ApplyWorkload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ApplyWorkloadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).ApplyWorkload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_ApplyWorkload_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).ApplyWorkload(ctx, req.(*ApplyWorkloadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_DeleteWorkload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteWorkloadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).DeleteWorkload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_DeleteWorkload_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).DeleteWorkload(ctx, req.(*DeleteWorkloadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_RetryWorkload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RetryWorkloadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentControlServer).RetryWorkload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentControl_RetryWorkload_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentControlServer).RetryWorkload(ctx, req.(*RetryWorkloadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentControl_ControlStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(AgentControlServer).ControlStream(&grpc.GenericServerStream[ControlMessage, ControlMessage]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type AgentControl_ControlStreamServer = grpc.BidiStreamingServer[ControlMessage, ControlMessage] + +// AgentControl_ServiceDesc is the grpc.ServiceDesc for AgentControl service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AgentControl_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "persys.control.v1.AgentControl", + HandlerType: (*AgentControlServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "RegisterNode", + Handler: _AgentControl_RegisterNode_Handler, + }, + { + MethodName: "Heartbeat", + Handler: _AgentControl_Heartbeat_Handler, + }, + { + MethodName: "ApplyWorkload", + Handler: _AgentControl_ApplyWorkload_Handler, + }, + { + MethodName: "DeleteWorkload", + Handler: _AgentControl_DeleteWorkload_Handler, + }, + { + MethodName: "RetryWorkload", + Handler: _AgentControl_RetryWorkload_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "ControlStream", + Handler: _AgentControl_ControlStream_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "control.proto", +} From da78de68be7f1e6b2d9f2ad0a832a017165dc62e Mon Sep 17 00:00:00 2001 From: milx Date: Wed, 18 Feb 2026 18:06:25 +0330 Subject: [PATCH 25/25] Fix: proto drift error in ci --- .github/workflows/ci.yml | 2 +- Makefile | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75411b8..770728e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: - name: Install protoc run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends protobuf-compiler + sudo apt-get install -y --no-install-recommends protobuf-compiler libprotobuf-dev protoc --version - name: Install protobuf generators diff --git a/Makefile b/Makefile index 8006c20..4a21308 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ DOCKER_IMAGE=persys/compute-agent PROTO_DIR=api/proto PKG_DIR=pkg/api/v1 CONTROL_PKG_DIR=pkg/control/v1 +PROTOC_SYSTEM_INCLUDE?=/usr/include # Go parameters GOCMD=go @@ -29,10 +30,10 @@ proto: @echo "==> Generating protobuf code..." @mkdir -p $(PKG_DIR) @mkdir -p $(CONTROL_PKG_DIR) - cd api/proto && protoc --go_out=../../$(PKG_DIR) --go_opt=paths=source_relative \ + cd api/proto && protoc -I. -I../../$(PROTO_DIR) -I$(PROTOC_SYSTEM_INCLUDE) --go_out=../../$(PKG_DIR) --go_opt=paths=source_relative \ --go-grpc_out=../../$(PKG_DIR) --go-grpc_opt=paths=source_relative \ agent.proto - cd api/proto && protoc --go_out=../../$(CONTROL_PKG_DIR) --go_opt=paths=source_relative \ + cd api/proto && protoc -I. -I../../$(PROTO_DIR) -I$(PROTOC_SYSTEM_INCLUDE) --go_out=../../$(CONTROL_PKG_DIR) --go_opt=paths=source_relative \ --go-grpc_out=../../$(CONTROL_PKG_DIR) --go-grpc_opt=paths=source_relative \ control.proto