From 714320c0189ad6b0dc7ffa0ff123792c58d7477c Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 2 Apr 2026 13:13:10 -0700 Subject: [PATCH] Add edge routing and health check RPCs (Phase 2) New agent RPCs for v2 multi-node orchestration: - SetupEdgeRoute: provisions TLS cert from Metacrypt, resolves backend hostname to Tailnet IP, validates it's in 100.64.0.0/10, registers L7 route in mc-proxy. Rejects backend_tls=false. - RemoveEdgeRoute: removes mc-proxy route, cleans up TLS cert, removes registry entry. - ListEdgeRoutes: returns all edge routes with cert serial/expiry. - HealthCheck: returns agent health and container count. New database table (migration 4): edge_routes stores hostname, backend info, and cert paths for persistence across agent restarts. ProxyRouter gains CertPath/KeyPath helpers for consistent cert path construction. Security: - Backend hostname must resolve to a Tailnet IP (100.64.0.0/10) - backend_tls=false is rejected (no cleartext to backends) - Cert provisioning failure fails the setup (no route to missing cert) Co-Authored-By: Claude Opus 4.6 (1M context) --- gen/mcp/v1/mcp.pb.go | 606 ++++++++++++++++++++++++++++--- gen/mcp/v1/mcp_grpc.pb.go | 156 ++++++++ internal/agent/edge_rpc.go | 196 ++++++++++ internal/agent/proxy.go | 10 + internal/registry/db.go | 14 + internal/registry/edge_routes.go | 93 +++++ proto/mcp/v1/mcp.proto | 50 +++ 7 files changed, 1069 insertions(+), 56 deletions(-) create mode 100644 internal/agent/edge_rpc.go create mode 100644 internal/registry/edge_routes.go diff --git a/gen/mcp/v1/mcp.pb.go b/gen/mcp/v1/mcp.pb.go index 4c78b67..8f517ce 100644 --- a/gen/mcp/v1/mcp.pb.go +++ b/gen/mcp/v1/mcp.pb.go @@ -211,6 +211,7 @@ type ServiceSpec struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Active bool `protobuf:"varint,2,opt,name=active,proto3" json:"active,omitempty"` Components []*ComponentSpec `protobuf:"bytes,3,rep,name=components,proto3" json:"components,omitempty"` + Comment string `protobuf:"bytes,4,opt,name=comment,proto3" json:"comment,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -266,6 +267,13 @@ func (x *ServiceSpec) GetComponents() []*ComponentSpec { return nil } +func (x *ServiceSpec) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + type DeployRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Service *ServiceSpec `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` @@ -990,6 +998,7 @@ type ServiceInfo struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Active bool `protobuf:"varint,2,opt,name=active,proto3" json:"active,omitempty"` Components []*ComponentInfo `protobuf:"bytes,3,rep,name=components,proto3" json:"components,omitempty"` + Comment string `protobuf:"bytes,4,opt,name=comment,proto3" json:"comment,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1045,6 +1054,13 @@ func (x *ServiceInfo) GetComponents() []*ComponentInfo { return nil } +func (x *ServiceInfo) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + type ComponentInfo struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -3048,6 +3064,434 @@ func (*RemoveProxyRouteResponse) Descriptor() ([]byte, []int) { return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{52} } +type SetupEdgeRouteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` // public hostname (e.g. "mcq.metacircular.net") + BackendHostname string `protobuf:"bytes,2,opt,name=backend_hostname,json=backendHostname,proto3" json:"backend_hostname,omitempty"` // internal .svc.mcp hostname + BackendPort int32 `protobuf:"varint,3,opt,name=backend_port,json=backendPort,proto3" json:"backend_port,omitempty"` // port on worker's mc-proxy + BackendTls bool `protobuf:"varint,4,opt,name=backend_tls,json=backendTls,proto3" json:"backend_tls,omitempty"` // MUST be true; agent rejects false + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetupEdgeRouteRequest) Reset() { + *x = SetupEdgeRouteRequest{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetupEdgeRouteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetupEdgeRouteRequest) ProtoMessage() {} + +func (x *SetupEdgeRouteRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[53] + 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 SetupEdgeRouteRequest.ProtoReflect.Descriptor instead. +func (*SetupEdgeRouteRequest) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{53} +} + +func (x *SetupEdgeRouteRequest) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *SetupEdgeRouteRequest) GetBackendHostname() string { + if x != nil { + return x.BackendHostname + } + return "" +} + +func (x *SetupEdgeRouteRequest) GetBackendPort() int32 { + if x != nil { + return x.BackendPort + } + return 0 +} + +func (x *SetupEdgeRouteRequest) GetBackendTls() bool { + if x != nil { + return x.BackendTls + } + return false +} + +type SetupEdgeRouteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetupEdgeRouteResponse) Reset() { + *x = SetupEdgeRouteResponse{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetupEdgeRouteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetupEdgeRouteResponse) ProtoMessage() {} + +func (x *SetupEdgeRouteResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[54] + 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 SetupEdgeRouteResponse.ProtoReflect.Descriptor instead. +func (*SetupEdgeRouteResponse) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{54} +} + +type RemoveEdgeRouteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveEdgeRouteRequest) Reset() { + *x = RemoveEdgeRouteRequest{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveEdgeRouteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveEdgeRouteRequest) ProtoMessage() {} + +func (x *RemoveEdgeRouteRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[55] + 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 RemoveEdgeRouteRequest.ProtoReflect.Descriptor instead. +func (*RemoveEdgeRouteRequest) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{55} +} + +func (x *RemoveEdgeRouteRequest) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +type RemoveEdgeRouteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveEdgeRouteResponse) Reset() { + *x = RemoveEdgeRouteResponse{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveEdgeRouteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveEdgeRouteResponse) ProtoMessage() {} + +func (x *RemoveEdgeRouteResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[56] + 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 RemoveEdgeRouteResponse.ProtoReflect.Descriptor instead. +func (*RemoveEdgeRouteResponse) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{56} +} + +type ListEdgeRoutesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListEdgeRoutesRequest) Reset() { + *x = ListEdgeRoutesRequest{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListEdgeRoutesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListEdgeRoutesRequest) ProtoMessage() {} + +func (x *ListEdgeRoutesRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[57] + 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 ListEdgeRoutesRequest.ProtoReflect.Descriptor instead. +func (*ListEdgeRoutesRequest) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{57} +} + +type ListEdgeRoutesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Routes []*EdgeRoute `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListEdgeRoutesResponse) Reset() { + *x = ListEdgeRoutesResponse{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListEdgeRoutesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListEdgeRoutesResponse) ProtoMessage() {} + +func (x *ListEdgeRoutesResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[58] + 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 ListEdgeRoutesResponse.ProtoReflect.Descriptor instead. +func (*ListEdgeRoutesResponse) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{58} +} + +func (x *ListEdgeRoutesResponse) GetRoutes() []*EdgeRoute { + if x != nil { + return x.Routes + } + return nil +} + +type EdgeRoute struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` + BackendHostname string `protobuf:"bytes,2,opt,name=backend_hostname,json=backendHostname,proto3" json:"backend_hostname,omitempty"` + BackendPort int32 `protobuf:"varint,3,opt,name=backend_port,json=backendPort,proto3" json:"backend_port,omitempty"` + CertSerial string `protobuf:"bytes,4,opt,name=cert_serial,json=certSerial,proto3" json:"cert_serial,omitempty"` + CertExpires string `protobuf:"bytes,5,opt,name=cert_expires,json=certExpires,proto3" json:"cert_expires,omitempty"` // RFC3339 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EdgeRoute) Reset() { + *x = EdgeRoute{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EdgeRoute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EdgeRoute) ProtoMessage() {} + +func (x *EdgeRoute) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[59] + 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 EdgeRoute.ProtoReflect.Descriptor instead. +func (*EdgeRoute) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{59} +} + +func (x *EdgeRoute) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *EdgeRoute) GetBackendHostname() string { + if x != nil { + return x.BackendHostname + } + return "" +} + +func (x *EdgeRoute) GetBackendPort() int32 { + if x != nil { + return x.BackendPort + } + return 0 +} + +func (x *EdgeRoute) GetCertSerial() string { + if x != nil { + return x.CertSerial + } + return "" +} + +func (x *EdgeRoute) GetCertExpires() string { + if x != nil { + return x.CertExpires + } + return "" +} + +type HealthCheckRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthCheckRequest) Reset() { + *x = HealthCheckRequest{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthCheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckRequest) ProtoMessage() {} + +func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[60] + 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 HealthCheckRequest.ProtoReflect.Descriptor instead. +func (*HealthCheckRequest) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{60} +} + +type HealthCheckResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` // "healthy" or "degraded" + Containers int32 `protobuf:"varint,2,opt,name=containers,proto3" json:"containers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthCheckResponse) Reset() { + *x = HealthCheckResponse{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthCheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckResponse) ProtoMessage() {} + +func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[61] + 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 HealthCheckResponse.ProtoReflect.Descriptor instead. +func (*HealthCheckResponse) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{61} +} + +func (x *HealthCheckResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *HealthCheckResponse) GetContainers() int32 { + if x != nil { + return x.Containers + } + return 0 +} + var File_proto_mcp_v1_mcp_proto protoreflect.FileDescriptor const file_proto_mcp_v1_mcp_proto_rawDesc = "" + @@ -3069,13 +3513,14 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" + "\x03cmd\x18\b \x03(\tR\x03cmd\x12)\n" + "\x06routes\x18\t \x03(\v2\x11.mcp.v1.RouteSpecR\x06routes\x12\x10\n" + "\x03env\x18\n" + - " \x03(\tR\x03env\"p\n" + + " \x03(\tR\x03env\"\x8a\x01\n" + "\vServiceSpec\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + "\x06active\x18\x02 \x01(\bR\x06active\x125\n" + "\n" + "components\x18\x03 \x03(\v2\x15.mcp.v1.ComponentSpecR\n" + - "components\"\\\n" + + "components\x12\x18\n" + + "\acomment\x18\x04 \x01(\tR\acomment\"\\\n" + "\rDeployRequest\x12-\n" + "\aservice\x18\x01 \x01(\v2\x13.mcp.v1.ServiceSpecR\aservice\x12\x1c\n" + "\tcomponent\x18\x02 \x01(\tR\tcomponent\"C\n" + @@ -3112,13 +3557,14 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + "\achanged\x18\x02 \x01(\bR\achanged\x12\x18\n" + "\asummary\x18\x03 \x01(\tR\asummary\"\x15\n" + - "\x13ListServicesRequest\"p\n" + + "\x13ListServicesRequest\"\x8a\x01\n" + "\vServiceInfo\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + "\x06active\x18\x02 \x01(\bR\x06active\x125\n" + "\n" + "components\x18\x03 \x03(\v2\x15.mcp.v1.ComponentInfoR\n" + - "components\"\xd5\x01\n" + + "components\x12\x18\n" + + "\acomment\x18\x04 \x01(\tR\acomment\"\xd5\x01\n" + "\rComponentInfo\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + "\x05image\x18\x02 \x01(\tR\x05image\x12#\n" + @@ -3255,7 +3701,33 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" + "\x17RemoveProxyRouteRequest\x12#\n" + "\rlistener_addr\x18\x01 \x01(\tR\flistenerAddr\x12\x1a\n" + "\bhostname\x18\x02 \x01(\tR\bhostname\"\x1a\n" + - "\x18RemoveProxyRouteResponse2\x92\v\n" + + "\x18RemoveProxyRouteResponse\"\xa2\x01\n" + + "\x15SetupEdgeRouteRequest\x12\x1a\n" + + "\bhostname\x18\x01 \x01(\tR\bhostname\x12)\n" + + "\x10backend_hostname\x18\x02 \x01(\tR\x0fbackendHostname\x12!\n" + + "\fbackend_port\x18\x03 \x01(\x05R\vbackendPort\x12\x1f\n" + + "\vbackend_tls\x18\x04 \x01(\bR\n" + + "backendTls\"\x18\n" + + "\x16SetupEdgeRouteResponse\"4\n" + + "\x16RemoveEdgeRouteRequest\x12\x1a\n" + + "\bhostname\x18\x01 \x01(\tR\bhostname\"\x19\n" + + "\x17RemoveEdgeRouteResponse\"\x17\n" + + "\x15ListEdgeRoutesRequest\"C\n" + + "\x16ListEdgeRoutesResponse\x12)\n" + + "\x06routes\x18\x01 \x03(\v2\x11.mcp.v1.EdgeRouteR\x06routes\"\xb9\x01\n" + + "\tEdgeRoute\x12\x1a\n" + + "\bhostname\x18\x01 \x01(\tR\bhostname\x12)\n" + + "\x10backend_hostname\x18\x02 \x01(\tR\x0fbackendHostname\x12!\n" + + "\fbackend_port\x18\x03 \x01(\x05R\vbackendPort\x12\x1f\n" + + "\vcert_serial\x18\x04 \x01(\tR\n" + + "certSerial\x12!\n" + + "\fcert_expires\x18\x05 \x01(\tR\vcertExpires\"\x14\n" + + "\x12HealthCheckRequest\"M\n" + + "\x13HealthCheckResponse\x12\x16\n" + + "\x06status\x18\x01 \x01(\tR\x06status\x12\x1e\n" + + "\n" + + "containers\x18\x02 \x01(\x05R\n" + + "containers2\xd0\r\n" + "\x0fMcpAgentService\x127\n" + "\x06Deploy\x12\x15.mcp.v1.DeployRequest\x1a\x16.mcp.v1.DeployResponse\x12R\n" + "\x0fUndeployService\x12\x1e.mcp.v1.UndeployServiceRequest\x1a\x1f.mcp.v1.UndeployServiceResponse\x12F\n" + @@ -3275,7 +3747,11 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" + "\x0eListDNSRecords\x12\x1d.mcp.v1.ListDNSRecordsRequest\x1a\x1e.mcp.v1.ListDNSRecordsResponse\x12R\n" + "\x0fListProxyRoutes\x12\x1e.mcp.v1.ListProxyRoutesRequest\x1a\x1f.mcp.v1.ListProxyRoutesResponse\x12L\n" + "\rAddProxyRoute\x12\x1c.mcp.v1.AddProxyRouteRequest\x1a\x1d.mcp.v1.AddProxyRouteResponse\x12U\n" + - "\x10RemoveProxyRoute\x12\x1f.mcp.v1.RemoveProxyRouteRequest\x1a .mcp.v1.RemoveProxyRouteResponse\x123\n" + + "\x10RemoveProxyRoute\x12\x1f.mcp.v1.RemoveProxyRouteRequest\x1a .mcp.v1.RemoveProxyRouteResponse\x12O\n" + + "\x0eSetupEdgeRoute\x12\x1d.mcp.v1.SetupEdgeRouteRequest\x1a\x1e.mcp.v1.SetupEdgeRouteResponse\x12R\n" + + "\x0fRemoveEdgeRoute\x12\x1e.mcp.v1.RemoveEdgeRouteRequest\x1a\x1f.mcp.v1.RemoveEdgeRouteResponse\x12O\n" + + "\x0eListEdgeRoutes\x12\x1d.mcp.v1.ListEdgeRoutesRequest\x1a\x1e.mcp.v1.ListEdgeRoutesResponse\x12F\n" + + "\vHealthCheck\x12\x1a.mcp.v1.HealthCheckRequest\x1a\x1b.mcp.v1.HealthCheckResponse\x123\n" + "\x04Logs\x12\x13.mcp.v1.LogsRequest\x1a\x14.mcp.v1.LogsResponse0\x01B*Z(git.wntrmute.dev/mc/mcp/gen/mcp/v1;mcpv1b\x06proto3" var ( @@ -3290,7 +3766,7 @@ func file_proto_mcp_v1_mcp_proto_rawDescGZIP() []byte { return file_proto_mcp_v1_mcp_proto_rawDescData } -var file_proto_mcp_v1_mcp_proto_msgTypes = make([]protoimpl.MessageInfo, 53) +var file_proto_mcp_v1_mcp_proto_msgTypes = make([]protoimpl.MessageInfo, 62) var file_proto_mcp_v1_mcp_proto_goTypes = []any{ (*RouteSpec)(nil), // 0: mcp.v1.RouteSpec (*ComponentSpec)(nil), // 1: mcp.v1.ComponentSpec @@ -3345,7 +3821,16 @@ var file_proto_mcp_v1_mcp_proto_goTypes = []any{ (*AddProxyRouteResponse)(nil), // 50: mcp.v1.AddProxyRouteResponse (*RemoveProxyRouteRequest)(nil), // 51: mcp.v1.RemoveProxyRouteRequest (*RemoveProxyRouteResponse)(nil), // 52: mcp.v1.RemoveProxyRouteResponse - (*timestamppb.Timestamp)(nil), // 53: google.protobuf.Timestamp + (*SetupEdgeRouteRequest)(nil), // 53: mcp.v1.SetupEdgeRouteRequest + (*SetupEdgeRouteResponse)(nil), // 54: mcp.v1.SetupEdgeRouteResponse + (*RemoveEdgeRouteRequest)(nil), // 55: mcp.v1.RemoveEdgeRouteRequest + (*RemoveEdgeRouteResponse)(nil), // 56: mcp.v1.RemoveEdgeRouteResponse + (*ListEdgeRoutesRequest)(nil), // 57: mcp.v1.ListEdgeRoutesRequest + (*ListEdgeRoutesResponse)(nil), // 58: mcp.v1.ListEdgeRoutesResponse + (*EdgeRoute)(nil), // 59: mcp.v1.EdgeRoute + (*HealthCheckRequest)(nil), // 60: mcp.v1.HealthCheckRequest + (*HealthCheckResponse)(nil), // 61: mcp.v1.HealthCheckResponse + (*timestamppb.Timestamp)(nil), // 62: google.protobuf.Timestamp } var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{ 0, // 0: mcp.v1.ComponentSpec.routes:type_name -> mcp.v1.RouteSpec @@ -3359,64 +3844,73 @@ var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{ 2, // 8: mcp.v1.SyncDesiredStateRequest.services:type_name -> mcp.v1.ServiceSpec 16, // 9: mcp.v1.SyncDesiredStateResponse.results:type_name -> mcp.v1.ServiceSyncResult 19, // 10: mcp.v1.ServiceInfo.components:type_name -> mcp.v1.ComponentInfo - 53, // 11: mcp.v1.ComponentInfo.started:type_name -> google.protobuf.Timestamp + 62, // 11: mcp.v1.ComponentInfo.started:type_name -> google.protobuf.Timestamp 18, // 12: mcp.v1.ListServicesResponse.services:type_name -> mcp.v1.ServiceInfo - 53, // 13: mcp.v1.EventInfo.timestamp:type_name -> google.protobuf.Timestamp + 62, // 13: mcp.v1.EventInfo.timestamp:type_name -> google.protobuf.Timestamp 18, // 14: mcp.v1.GetServiceStatusResponse.services:type_name -> mcp.v1.ServiceInfo 22, // 15: mcp.v1.GetServiceStatusResponse.drift:type_name -> mcp.v1.DriftInfo 23, // 16: mcp.v1.GetServiceStatusResponse.recent_events:type_name -> mcp.v1.EventInfo 18, // 17: mcp.v1.LiveCheckResponse.services:type_name -> mcp.v1.ServiceInfo 28, // 18: mcp.v1.AdoptContainersResponse.results:type_name -> mcp.v1.AdoptResult - 53, // 19: mcp.v1.NodeStatusResponse.uptime_since:type_name -> google.protobuf.Timestamp + 62, // 19: mcp.v1.NodeStatusResponse.uptime_since:type_name -> google.protobuf.Timestamp 38, // 20: mcp.v1.PurgeResponse.results:type_name -> mcp.v1.PurgeResult 43, // 21: mcp.v1.DNSZone.records:type_name -> mcp.v1.DNSRecord 42, // 22: mcp.v1.ListDNSRecordsResponse.zones:type_name -> mcp.v1.DNSZone 46, // 23: mcp.v1.ProxyListenerInfo.routes:type_name -> mcp.v1.ProxyRouteInfo - 53, // 24: mcp.v1.ListProxyRoutesResponse.started_at:type_name -> google.protobuf.Timestamp + 62, // 24: mcp.v1.ListProxyRoutesResponse.started_at:type_name -> google.protobuf.Timestamp 47, // 25: mcp.v1.ListProxyRoutesResponse.listeners:type_name -> mcp.v1.ProxyListenerInfo - 3, // 26: mcp.v1.McpAgentService.Deploy:input_type -> mcp.v1.DeployRequest - 12, // 27: mcp.v1.McpAgentService.UndeployService:input_type -> mcp.v1.UndeployServiceRequest - 6, // 28: mcp.v1.McpAgentService.StopService:input_type -> mcp.v1.StopServiceRequest - 8, // 29: mcp.v1.McpAgentService.StartService:input_type -> mcp.v1.StartServiceRequest - 10, // 30: mcp.v1.McpAgentService.RestartService:input_type -> mcp.v1.RestartServiceRequest - 14, // 31: mcp.v1.McpAgentService.SyncDesiredState:input_type -> mcp.v1.SyncDesiredStateRequest - 17, // 32: mcp.v1.McpAgentService.ListServices:input_type -> mcp.v1.ListServicesRequest - 21, // 33: mcp.v1.McpAgentService.GetServiceStatus:input_type -> mcp.v1.GetServiceStatusRequest - 25, // 34: mcp.v1.McpAgentService.LiveCheck:input_type -> mcp.v1.LiveCheckRequest - 27, // 35: mcp.v1.McpAgentService.AdoptContainers:input_type -> mcp.v1.AdoptContainersRequest - 36, // 36: mcp.v1.McpAgentService.PurgeComponent:input_type -> mcp.v1.PurgeRequest - 30, // 37: mcp.v1.McpAgentService.PushFile:input_type -> mcp.v1.PushFileRequest - 32, // 38: mcp.v1.McpAgentService.PullFile:input_type -> mcp.v1.PullFileRequest - 34, // 39: mcp.v1.McpAgentService.NodeStatus:input_type -> mcp.v1.NodeStatusRequest - 41, // 40: mcp.v1.McpAgentService.ListDNSRecords:input_type -> mcp.v1.ListDNSRecordsRequest - 45, // 41: mcp.v1.McpAgentService.ListProxyRoutes:input_type -> mcp.v1.ListProxyRoutesRequest - 49, // 42: mcp.v1.McpAgentService.AddProxyRoute:input_type -> mcp.v1.AddProxyRouteRequest - 51, // 43: mcp.v1.McpAgentService.RemoveProxyRoute:input_type -> mcp.v1.RemoveProxyRouteRequest - 39, // 44: mcp.v1.McpAgentService.Logs:input_type -> mcp.v1.LogsRequest - 4, // 45: mcp.v1.McpAgentService.Deploy:output_type -> mcp.v1.DeployResponse - 13, // 46: mcp.v1.McpAgentService.UndeployService:output_type -> mcp.v1.UndeployServiceResponse - 7, // 47: mcp.v1.McpAgentService.StopService:output_type -> mcp.v1.StopServiceResponse - 9, // 48: mcp.v1.McpAgentService.StartService:output_type -> mcp.v1.StartServiceResponse - 11, // 49: mcp.v1.McpAgentService.RestartService:output_type -> mcp.v1.RestartServiceResponse - 15, // 50: mcp.v1.McpAgentService.SyncDesiredState:output_type -> mcp.v1.SyncDesiredStateResponse - 20, // 51: mcp.v1.McpAgentService.ListServices:output_type -> mcp.v1.ListServicesResponse - 24, // 52: mcp.v1.McpAgentService.GetServiceStatus:output_type -> mcp.v1.GetServiceStatusResponse - 26, // 53: mcp.v1.McpAgentService.LiveCheck:output_type -> mcp.v1.LiveCheckResponse - 29, // 54: mcp.v1.McpAgentService.AdoptContainers:output_type -> mcp.v1.AdoptContainersResponse - 37, // 55: mcp.v1.McpAgentService.PurgeComponent:output_type -> mcp.v1.PurgeResponse - 31, // 56: mcp.v1.McpAgentService.PushFile:output_type -> mcp.v1.PushFileResponse - 33, // 57: mcp.v1.McpAgentService.PullFile:output_type -> mcp.v1.PullFileResponse - 35, // 58: mcp.v1.McpAgentService.NodeStatus:output_type -> mcp.v1.NodeStatusResponse - 44, // 59: mcp.v1.McpAgentService.ListDNSRecords:output_type -> mcp.v1.ListDNSRecordsResponse - 48, // 60: mcp.v1.McpAgentService.ListProxyRoutes:output_type -> mcp.v1.ListProxyRoutesResponse - 50, // 61: mcp.v1.McpAgentService.AddProxyRoute:output_type -> mcp.v1.AddProxyRouteResponse - 52, // 62: mcp.v1.McpAgentService.RemoveProxyRoute:output_type -> mcp.v1.RemoveProxyRouteResponse - 40, // 63: mcp.v1.McpAgentService.Logs:output_type -> mcp.v1.LogsResponse - 45, // [45:64] is the sub-list for method output_type - 26, // [26:45] 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 + 59, // 26: mcp.v1.ListEdgeRoutesResponse.routes:type_name -> mcp.v1.EdgeRoute + 3, // 27: mcp.v1.McpAgentService.Deploy:input_type -> mcp.v1.DeployRequest + 12, // 28: mcp.v1.McpAgentService.UndeployService:input_type -> mcp.v1.UndeployServiceRequest + 6, // 29: mcp.v1.McpAgentService.StopService:input_type -> mcp.v1.StopServiceRequest + 8, // 30: mcp.v1.McpAgentService.StartService:input_type -> mcp.v1.StartServiceRequest + 10, // 31: mcp.v1.McpAgentService.RestartService:input_type -> mcp.v1.RestartServiceRequest + 14, // 32: mcp.v1.McpAgentService.SyncDesiredState:input_type -> mcp.v1.SyncDesiredStateRequest + 17, // 33: mcp.v1.McpAgentService.ListServices:input_type -> mcp.v1.ListServicesRequest + 21, // 34: mcp.v1.McpAgentService.GetServiceStatus:input_type -> mcp.v1.GetServiceStatusRequest + 25, // 35: mcp.v1.McpAgentService.LiveCheck:input_type -> mcp.v1.LiveCheckRequest + 27, // 36: mcp.v1.McpAgentService.AdoptContainers:input_type -> mcp.v1.AdoptContainersRequest + 36, // 37: mcp.v1.McpAgentService.PurgeComponent:input_type -> mcp.v1.PurgeRequest + 30, // 38: mcp.v1.McpAgentService.PushFile:input_type -> mcp.v1.PushFileRequest + 32, // 39: mcp.v1.McpAgentService.PullFile:input_type -> mcp.v1.PullFileRequest + 34, // 40: mcp.v1.McpAgentService.NodeStatus:input_type -> mcp.v1.NodeStatusRequest + 41, // 41: mcp.v1.McpAgentService.ListDNSRecords:input_type -> mcp.v1.ListDNSRecordsRequest + 45, // 42: mcp.v1.McpAgentService.ListProxyRoutes:input_type -> mcp.v1.ListProxyRoutesRequest + 49, // 43: mcp.v1.McpAgentService.AddProxyRoute:input_type -> mcp.v1.AddProxyRouteRequest + 51, // 44: mcp.v1.McpAgentService.RemoveProxyRoute:input_type -> mcp.v1.RemoveProxyRouteRequest + 53, // 45: mcp.v1.McpAgentService.SetupEdgeRoute:input_type -> mcp.v1.SetupEdgeRouteRequest + 55, // 46: mcp.v1.McpAgentService.RemoveEdgeRoute:input_type -> mcp.v1.RemoveEdgeRouteRequest + 57, // 47: mcp.v1.McpAgentService.ListEdgeRoutes:input_type -> mcp.v1.ListEdgeRoutesRequest + 60, // 48: mcp.v1.McpAgentService.HealthCheck:input_type -> mcp.v1.HealthCheckRequest + 39, // 49: mcp.v1.McpAgentService.Logs:input_type -> mcp.v1.LogsRequest + 4, // 50: mcp.v1.McpAgentService.Deploy:output_type -> mcp.v1.DeployResponse + 13, // 51: mcp.v1.McpAgentService.UndeployService:output_type -> mcp.v1.UndeployServiceResponse + 7, // 52: mcp.v1.McpAgentService.StopService:output_type -> mcp.v1.StopServiceResponse + 9, // 53: mcp.v1.McpAgentService.StartService:output_type -> mcp.v1.StartServiceResponse + 11, // 54: mcp.v1.McpAgentService.RestartService:output_type -> mcp.v1.RestartServiceResponse + 15, // 55: mcp.v1.McpAgentService.SyncDesiredState:output_type -> mcp.v1.SyncDesiredStateResponse + 20, // 56: mcp.v1.McpAgentService.ListServices:output_type -> mcp.v1.ListServicesResponse + 24, // 57: mcp.v1.McpAgentService.GetServiceStatus:output_type -> mcp.v1.GetServiceStatusResponse + 26, // 58: mcp.v1.McpAgentService.LiveCheck:output_type -> mcp.v1.LiveCheckResponse + 29, // 59: mcp.v1.McpAgentService.AdoptContainers:output_type -> mcp.v1.AdoptContainersResponse + 37, // 60: mcp.v1.McpAgentService.PurgeComponent:output_type -> mcp.v1.PurgeResponse + 31, // 61: mcp.v1.McpAgentService.PushFile:output_type -> mcp.v1.PushFileResponse + 33, // 62: mcp.v1.McpAgentService.PullFile:output_type -> mcp.v1.PullFileResponse + 35, // 63: mcp.v1.McpAgentService.NodeStatus:output_type -> mcp.v1.NodeStatusResponse + 44, // 64: mcp.v1.McpAgentService.ListDNSRecords:output_type -> mcp.v1.ListDNSRecordsResponse + 48, // 65: mcp.v1.McpAgentService.ListProxyRoutes:output_type -> mcp.v1.ListProxyRoutesResponse + 50, // 66: mcp.v1.McpAgentService.AddProxyRoute:output_type -> mcp.v1.AddProxyRouteResponse + 52, // 67: mcp.v1.McpAgentService.RemoveProxyRoute:output_type -> mcp.v1.RemoveProxyRouteResponse + 54, // 68: mcp.v1.McpAgentService.SetupEdgeRoute:output_type -> mcp.v1.SetupEdgeRouteResponse + 56, // 69: mcp.v1.McpAgentService.RemoveEdgeRoute:output_type -> mcp.v1.RemoveEdgeRouteResponse + 58, // 70: mcp.v1.McpAgentService.ListEdgeRoutes:output_type -> mcp.v1.ListEdgeRoutesResponse + 61, // 71: mcp.v1.McpAgentService.HealthCheck:output_type -> mcp.v1.HealthCheckResponse + 40, // 72: mcp.v1.McpAgentService.Logs:output_type -> mcp.v1.LogsResponse + 50, // [50:73] is the sub-list for method output_type + 27, // [27:50] 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_proto_mcp_v1_mcp_proto_init() } @@ -3430,7 +3924,7 @@ func file_proto_mcp_v1_mcp_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcp_v1_mcp_proto_rawDesc), len(file_proto_mcp_v1_mcp_proto_rawDesc)), NumEnums: 0, - NumMessages: 53, + NumMessages: 62, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/mcp/v1/mcp_grpc.pb.go b/gen/mcp/v1/mcp_grpc.pb.go index 5c5fddc..75f5120 100644 --- a/gen/mcp/v1/mcp_grpc.pb.go +++ b/gen/mcp/v1/mcp_grpc.pb.go @@ -37,6 +37,10 @@ const ( McpAgentService_ListProxyRoutes_FullMethodName = "/mcp.v1.McpAgentService/ListProxyRoutes" McpAgentService_AddProxyRoute_FullMethodName = "/mcp.v1.McpAgentService/AddProxyRoute" McpAgentService_RemoveProxyRoute_FullMethodName = "/mcp.v1.McpAgentService/RemoveProxyRoute" + McpAgentService_SetupEdgeRoute_FullMethodName = "/mcp.v1.McpAgentService/SetupEdgeRoute" + McpAgentService_RemoveEdgeRoute_FullMethodName = "/mcp.v1.McpAgentService/RemoveEdgeRoute" + McpAgentService_ListEdgeRoutes_FullMethodName = "/mcp.v1.McpAgentService/ListEdgeRoutes" + McpAgentService_HealthCheck_FullMethodName = "/mcp.v1.McpAgentService/HealthCheck" McpAgentService_Logs_FullMethodName = "/mcp.v1.McpAgentService/Logs" ) @@ -71,6 +75,12 @@ type McpAgentServiceClient interface { ListProxyRoutes(ctx context.Context, in *ListProxyRoutesRequest, opts ...grpc.CallOption) (*ListProxyRoutesResponse, error) AddProxyRoute(ctx context.Context, in *AddProxyRouteRequest, opts ...grpc.CallOption) (*AddProxyRouteResponse, error) RemoveProxyRoute(ctx context.Context, in *RemoveProxyRouteRequest, opts ...grpc.CallOption) (*RemoveProxyRouteResponse, error) + // Edge routing (called by master on edge nodes) + SetupEdgeRoute(ctx context.Context, in *SetupEdgeRouteRequest, opts ...grpc.CallOption) (*SetupEdgeRouteResponse, error) + RemoveEdgeRoute(ctx context.Context, in *RemoveEdgeRouteRequest, opts ...grpc.CallOption) (*RemoveEdgeRouteResponse, error) + ListEdgeRoutes(ctx context.Context, in *ListEdgeRoutesRequest, opts ...grpc.CallOption) (*ListEdgeRoutesResponse, error) + // Health (called by master on missed heartbeats) + HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) // Logs Logs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogsResponse], error) } @@ -263,6 +273,46 @@ func (c *mcpAgentServiceClient) RemoveProxyRoute(ctx context.Context, in *Remove return out, nil } +func (c *mcpAgentServiceClient) SetupEdgeRoute(ctx context.Context, in *SetupEdgeRouteRequest, opts ...grpc.CallOption) (*SetupEdgeRouteResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SetupEdgeRouteResponse) + err := c.cc.Invoke(ctx, McpAgentService_SetupEdgeRoute_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *mcpAgentServiceClient) RemoveEdgeRoute(ctx context.Context, in *RemoveEdgeRouteRequest, opts ...grpc.CallOption) (*RemoveEdgeRouteResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RemoveEdgeRouteResponse) + err := c.cc.Invoke(ctx, McpAgentService_RemoveEdgeRoute_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *mcpAgentServiceClient) ListEdgeRoutes(ctx context.Context, in *ListEdgeRoutesRequest, opts ...grpc.CallOption) (*ListEdgeRoutesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListEdgeRoutesResponse) + err := c.cc.Invoke(ctx, McpAgentService_ListEdgeRoutes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *mcpAgentServiceClient) HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HealthCheckResponse) + err := c.cc.Invoke(ctx, McpAgentService_HealthCheck_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *mcpAgentServiceClient) Logs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogsResponse], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &McpAgentService_ServiceDesc.Streams[0], McpAgentService_Logs_FullMethodName, cOpts...) @@ -313,6 +363,12 @@ type McpAgentServiceServer interface { ListProxyRoutes(context.Context, *ListProxyRoutesRequest) (*ListProxyRoutesResponse, error) AddProxyRoute(context.Context, *AddProxyRouteRequest) (*AddProxyRouteResponse, error) RemoveProxyRoute(context.Context, *RemoveProxyRouteRequest) (*RemoveProxyRouteResponse, error) + // Edge routing (called by master on edge nodes) + SetupEdgeRoute(context.Context, *SetupEdgeRouteRequest) (*SetupEdgeRouteResponse, error) + RemoveEdgeRoute(context.Context, *RemoveEdgeRouteRequest) (*RemoveEdgeRouteResponse, error) + ListEdgeRoutes(context.Context, *ListEdgeRoutesRequest) (*ListEdgeRoutesResponse, error) + // Health (called by master on missed heartbeats) + HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) // Logs Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error mustEmbedUnimplementedMcpAgentServiceServer() @@ -379,6 +435,18 @@ func (UnimplementedMcpAgentServiceServer) AddProxyRoute(context.Context, *AddPro func (UnimplementedMcpAgentServiceServer) RemoveProxyRoute(context.Context, *RemoveProxyRouteRequest) (*RemoveProxyRouteResponse, error) { return nil, status.Error(codes.Unimplemented, "method RemoveProxyRoute not implemented") } +func (UnimplementedMcpAgentServiceServer) SetupEdgeRoute(context.Context, *SetupEdgeRouteRequest) (*SetupEdgeRouteResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SetupEdgeRoute not implemented") +} +func (UnimplementedMcpAgentServiceServer) RemoveEdgeRoute(context.Context, *RemoveEdgeRouteRequest) (*RemoveEdgeRouteResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveEdgeRoute not implemented") +} +func (UnimplementedMcpAgentServiceServer) ListEdgeRoutes(context.Context, *ListEdgeRoutesRequest) (*ListEdgeRoutesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListEdgeRoutes not implemented") +} +func (UnimplementedMcpAgentServiceServer) HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) { + return nil, status.Error(codes.Unimplemented, "method HealthCheck not implemented") +} func (UnimplementedMcpAgentServiceServer) Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error { return status.Error(codes.Unimplemented, "method Logs not implemented") } @@ -727,6 +795,78 @@ func _McpAgentService_RemoveProxyRoute_Handler(srv interface{}, ctx context.Cont return interceptor(ctx, in, info, handler) } +func _McpAgentService_SetupEdgeRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetupEdgeRouteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(McpAgentServiceServer).SetupEdgeRoute(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: McpAgentService_SetupEdgeRoute_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(McpAgentServiceServer).SetupEdgeRoute(ctx, req.(*SetupEdgeRouteRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _McpAgentService_RemoveEdgeRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveEdgeRouteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(McpAgentServiceServer).RemoveEdgeRoute(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: McpAgentService_RemoveEdgeRoute_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(McpAgentServiceServer).RemoveEdgeRoute(ctx, req.(*RemoveEdgeRouteRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _McpAgentService_ListEdgeRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListEdgeRoutesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(McpAgentServiceServer).ListEdgeRoutes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: McpAgentService_ListEdgeRoutes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(McpAgentServiceServer).ListEdgeRoutes(ctx, req.(*ListEdgeRoutesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _McpAgentService_HealthCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthCheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(McpAgentServiceServer).HealthCheck(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: McpAgentService_HealthCheck_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(McpAgentServiceServer).HealthCheck(ctx, req.(*HealthCheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _McpAgentService_Logs_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(LogsRequest) if err := stream.RecvMsg(m); err != nil { @@ -817,6 +957,22 @@ var McpAgentService_ServiceDesc = grpc.ServiceDesc{ MethodName: "RemoveProxyRoute", Handler: _McpAgentService_RemoveProxyRoute_Handler, }, + { + MethodName: "SetupEdgeRoute", + Handler: _McpAgentService_SetupEdgeRoute_Handler, + }, + { + MethodName: "RemoveEdgeRoute", + Handler: _McpAgentService_RemoveEdgeRoute_Handler, + }, + { + MethodName: "ListEdgeRoutes", + Handler: _McpAgentService_ListEdgeRoutes_Handler, + }, + { + MethodName: "HealthCheck", + Handler: _McpAgentService_HealthCheck_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/internal/agent/edge_rpc.go b/internal/agent/edge_rpc.go new file mode 100644 index 0000000..bea595c --- /dev/null +++ b/internal/agent/edge_rpc.go @@ -0,0 +1,196 @@ +package agent + +import ( + "context" + "crypto/x509" + "encoding/pem" + "fmt" + "net" + "os" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1" + mcproxy "git.wntrmute.dev/mc/mc-proxy/client/mcproxy" + "git.wntrmute.dev/mc/mcp/internal/registry" +) + +// SetupEdgeRoute provisions a TLS cert and registers an mc-proxy route for a +// public hostname. Called by the master on edge nodes. +func (a *Agent) SetupEdgeRoute(ctx context.Context, req *mcpv1.SetupEdgeRouteRequest) (*mcpv1.SetupEdgeRouteResponse, error) { + a.Logger.Info("SetupEdgeRoute", "hostname", req.GetHostname(), + "backend_hostname", req.GetBackendHostname(), "backend_port", req.GetBackendPort()) + + // Validate required fields. + if req.GetHostname() == "" { + return nil, status.Error(codes.InvalidArgument, "hostname is required") + } + if req.GetBackendHostname() == "" { + return nil, status.Error(codes.InvalidArgument, "backend_hostname is required") + } + if req.GetBackendPort() == 0 { + return nil, status.Error(codes.InvalidArgument, "backend_port is required") + } + if !req.GetBackendTls() { + return nil, status.Error(codes.InvalidArgument, "backend_tls must be true") + } + + if a.Proxy == nil { + return nil, status.Error(codes.FailedPrecondition, "mc-proxy not configured") + } + + // Resolve the backend hostname to a Tailnet IP. + ips, err := net.LookupHost(req.GetBackendHostname()) + if err != nil || len(ips) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "cannot resolve backend_hostname %q: %v", req.GetBackendHostname(), err) + } + backendIP := ips[0] + + // Validate the resolved IP is a Tailnet address (100.64.0.0/10). + ip := net.ParseIP(backendIP) + if ip == nil { + return nil, status.Errorf(codes.InvalidArgument, "resolved IP %q is not valid", backendIP) + } + _, tailnet, _ := net.ParseCIDR("100.64.0.0/10") + if !tailnet.Contains(ip) { + return nil, status.Errorf(codes.InvalidArgument, "resolved IP %s is not a Tailnet address", backendIP) + } + + backend := fmt.Sprintf("%s:%d", backendIP, req.GetBackendPort()) + + // Provision TLS cert for the public hostname if cert provisioner is available. + certPath := "" + keyPath := "" + if a.Certs != nil { + if err := a.Certs.EnsureCert(ctx, req.GetHostname(), []string{req.GetHostname()}); err != nil { + return nil, status.Errorf(codes.Internal, "provision cert for %s: %v", req.GetHostname(), err) + } + certPath = a.Proxy.CertPath(req.GetHostname()) + keyPath = a.Proxy.KeyPath(req.GetHostname()) + } else { + // No cert provisioner — check if certs already exist on disk. + certPath = a.Proxy.CertPath(req.GetHostname()) + keyPath = a.Proxy.KeyPath(req.GetHostname()) + if _, err := os.Stat(certPath); err != nil { + return nil, status.Errorf(codes.FailedPrecondition, "no cert provisioner and cert not found at %s", certPath) + } + } + + // Register the L7 route in mc-proxy. + route := mcproxy.Route{ + Hostname: req.GetHostname(), + Backend: backend, + Mode: "l7", + TLSCert: certPath, + TLSKey: keyPath, + BackendTLS: true, + } + if err := a.Proxy.AddRoute(ctx, ":443", route); err != nil { + return nil, status.Errorf(codes.Internal, "add mc-proxy route: %v", err) + } + + // Persist the edge route in the registry. + if err := registry.CreateEdgeRoute(a.DB, req.GetHostname(), req.GetBackendHostname(), int(req.GetBackendPort()), certPath, keyPath); err != nil { + a.Logger.Warn("failed to persist edge route", "hostname", req.GetHostname(), "err", err) + } + + a.Logger.Info("edge route established", + "hostname", req.GetHostname(), "backend", backend, "cert", certPath) + + return &mcpv1.SetupEdgeRouteResponse{}, nil +} + +// RemoveEdgeRoute removes an mc-proxy route and cleans up the TLS cert for a +// public hostname. Called by the master on edge nodes. +func (a *Agent) RemoveEdgeRoute(ctx context.Context, req *mcpv1.RemoveEdgeRouteRequest) (*mcpv1.RemoveEdgeRouteResponse, error) { + a.Logger.Info("RemoveEdgeRoute", "hostname", req.GetHostname()) + + if req.GetHostname() == "" { + return nil, status.Error(codes.InvalidArgument, "hostname is required") + } + + if a.Proxy == nil { + return nil, status.Error(codes.FailedPrecondition, "mc-proxy not configured") + } + + // Remove the mc-proxy route. + if err := a.Proxy.RemoveRoute(ctx, ":443", req.GetHostname()); err != nil { + a.Logger.Warn("remove mc-proxy route", "hostname", req.GetHostname(), "err", err) + // Continue — clean up cert and registry even if route removal fails. + } + + // Remove the TLS cert. + if a.Certs != nil { + if err := a.Certs.RemoveCert(req.GetHostname()); err != nil { + a.Logger.Warn("remove cert", "hostname", req.GetHostname(), "err", err) + } + } + + // Remove from registry. + if err := registry.DeleteEdgeRoute(a.DB, req.GetHostname()); err != nil { + a.Logger.Warn("delete edge route from registry", "hostname", req.GetHostname(), "err", err) + } + + a.Logger.Info("edge route removed", "hostname", req.GetHostname()) + return &mcpv1.RemoveEdgeRouteResponse{}, nil +} + +// ListEdgeRoutes returns all edge routes managed by this agent. +func (a *Agent) ListEdgeRoutes(_ context.Context, _ *mcpv1.ListEdgeRoutesRequest) (*mcpv1.ListEdgeRoutesResponse, error) { + a.Logger.Debug("ListEdgeRoutes called") + + routes, err := registry.ListEdgeRoutes(a.DB) + if err != nil { + return nil, status.Errorf(codes.Internal, "list edge routes: %v", err) + } + + resp := &mcpv1.ListEdgeRoutesResponse{} + for _, r := range routes { + er := &mcpv1.EdgeRoute{ + Hostname: r.Hostname, + BackendHostname: r.BackendHostname, + BackendPort: int32(r.BackendPort), //nolint:gosec // port is a small positive integer + } + + // Read cert metadata if available. + if r.TLSCert != "" { + if certData, readErr := os.ReadFile(r.TLSCert); readErr == nil { //nolint:gosec // path from registry, not user input + if block, _ := pem.Decode(certData); block != nil { + if cert, parseErr := x509.ParseCertificate(block.Bytes); parseErr == nil { + er.CertSerial = cert.SerialNumber.String() + er.CertExpires = cert.NotAfter.UTC().Format(time.RFC3339) + } + } + } + } + + resp.Routes = append(resp.Routes, er) + } + + return resp, nil +} + +// HealthCheck returns the agent's health status. Called by the master when +// heartbeats are missed. +func (a *Agent) HealthCheck(_ context.Context, _ *mcpv1.HealthCheckRequest) (*mcpv1.HealthCheckResponse, error) { + a.Logger.Debug("HealthCheck called") + + st := "healthy" + containers := int32(0) + + // Count running containers if the runtime is available. + if a.Runtime != nil { + if list, err := a.Runtime.List(context.Background()); err == nil { + containers = int32(len(list)) //nolint:gosec // container count is small + } else { + st = "degraded" + } + } + + return &mcpv1.HealthCheckResponse{ + Status: st, + Containers: containers, + }, nil +} diff --git a/internal/agent/proxy.go b/internal/agent/proxy.go index b0ef2cd..5b400ec 100644 --- a/internal/agent/proxy.go +++ b/internal/agent/proxy.go @@ -48,6 +48,16 @@ func (p *ProxyRouter) Close() error { return p.client.Close() } +// CertPath returns the expected TLS certificate path for a given name. +func (p *ProxyRouter) CertPath(name string) string { + return filepath.Join(p.certDir, name+".pem") +} + +// KeyPath returns the expected TLS key path for a given name. +func (p *ProxyRouter) KeyPath(name string) string { + return filepath.Join(p.certDir, name+".key") +} + // GetStatus returns the mc-proxy server status. func (p *ProxyRouter) GetStatus(ctx context.Context) (*mcproxy.Status, error) { if p == nil { diff --git a/internal/registry/db.go b/internal/registry/db.go index cbc7b13..db3d367 100644 --- a/internal/registry/db.go +++ b/internal/registry/db.go @@ -142,4 +142,18 @@ var migrations = []string{ FOREIGN KEY (service, component) REFERENCES components(service, name) ON DELETE CASCADE ); `, + + // Migration 3: service comment + `ALTER TABLE services ADD COLUMN comment TEXT NOT NULL DEFAULT '';`, + + // Migration 4: edge routes (v2 — public routes managed by the master) + `CREATE TABLE IF NOT EXISTS edge_routes ( + hostname TEXT NOT NULL PRIMARY KEY, + backend_hostname TEXT NOT NULL, + backend_port INTEGER NOT NULL, + tls_cert TEXT NOT NULL DEFAULT '', + tls_key TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + );`, } diff --git a/internal/registry/edge_routes.go b/internal/registry/edge_routes.go new file mode 100644 index 0000000..8a3b296 --- /dev/null +++ b/internal/registry/edge_routes.go @@ -0,0 +1,93 @@ +package registry + +import ( + "database/sql" + "fmt" + "time" +) + +// EdgeRoute represents a public edge route managed by the master. +type EdgeRoute struct { + Hostname string + BackendHostname string + BackendPort int + TLSCert string + TLSKey string + CreatedAt time.Time + UpdatedAt time.Time +} + +// CreateEdgeRoute inserts or replaces an edge route. +func CreateEdgeRoute(db *sql.DB, hostname, backendHostname string, backendPort int, tlsCert, tlsKey string) error { + _, err := db.Exec(` + INSERT INTO edge_routes (hostname, backend_hostname, backend_port, tls_cert, tls_key, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now')) + ON CONFLICT(hostname) DO UPDATE SET + backend_hostname = excluded.backend_hostname, + backend_port = excluded.backend_port, + tls_cert = excluded.tls_cert, + tls_key = excluded.tls_key, + updated_at = datetime('now') + `, hostname, backendHostname, backendPort, tlsCert, tlsKey) + if err != nil { + return fmt.Errorf("create edge route %s: %w", hostname, err) + } + return nil +} + +// GetEdgeRoute returns a single edge route by hostname. +func GetEdgeRoute(db *sql.DB, hostname string) (*EdgeRoute, error) { + var r EdgeRoute + var createdAt, updatedAt string + err := db.QueryRow(` + SELECT hostname, backend_hostname, backend_port, tls_cert, tls_key, created_at, updated_at + FROM edge_routes WHERE hostname = ? + `, hostname).Scan(&r.Hostname, &r.BackendHostname, &r.BackendPort, &r.TLSCert, &r.TLSKey, &createdAt, &updatedAt) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("get edge route %s: %w", hostname, err) + } + r.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + r.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt) + return &r, nil +} + +// ListEdgeRoutes returns all edge routes. +func ListEdgeRoutes(db *sql.DB) ([]*EdgeRoute, error) { + rows, err := db.Query(` + SELECT hostname, backend_hostname, backend_port, tls_cert, tls_key, created_at, updated_at + FROM edge_routes ORDER BY hostname + `) + if err != nil { + return nil, fmt.Errorf("list edge routes: %w", err) + } + defer func() { _ = rows.Close() }() + + var routes []*EdgeRoute + for rows.Next() { + var r EdgeRoute + var createdAt, updatedAt string + if err := rows.Scan(&r.Hostname, &r.BackendHostname, &r.BackendPort, &r.TLSCert, &r.TLSKey, &createdAt, &updatedAt); err != nil { + return nil, fmt.Errorf("scan edge route: %w", err) + } + r.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + r.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt) + routes = append(routes, &r) + } + return routes, rows.Err() +} + +// DeleteEdgeRoute removes an edge route by hostname. +func DeleteEdgeRoute(db *sql.DB, hostname string) error { + result, err := db.Exec(`DELETE FROM edge_routes WHERE hostname = ?`, hostname) + if err != nil { + return fmt.Errorf("delete edge route %s: %w", hostname, err) + } + n, _ := result.RowsAffected() + if n == 0 { + return fmt.Errorf("edge route %s not found", hostname) + } + return nil +} diff --git a/proto/mcp/v1/mcp.proto b/proto/mcp/v1/mcp.proto index 76922c7..c0fc2d5 100644 --- a/proto/mcp/v1/mcp.proto +++ b/proto/mcp/v1/mcp.proto @@ -42,6 +42,14 @@ service McpAgentService { rpc AddProxyRoute(AddProxyRouteRequest) returns (AddProxyRouteResponse); rpc RemoveProxyRoute(RemoveProxyRouteRequest) returns (RemoveProxyRouteResponse); + // Edge routing (called by master on edge nodes) + rpc SetupEdgeRoute(SetupEdgeRouteRequest) returns (SetupEdgeRouteResponse); + rpc RemoveEdgeRoute(RemoveEdgeRouteRequest) returns (RemoveEdgeRouteResponse); + rpc ListEdgeRoutes(ListEdgeRoutesRequest) returns (ListEdgeRoutesResponse); + + // Health (called by master on missed heartbeats) + rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse); + // Logs rpc Logs(LogsRequest) returns (stream LogsResponse); } @@ -72,6 +80,7 @@ message ServiceSpec { string name = 1; bool active = 2; repeated ComponentSpec components = 3; + string comment = 4; } message DeployRequest { @@ -151,6 +160,7 @@ message ServiceInfo { string name = 1; bool active = 2; repeated ComponentInfo components = 3; + string comment = 4; } message ComponentInfo { @@ -377,3 +387,43 @@ message RemoveProxyRouteRequest { } message RemoveProxyRouteResponse {} + +// --- Edge routes (v2) --- + +message SetupEdgeRouteRequest { + string hostname = 1; // public hostname (e.g. "mcq.metacircular.net") + string backend_hostname = 2; // internal .svc.mcp hostname + int32 backend_port = 3; // port on worker's mc-proxy + bool backend_tls = 4; // MUST be true; agent rejects false +} + +message SetupEdgeRouteResponse {} + +message RemoveEdgeRouteRequest { + string hostname = 1; +} + +message RemoveEdgeRouteResponse {} + +message ListEdgeRoutesRequest {} + +message ListEdgeRoutesResponse { + repeated EdgeRoute routes = 1; +} + +message EdgeRoute { + string hostname = 1; + string backend_hostname = 2; + int32 backend_port = 3; + string cert_serial = 4; + string cert_expires = 5; // RFC3339 +} + +// --- Health check (v2) --- + +message HealthCheckRequest {} + +message HealthCheckResponse { + string status = 1; // "healthy" or "degraded" + int32 containers = 2; +}