diff --git a/agent/agentsocket/client.go b/agent/agentsocket/client.go index ba7b03bbfe..d4a3a41f4c 100644 --- a/agent/agentsocket/client.go +++ b/agent/agentsocket/client.go @@ -133,6 +133,25 @@ func (c *Client) SyncStatus(ctx context.Context, unitName unit.ID) (SyncStatusRe }, nil } +// SyncList returns all registered units and their current statuses. +func (c *Client) SyncList(ctx context.Context) ([]SyncListItem, error) { + resp, err := c.client.SyncList(ctx, &proto.SyncListRequest{}) + if err != nil { + return nil, err + } + + var items []SyncListItem + for _, u := range resp.Units { + items = append(items, SyncListItem{ + UnitName: unit.ID(u.Unit), + Status: unit.Status(u.Status), + IsReady: u.IsReady, + }) + } + + return items, nil +} + // UpdateAppStatus forwards an app status update to coderd via the agent. func (c *Client) UpdateAppStatus(ctx context.Context, req *agentproto.UpdateAppStatusRequest) (*agentproto.UpdateAppStatusResponse, error) { return c.client.UpdateAppStatus(ctx, req) @@ -146,6 +165,13 @@ type SyncStatusResponse struct { Dependencies []DependencyInfo `table:"dependencies" json:"dependencies"` } +// SyncListItem contains summary information for a single unit. +type SyncListItem struct { + UnitName unit.ID `table:"unit,default_sort" json:"unit_name"` + Status unit.Status `table:"status" json:"status"` + IsReady bool `table:"ready" json:"is_ready"` +} + // DependencyInfo contains information about a unit dependency. type DependencyInfo struct { DependsOn unit.ID `table:"depends on,default_sort" json:"depends_on"` diff --git a/agent/agentsocket/proto/agentsocket.pb.go b/agent/agentsocket/proto/agentsocket.pb.go index 4ddfaa5126..298664b2d9 100644 --- a/agent/agentsocket/proto/agentsocket.pb.go +++ b/agent/agentsocket/proto/agentsocket.pb.go @@ -501,6 +501,8 @@ func (x *SyncStatusRequest) GetUnit() string { return "" } +// DependencyInfo represents a directed edge in the dependency graph from one unit +// to a unit it depends on, along with the required and current status of the dependency. type DependencyInfo struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -643,6 +645,156 @@ func (x *SyncStatusResponse) GetDependencies() []*DependencyInfo { return nil } +type SyncListRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SyncListRequest) Reset() { + *x = SyncListRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SyncListRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncListRequest) ProtoMessage() {} + +func (x *SyncListRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncListRequest.ProtoReflect.Descriptor instead. +func (*SyncListRequest) Descriptor() ([]byte, []int) { + return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{13} +} + +// UnitInfo represents a single unit vertex in the dependency graph. +// Includes the state of the unit itself. +type UnitInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Unit string `protobuf:"bytes,1,opt,name=unit,proto3" json:"unit,omitempty"` + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + IsReady bool `protobuf:"varint,3,opt,name=is_ready,json=isReady,proto3" json:"is_ready,omitempty"` +} + +func (x *UnitInfo) Reset() { + *x = UnitInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UnitInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnitInfo) ProtoMessage() {} + +func (x *UnitInfo) ProtoReflect() protoreflect.Message { + mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnitInfo.ProtoReflect.Descriptor instead. +func (*UnitInfo) Descriptor() ([]byte, []int) { + return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{14} +} + +func (x *UnitInfo) GetUnit() string { + if x != nil { + return x.Unit + } + return "" +} + +func (x *UnitInfo) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *UnitInfo) GetIsReady() bool { + if x != nil { + return x.IsReady + } + return false +} + +type SyncListResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Units []*UnitInfo `protobuf:"bytes,1,rep,name=units,proto3" json:"units,omitempty"` +} + +func (x *SyncListResponse) Reset() { + *x = SyncListResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SyncListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncListResponse) ProtoMessage() {} + +func (x *SyncListResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_agentsocket_proto_agentsocket_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncListResponse.ProtoReflect.Descriptor instead. +func (*SyncListResponse) Descriptor() ([]byte, []int) { + return file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP(), []int{15} +} + +func (x *SyncListResponse) GetUnits() []*UnitInfo { + if x != nil { + return x.Units + } + return nil +} + var File_agent_agentsocket_proto_agentsocket_proto protoreflect.FileDescriptor var file_agent_agentsocket_proto_agentsocket_proto_rawDesc = []byte{ @@ -695,7 +847,18 @@ var file_agent_agentsocket_proto_agentsocket_proto_rawDesc = []byte{ 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0c, - 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x32, 0x9f, 0x05, 0x0a, + 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x22, 0x11, 0x0a, 0x0f, + 0x53, 0x79, 0x6e, 0x63, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, + 0x51, 0x0a, 0x08, 0x55, 0x6e, 0x69, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x75, + 0x6e, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x6e, 0x69, 0x74, 0x12, + 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x72, 0x65, + 0x61, 0x64, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x52, 0x65, 0x61, + 0x64, 0x79, 0x22, 0x48, 0x0a, 0x10, 0x53, 0x79, 0x6e, 0x63, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x05, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x69, + 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x32, 0xfa, 0x05, 0x0a, 0x0b, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x4d, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x21, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, @@ -731,17 +894,23 @@ var file_agent_agentsocket_proto_agentsocket_proto_rawDesc = []byte{ 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x26, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x33, - 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x08, 0x53, 0x79, + 0x6e, 0x63, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x25, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, + 0x6e, 0x63, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, + 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -756,7 +925,7 @@ func file_agent_agentsocket_proto_agentsocket_proto_rawDescGZIP() []byte { return file_agent_agentsocket_proto_agentsocket_proto_rawDescData } -var file_agent_agentsocket_proto_agentsocket_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_agent_agentsocket_proto_agentsocket_proto_msgTypes = make([]protoimpl.MessageInfo, 16) var file_agent_agentsocket_proto_agentsocket_proto_goTypes = []interface{}{ (*PingRequest)(nil), // 0: coder.agentsocket.v1.PingRequest (*PingResponse)(nil), // 1: coder.agentsocket.v1.PingResponse @@ -771,30 +940,36 @@ var file_agent_agentsocket_proto_agentsocket_proto_goTypes = []interface{}{ (*SyncStatusRequest)(nil), // 10: coder.agentsocket.v1.SyncStatusRequest (*DependencyInfo)(nil), // 11: coder.agentsocket.v1.DependencyInfo (*SyncStatusResponse)(nil), // 12: coder.agentsocket.v1.SyncStatusResponse - (*proto.UpdateAppStatusRequest)(nil), // 13: coder.agent.v2.UpdateAppStatusRequest - (*proto.UpdateAppStatusResponse)(nil), // 14: coder.agent.v2.UpdateAppStatusResponse + (*SyncListRequest)(nil), // 13: coder.agentsocket.v1.SyncListRequest + (*UnitInfo)(nil), // 14: coder.agentsocket.v1.UnitInfo + (*SyncListResponse)(nil), // 15: coder.agentsocket.v1.SyncListResponse + (*proto.UpdateAppStatusRequest)(nil), // 16: coder.agent.v2.UpdateAppStatusRequest + (*proto.UpdateAppStatusResponse)(nil), // 17: coder.agent.v2.UpdateAppStatusResponse } var file_agent_agentsocket_proto_agentsocket_proto_depIdxs = []int32{ 11, // 0: coder.agentsocket.v1.SyncStatusResponse.dependencies:type_name -> coder.agentsocket.v1.DependencyInfo - 0, // 1: coder.agentsocket.v1.AgentSocket.Ping:input_type -> coder.agentsocket.v1.PingRequest - 2, // 2: coder.agentsocket.v1.AgentSocket.SyncStart:input_type -> coder.agentsocket.v1.SyncStartRequest - 4, // 3: coder.agentsocket.v1.AgentSocket.SyncWant:input_type -> coder.agentsocket.v1.SyncWantRequest - 6, // 4: coder.agentsocket.v1.AgentSocket.SyncComplete:input_type -> coder.agentsocket.v1.SyncCompleteRequest - 8, // 5: coder.agentsocket.v1.AgentSocket.SyncReady:input_type -> coder.agentsocket.v1.SyncReadyRequest - 10, // 6: coder.agentsocket.v1.AgentSocket.SyncStatus:input_type -> coder.agentsocket.v1.SyncStatusRequest - 13, // 7: coder.agentsocket.v1.AgentSocket.UpdateAppStatus:input_type -> coder.agent.v2.UpdateAppStatusRequest - 1, // 8: coder.agentsocket.v1.AgentSocket.Ping:output_type -> coder.agentsocket.v1.PingResponse - 3, // 9: coder.agentsocket.v1.AgentSocket.SyncStart:output_type -> coder.agentsocket.v1.SyncStartResponse - 5, // 10: coder.agentsocket.v1.AgentSocket.SyncWant:output_type -> coder.agentsocket.v1.SyncWantResponse - 7, // 11: coder.agentsocket.v1.AgentSocket.SyncComplete:output_type -> coder.agentsocket.v1.SyncCompleteResponse - 9, // 12: coder.agentsocket.v1.AgentSocket.SyncReady:output_type -> coder.agentsocket.v1.SyncReadyResponse - 12, // 13: coder.agentsocket.v1.AgentSocket.SyncStatus:output_type -> coder.agentsocket.v1.SyncStatusResponse - 14, // 14: coder.agentsocket.v1.AgentSocket.UpdateAppStatus:output_type -> coder.agent.v2.UpdateAppStatusResponse - 8, // [8:15] is the sub-list for method output_type - 1, // [1:8] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 14, // 1: coder.agentsocket.v1.SyncListResponse.units:type_name -> coder.agentsocket.v1.UnitInfo + 0, // 2: coder.agentsocket.v1.AgentSocket.Ping:input_type -> coder.agentsocket.v1.PingRequest + 2, // 3: coder.agentsocket.v1.AgentSocket.SyncStart:input_type -> coder.agentsocket.v1.SyncStartRequest + 4, // 4: coder.agentsocket.v1.AgentSocket.SyncWant:input_type -> coder.agentsocket.v1.SyncWantRequest + 6, // 5: coder.agentsocket.v1.AgentSocket.SyncComplete:input_type -> coder.agentsocket.v1.SyncCompleteRequest + 8, // 6: coder.agentsocket.v1.AgentSocket.SyncReady:input_type -> coder.agentsocket.v1.SyncReadyRequest + 10, // 7: coder.agentsocket.v1.AgentSocket.SyncStatus:input_type -> coder.agentsocket.v1.SyncStatusRequest + 13, // 8: coder.agentsocket.v1.AgentSocket.SyncList:input_type -> coder.agentsocket.v1.SyncListRequest + 16, // 9: coder.agentsocket.v1.AgentSocket.UpdateAppStatus:input_type -> coder.agent.v2.UpdateAppStatusRequest + 1, // 10: coder.agentsocket.v1.AgentSocket.Ping:output_type -> coder.agentsocket.v1.PingResponse + 3, // 11: coder.agentsocket.v1.AgentSocket.SyncStart:output_type -> coder.agentsocket.v1.SyncStartResponse + 5, // 12: coder.agentsocket.v1.AgentSocket.SyncWant:output_type -> coder.agentsocket.v1.SyncWantResponse + 7, // 13: coder.agentsocket.v1.AgentSocket.SyncComplete:output_type -> coder.agentsocket.v1.SyncCompleteResponse + 9, // 14: coder.agentsocket.v1.AgentSocket.SyncReady:output_type -> coder.agentsocket.v1.SyncReadyResponse + 12, // 15: coder.agentsocket.v1.AgentSocket.SyncStatus:output_type -> coder.agentsocket.v1.SyncStatusResponse + 15, // 16: coder.agentsocket.v1.AgentSocket.SyncList:output_type -> coder.agentsocket.v1.SyncListResponse + 17, // 17: coder.agentsocket.v1.AgentSocket.UpdateAppStatus:output_type -> coder.agent.v2.UpdateAppStatusResponse + 10, // [10:18] is the sub-list for method output_type + 2, // [2:10] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_agent_agentsocket_proto_agentsocket_proto_init() } @@ -959,6 +1134,42 @@ func file_agent_agentsocket_proto_agentsocket_proto_init() { return nil } } + file_agent_agentsocket_proto_agentsocket_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SyncListRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_agentsocket_proto_agentsocket_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UnitInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_agentsocket_proto_agentsocket_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SyncListResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -966,7 +1177,7 @@ func file_agent_agentsocket_proto_agentsocket_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_agent_agentsocket_proto_agentsocket_proto_rawDesc, NumEnums: 0, - NumMessages: 13, + NumMessages: 16, NumExtensions: 0, NumServices: 1, }, diff --git a/agent/agentsocket/proto/agentsocket.proto b/agent/agentsocket/proto/agentsocket.proto index b037c0fabe..ad0bbe0f7e 100644 --- a/agent/agentsocket/proto/agentsocket.proto +++ b/agent/agentsocket/proto/agentsocket.proto @@ -40,6 +40,8 @@ message SyncStatusRequest { string unit = 1; } +// DependencyInfo represents a directed edge in the dependency graph from one unit +// to a unit it depends on, along with the required and current status of the dependency. message DependencyInfo { string unit = 1; string depends_on = 2; @@ -54,6 +56,20 @@ message SyncStatusResponse { repeated DependencyInfo dependencies = 3; } +message SyncListRequest {} + +// UnitInfo represents a single unit vertex in the dependency graph. +// Includes the state of the unit itself. +message UnitInfo { + string unit = 1; + string status = 2; + bool is_ready = 3; +} + +message SyncListResponse { + repeated UnitInfo units = 1; +} + // AgentSocket provides direct access to the agent over local IPC. service AgentSocket { // Ping the agent to check if it is alive. @@ -68,6 +84,8 @@ service AgentSocket { rpc SyncReady(SyncReadyRequest) returns (SyncReadyResponse); // Get the status of a unit and list its dependencies. rpc SyncStatus(SyncStatusRequest) returns (SyncStatusResponse); + // List all registered units and their current statuses. + rpc SyncList(SyncListRequest) returns (SyncListResponse); // Update app status, forwarded to coderd. rpc UpdateAppStatus(coder.agent.v2.UpdateAppStatusRequest) returns (coder.agent.v2.UpdateAppStatusResponse); } diff --git a/agent/agentsocket/proto/agentsocket_drpc.pb.go b/agent/agentsocket/proto/agentsocket_drpc.pb.go index ad5a842bad..04836e6f87 100644 --- a/agent/agentsocket/proto/agentsocket_drpc.pb.go +++ b/agent/agentsocket/proto/agentsocket_drpc.pb.go @@ -45,6 +45,7 @@ type DRPCAgentSocketClient interface { SyncComplete(ctx context.Context, in *SyncCompleteRequest) (*SyncCompleteResponse, error) SyncReady(ctx context.Context, in *SyncReadyRequest) (*SyncReadyResponse, error) SyncStatus(ctx context.Context, in *SyncStatusRequest) (*SyncStatusResponse, error) + SyncList(ctx context.Context, in *SyncListRequest) (*SyncListResponse, error) UpdateAppStatus(ctx context.Context, in *proto1.UpdateAppStatusRequest) (*proto1.UpdateAppStatusResponse, error) } @@ -112,6 +113,15 @@ func (c *drpcAgentSocketClient) SyncStatus(ctx context.Context, in *SyncStatusRe return out, nil } +func (c *drpcAgentSocketClient) SyncList(ctx context.Context, in *SyncListRequest) (*SyncListResponse, error) { + out := new(SyncListResponse) + err := c.cc.Invoke(ctx, "/coder.agentsocket.v1.AgentSocket/SyncList", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + func (c *drpcAgentSocketClient) UpdateAppStatus(ctx context.Context, in *proto1.UpdateAppStatusRequest) (*proto1.UpdateAppStatusResponse, error) { out := new(proto1.UpdateAppStatusResponse) err := c.cc.Invoke(ctx, "/coder.agentsocket.v1.AgentSocket/UpdateAppStatus", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}, in, out) @@ -128,6 +138,7 @@ type DRPCAgentSocketServer interface { SyncComplete(context.Context, *SyncCompleteRequest) (*SyncCompleteResponse, error) SyncReady(context.Context, *SyncReadyRequest) (*SyncReadyResponse, error) SyncStatus(context.Context, *SyncStatusRequest) (*SyncStatusResponse, error) + SyncList(context.Context, *SyncListRequest) (*SyncListResponse, error) UpdateAppStatus(context.Context, *proto1.UpdateAppStatusRequest) (*proto1.UpdateAppStatusResponse, error) } @@ -157,13 +168,17 @@ func (s *DRPCAgentSocketUnimplementedServer) SyncStatus(context.Context, *SyncSt return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } +func (s *DRPCAgentSocketUnimplementedServer) SyncList(context.Context, *SyncListRequest) (*SyncListResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + func (s *DRPCAgentSocketUnimplementedServer) UpdateAppStatus(context.Context, *proto1.UpdateAppStatusRequest) (*proto1.UpdateAppStatusResponse, error) { return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } type DRPCAgentSocketDescription struct{} -func (DRPCAgentSocketDescription) NumMethods() int { return 7 } +func (DRPCAgentSocketDescription) NumMethods() int { return 8 } func (DRPCAgentSocketDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { @@ -222,6 +237,15 @@ func (DRPCAgentSocketDescription) Method(n int) (string, drpc.Encoding, drpc.Rec ) }, DRPCAgentSocketServer.SyncStatus, true case 6: + return "/coder.agentsocket.v1.AgentSocket/SyncList", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAgentSocketServer). + SyncList( + ctx, + in1.(*SyncListRequest), + ) + }, DRPCAgentSocketServer.SyncList, true + case 7: return "/coder.agentsocket.v1.AgentSocket/UpdateAppStatus", drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCAgentSocketServer). @@ -335,6 +359,22 @@ func (x *drpcAgentSocket_SyncStatusStream) SendAndClose(m *SyncStatusResponse) e return x.CloseSend() } +type DRPCAgentSocket_SyncListStream interface { + drpc.Stream + SendAndClose(*SyncListResponse) error +} + +type drpcAgentSocket_SyncListStream struct { + drpc.Stream +} + +func (x *drpcAgentSocket_SyncListStream) SendAndClose(m *SyncListResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_agent_agentsocket_proto_agentsocket_proto{}); err != nil { + return err + } + return x.CloseSend() +} + type DRPCAgentSocket_UpdateAppStatusStream interface { drpc.Stream SendAndClose(*proto1.UpdateAppStatusResponse) error diff --git a/agent/agentsocket/proto/version.go b/agent/agentsocket/proto/version.go index 91be18a536..ee373de003 100644 --- a/agent/agentsocket/proto/version.go +++ b/agent/agentsocket/proto/version.go @@ -11,10 +11,13 @@ import "github.com/coder/coder/v2/apiversion" // // API v1.1: // - UpdateAppStatus RPC (forwarded to coderd) +// +// API v1.2: +// - SyncList RPC (list all registered units) const ( CurrentMajor = 1 - CurrentMinor = 1 + CurrentMinor = 2 ) var CurrentVersion = apiversion.New(CurrentMajor, CurrentMinor) diff --git a/agent/agentsocket/service.go b/agent/agentsocket/service.go index 17aecc62a0..97be1b45c5 100644 --- a/agent/agentsocket/service.go +++ b/agent/agentsocket/service.go @@ -175,6 +175,29 @@ func (s *DRPCAgentSocketService) SyncStatus(_ context.Context, req *proto.SyncSt }, nil } +// SyncList returns all registered units and their current statuses. +func (s *DRPCAgentSocketService) SyncList(_ context.Context, _ *proto.SyncListRequest) (*proto.SyncListResponse, error) { + if s.unitManager == nil { + return nil, xerrors.Errorf("cannot list units: %w", ErrUnitManagerNotAvailable) + } + + units := s.unitManager.ListUnits() + var unitInfos []*proto.UnitInfo + for _, u := range units { + isReady, err := s.unitManager.IsReady(u.ID()) + if err != nil { + return nil, xerrors.Errorf("cannot check readiness for unit %q: %w", u.ID(), err) + } + unitInfos = append(unitInfos, &proto.UnitInfo{ + Unit: string(u.ID()), + Status: string(u.Status()), + IsReady: isReady, + }) + } + + return &proto.SyncListResponse{Units: unitInfos}, nil +} + // UpdateAppStatus forwards an app status update to coderd via the // agent API. Returns an error if the agent is not connected. func (s *DRPCAgentSocketService) UpdateAppStatus(ctx context.Context, req *agentproto.UpdateAppStatusRequest) (*agentproto.UpdateAppStatusResponse, error) { diff --git a/agent/unit/manager.go b/agent/unit/manager.go index 88185d3f5e..8805abfc7a 100644 --- a/agent/unit/manager.go +++ b/agent/unit/manager.go @@ -284,6 +284,18 @@ func (m *Manager) GetUnmetDependencies(unit ID) ([]Dependency, error) { return unmetDependencies, nil } +// ListUnits returns a snapshot of all registered units and their current status. +func (m *Manager) ListUnits() []Unit { + m.mu.RLock() + defer m.mu.RUnlock() + + units := make([]Unit, 0, len(m.units)) + for _, u := range m.units { + units = append(units, u) + } + return units +} + // ExportDOT exports the dependency graph to DOT format for visualization. func (m *Manager) ExportDOT(name string) (string, error) { return m.graph.ToDOT(name) diff --git a/cli/root_test.go b/cli/root_test.go index cd2c10a781..fa65cf2973 100644 --- a/cli/root_test.go +++ b/cli/root_test.go @@ -98,6 +98,10 @@ func TestCommandHelp(t *testing.T) { Name: "coder exp sync status --help", Cmd: []string{"exp", "sync", "status", "--help"}, }, + clitest.CommandHelpCase{ + Name: "coder exp sync list --help", + Cmd: []string{"exp", "sync", "list", "--help"}, + }, )) } diff --git a/cli/sync.go b/cli/sync.go index 1d3d344ba6..01046cbeac 100644 --- a/cli/sync.go +++ b/cli/sync.go @@ -20,6 +20,7 @@ func (r *RootCmd) syncCommand() *serpent.Command { r.syncWant(&socketPath), r.syncComplete(&socketPath), r.syncStatus(&socketPath), + r.syncList(&socketPath), }, Options: serpent.OptionSet{ { diff --git a/cli/sync_list.go b/cli/sync_list.go new file mode 100644 index 0000000000..8d8b7be2b7 --- /dev/null +++ b/cli/sync_list.go @@ -0,0 +1,67 @@ +package cli + +import ( + "fmt" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/agent/agentsocket" + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/serpent" +) + +func (*RootCmd) syncList(socketPath *string) *serpent.Command { + formatter := cliui.NewOutputFormatter( + cliui.TableFormat( + []agentsocket.SyncListItem{}, + []string{ + "unit", + "status", + "ready", + }, + ), + cliui.JSONFormat(), + ) + + cmd := &serpent.Command{ + Use: "list", + Short: "List all registered units and their statuses", + Long: "List all units currently registered with the workspace agent. Shows each unit's name, status, and whether it is ready to start.", + Handler: func(i *serpent.Invocation) error { + ctx := i.Context() + + opts := []agentsocket.Option{} + if *socketPath != "" { + opts = append(opts, agentsocket.WithPath(*socketPath)) + } + + client, err := agentsocket.NewClient(ctx, opts...) + if err != nil { + return xerrors.Errorf("connect to agent socket: %w", err) + } + defer client.Close() + + items, err := client.SyncList(ctx) + if err != nil { + return xerrors.Errorf("list units failed: %w", err) + } + + if len(items) == 0 && formatter.FormatID() == "table" { + cliui.Info(i.Stdout, "No units registered") + return nil + } + + out, err := formatter.Format(ctx, items) + if err != nil { + return xerrors.Errorf("format output: %w", err) + } + + _, _ = fmt.Fprintln(i.Stdout, out) + + return nil + }, + } + + formatter.AttachOptions(&cmd.Options) + return cmd +} diff --git a/cli/sync_status.go b/cli/sync_status.go index 586727c751..e394a0e6c8 100644 --- a/cli/sync_status.go +++ b/cli/sync_status.go @@ -36,7 +36,7 @@ func (*RootCmd) syncStatus(socketPath *string) *serpent.Command { cmd := &serpent.Command{ Use: "status ", Short: "Show unit status and dependency state", - Long: "Show the current status of a unit, whether it is ready to start, and lists its dependencies. Shows which dependencies are satisfied and which are still pending. Supports multiple output formats.", + Long: "Show the current status of a unit, whether it is ready to start, and lists its dependencies. Shows which dependencies are satisfied and which are still pending.", Handler: func(i *serpent.Invocation) error { ctx := i.Context() diff --git a/cli/sync_test.go b/cli/sync_test.go index 32ddede990..6af7b10b01 100644 --- a/cli/sync_test.go +++ b/cli/sync_test.go @@ -409,4 +409,81 @@ func TestSyncCommands_Golden(t *testing.T) { clitest.TestGoldenFile(t, "TestSyncCommands_Golden/status_json_format", outBuf.Bytes(), nil) }) + + t.Run("list_no_units", func(t *testing.T) { + t.Parallel() + path, cleanup := setupSocketServer(t) + defer cleanup() + + ctx := testutil.Context(t, testutil.WaitShort) + + var outBuf bytes.Buffer + inv, _ := clitest.New(t, "exp", "sync", "list", "--socket-path", path) + inv.Stdout = &outBuf + inv.Stderr = &outBuf + + err := inv.WithContext(ctx).Run() + require.NoError(t, err) + + clitest.TestGoldenFile(t, "TestSyncCommands_Golden/list_no_units", outBuf.Bytes(), nil) + }) + + t.Run("list_with_units", func(t *testing.T) { + t.Parallel() + path, cleanup := setupSocketServer(t) + defer cleanup() + + ctx := testutil.Context(t, testutil.WaitShort) + + // Register some units in various states. + client, err := agentsocket.NewClient(ctx, agentsocket.WithPath(path)) + require.NoError(t, err) + // unit-a: started + err = client.SyncStart(ctx, "unit-a") + require.NoError(t, err) + // unit-b: completed + err = client.SyncStart(ctx, "unit-b") + require.NoError(t, err) + err = client.SyncComplete(ctx, "unit-b") + require.NoError(t, err) + // unit-c: pending (has unsatisfied dependency on unit-a completing) + err = client.SyncWant(ctx, "unit-c", "unit-a") + require.NoError(t, err) + client.Close() + + var outBuf bytes.Buffer + inv, _ := clitest.New(t, "exp", "sync", "list", "--socket-path", path) + inv.Stdout = &outBuf + inv.Stderr = &outBuf + + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + + clitest.TestGoldenFile(t, "TestSyncCommands_Golden/list_with_units", outBuf.Bytes(), nil) + }) + + t.Run("list_json_format", func(t *testing.T) { + t.Parallel() + path, cleanup := setupSocketServer(t) + defer cleanup() + + ctx := testutil.Context(t, testutil.WaitShort) + + // Register a unit. + client, err := agentsocket.NewClient(ctx, agentsocket.WithPath(path)) + require.NoError(t, err) + err = client.SyncStart(ctx, "my-unit") + require.NoError(t, err) + client.Close() + + var outBuf bytes.Buffer + inv, _ := clitest.New(t, "exp", "sync", "list", "--output", "json", "--socket-path", path) + inv.Stdout = &outBuf + inv.Stderr = &outBuf + + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + + clitest.TestGoldenFile(t, "TestSyncCommands_Golden/list_json_format", outBuf.Bytes(), nil) + }) } diff --git a/cli/testdata/TestSyncCommands_Golden/list_json_format.golden b/cli/testdata/TestSyncCommands_Golden/list_json_format.golden new file mode 100644 index 0000000000..e6f2cab589 --- /dev/null +++ b/cli/testdata/TestSyncCommands_Golden/list_json_format.golden @@ -0,0 +1,7 @@ +[ + { + "unit_name": "my-unit", + "status": "started", + "is_ready": true + } +] diff --git a/cli/testdata/TestSyncCommands_Golden/list_no_units.golden b/cli/testdata/TestSyncCommands_Golden/list_no_units.golden new file mode 100644 index 0000000000..6461012009 --- /dev/null +++ b/cli/testdata/TestSyncCommands_Golden/list_no_units.golden @@ -0,0 +1 @@ +No units registered diff --git a/cli/testdata/TestSyncCommands_Golden/list_with_units.golden b/cli/testdata/TestSyncCommands_Golden/list_with_units.golden new file mode 100644 index 0000000000..822b91fd41 --- /dev/null +++ b/cli/testdata/TestSyncCommands_Golden/list_with_units.golden @@ -0,0 +1,4 @@ +UNIT STATUS READY +unit-a started true +unit-b completed true +unit-c pending false diff --git a/cli/testdata/coder_exp_sync_--help.golden b/cli/testdata/coder_exp_sync_--help.golden index 4bb4e53c90..7ac85c9dba 100644 --- a/cli/testdata/coder_exp_sync_--help.golden +++ b/cli/testdata/coder_exp_sync_--help.golden @@ -13,6 +13,7 @@ USAGE: SUBCOMMANDS: complete Mark a unit as complete + list List all registered units and their statuses ping Test agent socket connectivity and health start Wait until all unit dependencies are satisfied status Show unit status and dependency state diff --git a/cli/testdata/coder_exp_sync_list_--help.golden b/cli/testdata/coder_exp_sync_list_--help.golden new file mode 100644 index 0000000000..8185c671e9 --- /dev/null +++ b/cli/testdata/coder_exp_sync_list_--help.golden @@ -0,0 +1,19 @@ +coder v0.0.0-devel + +USAGE: + coder exp sync list [flags] + + List all registered units and their statuses + + List all units currently registered with the workspace agent. Shows each + unit's name, status, and whether it is ready to start. + +OPTIONS: + -c, --column [unit|status|ready] (default: unit,status,ready) + Columns to display in table output. + + -o, --output table|json (default: table) + Output format. + +——— +Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_exp_sync_status_--help.golden b/cli/testdata/coder_exp_sync_status_--help.golden index ce7d8617be..7b10d76be5 100644 --- a/cli/testdata/coder_exp_sync_status_--help.golden +++ b/cli/testdata/coder_exp_sync_status_--help.golden @@ -7,7 +7,7 @@ USAGE: Show the current status of a unit, whether it is ready to start, and lists its dependencies. Shows which dependencies are satisfied and which are still - pending. Supports multiple output formats. + pending. OPTIONS: -c, --column [depends on|required status|current status|satisfied] (default: depends on,required status,current status,satisfied)