From 93e26d37895a894b81b867b6a77ec9913cc7dc2a Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sun, 29 Mar 2026 18:51:53 -0700 Subject: [PATCH] Add mcp dns and mcp node routes commands mcp dns queries MCNS via an agent to list all zones and DNS records. mcp node routes queries mc-proxy on each node for listener/route status, matching the mcproxyctl status output format. New agent RPCs: ListDNSRecords, ListProxyRoutes. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/mcp/dns.go | 87 ++++++ cmd/mcp/main.go | 1 + cmd/mcp/node.go | 54 +++- gen/mcp/v1/mcp.pb.go | 584 +++++++++++++++++++++++++++++++++--- gen/mcp/v1/mcp_grpc.pb.go | 80 +++++ internal/agent/dns.go | 87 +++++- internal/agent/dns_rpc.go | 40 +++ internal/agent/dns_test.go | 6 +- internal/agent/proxy.go | 8 + internal/agent/proxy_rpc.go | 46 +++ proto/mcp/v1/mcp.proto | 52 ++++ 11 files changed, 994 insertions(+), 51 deletions(-) create mode 100644 cmd/mcp/dns.go create mode 100644 internal/agent/dns_rpc.go create mode 100644 internal/agent/proxy_rpc.go diff --git a/cmd/mcp/dns.go b/cmd/mcp/dns.go new file mode 100644 index 0000000..758d469 --- /dev/null +++ b/cmd/mcp/dns.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "fmt" + "os" + "time" + + mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1" + "git.wntrmute.dev/mc/mcp/internal/config" + "github.com/spf13/cobra" +) + +func dnsCmd() *cobra.Command { + return &cobra.Command{ + Use: "dns", + Short: "List all DNS zones and records from MCNS", + RunE: runDNS, + } +} + +func runDNS(_ *cobra.Command, _ []string) error { + cfg, err := config.LoadCLIConfig(cfgPath) + if err != nil { + return fmt.Errorf("load config: %w", err) + } + + // DNS is centralized — query the first reachable agent. + resp, nodeName, err := queryDNS(cfg) + if err != nil { + return err + } + + if len(resp.GetZones()) == 0 { + fmt.Println("no DNS zones configured") + return nil + } + + _ = nodeName + for i, zone := range resp.GetZones() { + if i > 0 { + fmt.Println() + } + fmt.Printf("ZONE: %s\n", zone.GetName()) + + if len(zone.GetRecords()) == 0 { + fmt.Println(" (no records)") + continue + } + + w := newTable() + _, _ = fmt.Fprintln(w, " NAME\tTYPE\tVALUE\tTTL") + for _, r := range zone.GetRecords() { + _, _ = fmt.Fprintf(w, " %s\t%s\t%s\t%d\n", + r.GetName(), r.GetType(), r.GetValue(), r.GetTtl()) + } + _ = w.Flush() + } + + return nil +} + +// queryDNS tries each configured agent and returns the first successful +// DNS listing. DNS is centralized so any agent with MCNS configured works. +func queryDNS(cfg *config.CLIConfig) (*mcpv1.ListDNSRecordsResponse, string, error) { + for _, node := range cfg.Nodes { + client, conn, err := dialAgent(node.Address, cfg) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "warning: %s: %v\n", node.Name, err) + continue + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + resp, err := client.ListDNSRecords(ctx, &mcpv1.ListDNSRecordsRequest{}) + cancel() + _ = conn.Close() + + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "warning: %s: list DNS: %v\n", node.Name, err) + continue + } + + return resp, node.Name, nil + } + + return nil, "", fmt.Errorf("no reachable agent with DNS configured") +} diff --git a/cmd/mcp/main.go b/cmd/mcp/main.go index 62d4f56..fee1783 100644 --- a/cmd/mcp/main.go +++ b/cmd/mcp/main.go @@ -52,6 +52,7 @@ func main() { root.AddCommand(purgeCmd()) root.AddCommand(logsCmd()) root.AddCommand(editCmd()) + root.AddCommand(dnsCmd()) if err := root.Execute(); err != nil { log.Fatal(err) diff --git a/cmd/mcp/node.go b/cmd/mcp/node.go index 25113d3..5e530f0 100644 --- a/cmd/mcp/node.go +++ b/cmd/mcp/node.go @@ -40,10 +40,62 @@ func nodeCmd() *cobra.Command { RunE: runNodeRemove, } - cmd.AddCommand(list, add, remove) + routes := &cobra.Command{ + Use: "routes", + Short: "List mc-proxy routes on all nodes", + RunE: runNodeRoutes, + } + + cmd.AddCommand(list, add, remove, routes) return cmd } +func runNodeRoutes(_ *cobra.Command, _ []string) error { + first := true + return forEachNode(func(node config.NodeConfig, client mcpv1.McpAgentServiceClient) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + resp, err := client.ListProxyRoutes(ctx, &mcpv1.ListProxyRoutesRequest{}) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "warning: %s: list routes: %v\n", node.Name, err) + return nil + } + + if !first { + fmt.Println() + } + first = false + + fmt.Printf("NODE: %s\n", node.Name) + fmt.Printf("mc-proxy %s\n", resp.GetVersion()) + if resp.GetStartedAt() != nil { + uptime := time.Since(resp.GetStartedAt().AsTime()).Truncate(time.Second) + fmt.Printf("uptime: %s\n", uptime) + } + fmt.Printf("connections: %d\n", resp.GetTotalConnections()) + fmt.Println() + + for _, ls := range resp.GetListeners() { + fmt.Printf(" %s routes=%d active=%d\n", + ls.GetAddr(), ls.GetRouteCount(), ls.GetActiveConnections()) + for _, r := range ls.GetRoutes() { + mode := r.GetMode() + if mode == "" { + mode = "l4" + } + extra := "" + if r.GetBackendTls() { + extra = " (re-encrypt)" + } + fmt.Printf(" %s %s → %s%s\n", mode, r.GetHostname(), r.GetBackend(), extra) + } + } + + return nil + }) +} + func runNodeList(_ *cobra.Command, _ []string) error { cfg, err := config.LoadCLIConfig(cfgPath) if err != nil { diff --git a/gen/mcp/v1/mcp.pb.go b/gen/mcp/v1/mcp.pb.go index 6131208..6d0b89c 100644 --- a/gen/mcp/v1/mcp.pb.go +++ b/gen/mcp/v1/mcp.pb.go @@ -2360,6 +2360,454 @@ func (x *LogsResponse) GetData() []byte { return nil } +type ListDNSRecordsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListDNSRecordsRequest) Reset() { + *x = ListDNSRecordsRequest{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListDNSRecordsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListDNSRecordsRequest) ProtoMessage() {} + +func (x *ListDNSRecordsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[41] + 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 ListDNSRecordsRequest.ProtoReflect.Descriptor instead. +func (*ListDNSRecordsRequest) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{41} +} + +type DNSZone struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Records []*DNSRecord `protobuf:"bytes,2,rep,name=records,proto3" json:"records,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DNSZone) Reset() { + *x = DNSZone{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DNSZone) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DNSZone) ProtoMessage() {} + +func (x *DNSZone) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[42] + 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 DNSZone.ProtoReflect.Descriptor instead. +func (*DNSZone) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{42} +} + +func (x *DNSZone) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DNSZone) GetRecords() []*DNSRecord { + if x != nil { + return x.Records + } + return nil +} + +type DNSRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Value string `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` + Ttl int32 `protobuf:"varint,5,opt,name=ttl,proto3" json:"ttl,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DNSRecord) Reset() { + *x = DNSRecord{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DNSRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DNSRecord) ProtoMessage() {} + +func (x *DNSRecord) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[43] + 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 DNSRecord.ProtoReflect.Descriptor instead. +func (*DNSRecord) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{43} +} + +func (x *DNSRecord) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *DNSRecord) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DNSRecord) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *DNSRecord) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *DNSRecord) GetTtl() int32 { + if x != nil { + return x.Ttl + } + return 0 +} + +type ListDNSRecordsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Zones []*DNSZone `protobuf:"bytes,1,rep,name=zones,proto3" json:"zones,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListDNSRecordsResponse) Reset() { + *x = ListDNSRecordsResponse{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListDNSRecordsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListDNSRecordsResponse) ProtoMessage() {} + +func (x *ListDNSRecordsResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[44] + 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 ListDNSRecordsResponse.ProtoReflect.Descriptor instead. +func (*ListDNSRecordsResponse) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{44} +} + +func (x *ListDNSRecordsResponse) GetZones() []*DNSZone { + if x != nil { + return x.Zones + } + return nil +} + +type ListProxyRoutesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProxyRoutesRequest) Reset() { + *x = ListProxyRoutesRequest{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProxyRoutesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProxyRoutesRequest) ProtoMessage() {} + +func (x *ListProxyRoutesRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[45] + 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 ListProxyRoutesRequest.ProtoReflect.Descriptor instead. +func (*ListProxyRoutesRequest) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{45} +} + +type ProxyRouteInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` + Backend string `protobuf:"bytes,2,opt,name=backend,proto3" json:"backend,omitempty"` + Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` + BackendTls bool `protobuf:"varint,4,opt,name=backend_tls,json=backendTls,proto3" json:"backend_tls,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProxyRouteInfo) Reset() { + *x = ProxyRouteInfo{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProxyRouteInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyRouteInfo) ProtoMessage() {} + +func (x *ProxyRouteInfo) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[46] + 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 ProxyRouteInfo.ProtoReflect.Descriptor instead. +func (*ProxyRouteInfo) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{46} +} + +func (x *ProxyRouteInfo) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *ProxyRouteInfo) GetBackend() string { + if x != nil { + return x.Backend + } + return "" +} + +func (x *ProxyRouteInfo) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *ProxyRouteInfo) GetBackendTls() bool { + if x != nil { + return x.BackendTls + } + return false +} + +type ProxyListenerInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addr string `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"` + RouteCount int32 `protobuf:"varint,2,opt,name=route_count,json=routeCount,proto3" json:"route_count,omitempty"` + ActiveConnections int64 `protobuf:"varint,3,opt,name=active_connections,json=activeConnections,proto3" json:"active_connections,omitempty"` + Routes []*ProxyRouteInfo `protobuf:"bytes,4,rep,name=routes,proto3" json:"routes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProxyListenerInfo) Reset() { + *x = ProxyListenerInfo{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProxyListenerInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyListenerInfo) ProtoMessage() {} + +func (x *ProxyListenerInfo) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[47] + 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 ProxyListenerInfo.ProtoReflect.Descriptor instead. +func (*ProxyListenerInfo) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{47} +} + +func (x *ProxyListenerInfo) GetAddr() string { + if x != nil { + return x.Addr + } + return "" +} + +func (x *ProxyListenerInfo) GetRouteCount() int32 { + if x != nil { + return x.RouteCount + } + return 0 +} + +func (x *ProxyListenerInfo) GetActiveConnections() int64 { + if x != nil { + return x.ActiveConnections + } + return 0 +} + +func (x *ProxyListenerInfo) GetRoutes() []*ProxyRouteInfo { + if x != nil { + return x.Routes + } + return nil +} + +type ListProxyRoutesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + TotalConnections int64 `protobuf:"varint,2,opt,name=total_connections,json=totalConnections,proto3" json:"total_connections,omitempty"` + StartedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` + Listeners []*ProxyListenerInfo `protobuf:"bytes,4,rep,name=listeners,proto3" json:"listeners,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProxyRoutesResponse) Reset() { + *x = ListProxyRoutesResponse{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProxyRoutesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProxyRoutesResponse) ProtoMessage() {} + +func (x *ListProxyRoutesResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[48] + 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 ListProxyRoutesResponse.ProtoReflect.Descriptor instead. +func (*ListProxyRoutesResponse) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{48} +} + +func (x *ListProxyRoutesResponse) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *ListProxyRoutesResponse) GetTotalConnections() int64 { + if x != nil { + return x.TotalConnections + } + return 0 +} + +func (x *ListProxyRoutesResponse) GetStartedAt() *timestamppb.Timestamp { + if x != nil { + return x.StartedAt + } + return nil +} + +func (x *ListProxyRoutesResponse) GetListeners() []*ProxyListenerInfo { + if x != nil { + return x.Listeners + } + return nil +} + var File_proto_mcp_v1_mcp_proto protoreflect.FileDescriptor const file_proto_mcp_v1_mcp_proto_rawDesc = "" + @@ -2519,7 +2967,38 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" + "timestamps\x12\x14\n" + "\x05since\x18\x06 \x01(\tR\x05since\"\"\n" + "\fLogsResponse\x12\x12\n" + - "\x04data\x18\x01 \x01(\fR\x04data2\xc8\b\n" + + "\x04data\x18\x01 \x01(\fR\x04data\"\x17\n" + + "\x15ListDNSRecordsRequest\"J\n" + + "\aDNSZone\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12+\n" + + "\arecords\x18\x02 \x03(\v2\x11.mcp.v1.DNSRecordR\arecords\"k\n" + + "\tDNSRecord\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\x12\x14\n" + + "\x05value\x18\x04 \x01(\tR\x05value\x12\x10\n" + + "\x03ttl\x18\x05 \x01(\x05R\x03ttl\"?\n" + + "\x16ListDNSRecordsResponse\x12%\n" + + "\x05zones\x18\x01 \x03(\v2\x0f.mcp.v1.DNSZoneR\x05zones\"\x18\n" + + "\x16ListProxyRoutesRequest\"{\n" + + "\x0eProxyRouteInfo\x12\x1a\n" + + "\bhostname\x18\x01 \x01(\tR\bhostname\x12\x18\n" + + "\abackend\x18\x02 \x01(\tR\abackend\x12\x12\n" + + "\x04mode\x18\x03 \x01(\tR\x04mode\x12\x1f\n" + + "\vbackend_tls\x18\x04 \x01(\bR\n" + + "backendTls\"\xa7\x01\n" + + "\x11ProxyListenerInfo\x12\x12\n" + + "\x04addr\x18\x01 \x01(\tR\x04addr\x12\x1f\n" + + "\vroute_count\x18\x02 \x01(\x05R\n" + + "routeCount\x12-\n" + + "\x12active_connections\x18\x03 \x01(\x03R\x11activeConnections\x12.\n" + + "\x06routes\x18\x04 \x03(\v2\x16.mcp.v1.ProxyRouteInfoR\x06routes\"\xd4\x01\n" + + "\x17ListProxyRoutesResponse\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12+\n" + + "\x11total_connections\x18\x02 \x01(\x03R\x10totalConnections\x129\n" + + "\n" + + "started_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tstartedAt\x127\n" + + "\tlisteners\x18\x04 \x03(\v2\x19.mcp.v1.ProxyListenerInfoR\tlisteners2\xed\t\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" + @@ -2535,7 +3014,9 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" + "\bPushFile\x12\x17.mcp.v1.PushFileRequest\x1a\x18.mcp.v1.PushFileResponse\x12=\n" + "\bPullFile\x12\x17.mcp.v1.PullFileRequest\x1a\x18.mcp.v1.PullFileResponse\x12C\n" + "\n" + - "NodeStatus\x12\x19.mcp.v1.NodeStatusRequest\x1a\x1a.mcp.v1.NodeStatusResponse\x123\n" + + "NodeStatus\x12\x19.mcp.v1.NodeStatusRequest\x1a\x1a.mcp.v1.NodeStatusResponse\x12O\n" + + "\x0eListDNSRecords\x12\x1d.mcp.v1.ListDNSRecordsRequest\x1a\x1e.mcp.v1.ListDNSRecordsResponse\x12R\n" + + "\x0fListProxyRoutes\x12\x1e.mcp.v1.ListProxyRoutesRequest\x1a\x1f.mcp.v1.ListProxyRoutesResponse\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 ( @@ -2550,7 +3031,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, 41) +var file_proto_mcp_v1_mcp_proto_msgTypes = make([]protoimpl.MessageInfo, 49) var file_proto_mcp_v1_mcp_proto_goTypes = []any{ (*RouteSpec)(nil), // 0: mcp.v1.RouteSpec (*ComponentSpec)(nil), // 1: mcp.v1.ComponentSpec @@ -2593,7 +3074,15 @@ var file_proto_mcp_v1_mcp_proto_goTypes = []any{ (*PurgeResult)(nil), // 38: mcp.v1.PurgeResult (*LogsRequest)(nil), // 39: mcp.v1.LogsRequest (*LogsResponse)(nil), // 40: mcp.v1.LogsResponse - (*timestamppb.Timestamp)(nil), // 41: google.protobuf.Timestamp + (*ListDNSRecordsRequest)(nil), // 41: mcp.v1.ListDNSRecordsRequest + (*DNSZone)(nil), // 42: mcp.v1.DNSZone + (*DNSRecord)(nil), // 43: mcp.v1.DNSRecord + (*ListDNSRecordsResponse)(nil), // 44: mcp.v1.ListDNSRecordsResponse + (*ListProxyRoutesRequest)(nil), // 45: mcp.v1.ListProxyRoutesRequest + (*ProxyRouteInfo)(nil), // 46: mcp.v1.ProxyRouteInfo + (*ProxyListenerInfo)(nil), // 47: mcp.v1.ProxyListenerInfo + (*ListProxyRoutesResponse)(nil), // 48: mcp.v1.ListProxyRoutesResponse + (*timestamppb.Timestamp)(nil), // 49: google.protobuf.Timestamp } var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{ 0, // 0: mcp.v1.ComponentSpec.routes:type_name -> mcp.v1.RouteSpec @@ -2607,51 +3096,60 @@ 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 - 41, // 11: mcp.v1.ComponentInfo.started:type_name -> google.protobuf.Timestamp + 49, // 11: mcp.v1.ComponentInfo.started:type_name -> google.protobuf.Timestamp 18, // 12: mcp.v1.ListServicesResponse.services:type_name -> mcp.v1.ServiceInfo - 41, // 13: mcp.v1.EventInfo.timestamp:type_name -> google.protobuf.Timestamp + 49, // 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 - 41, // 19: mcp.v1.NodeStatusResponse.uptime_since:type_name -> google.protobuf.Timestamp + 49, // 19: mcp.v1.NodeStatusResponse.uptime_since:type_name -> google.protobuf.Timestamp 38, // 20: mcp.v1.PurgeResponse.results:type_name -> mcp.v1.PurgeResult - 3, // 21: mcp.v1.McpAgentService.Deploy:input_type -> mcp.v1.DeployRequest - 12, // 22: mcp.v1.McpAgentService.UndeployService:input_type -> mcp.v1.UndeployServiceRequest - 6, // 23: mcp.v1.McpAgentService.StopService:input_type -> mcp.v1.StopServiceRequest - 8, // 24: mcp.v1.McpAgentService.StartService:input_type -> mcp.v1.StartServiceRequest - 10, // 25: mcp.v1.McpAgentService.RestartService:input_type -> mcp.v1.RestartServiceRequest - 14, // 26: mcp.v1.McpAgentService.SyncDesiredState:input_type -> mcp.v1.SyncDesiredStateRequest - 17, // 27: mcp.v1.McpAgentService.ListServices:input_type -> mcp.v1.ListServicesRequest - 21, // 28: mcp.v1.McpAgentService.GetServiceStatus:input_type -> mcp.v1.GetServiceStatusRequest - 25, // 29: mcp.v1.McpAgentService.LiveCheck:input_type -> mcp.v1.LiveCheckRequest - 27, // 30: mcp.v1.McpAgentService.AdoptContainers:input_type -> mcp.v1.AdoptContainersRequest - 36, // 31: mcp.v1.McpAgentService.PurgeComponent:input_type -> mcp.v1.PurgeRequest - 30, // 32: mcp.v1.McpAgentService.PushFile:input_type -> mcp.v1.PushFileRequest - 32, // 33: mcp.v1.McpAgentService.PullFile:input_type -> mcp.v1.PullFileRequest - 34, // 34: mcp.v1.McpAgentService.NodeStatus:input_type -> mcp.v1.NodeStatusRequest - 39, // 35: mcp.v1.McpAgentService.Logs:input_type -> mcp.v1.LogsRequest - 4, // 36: mcp.v1.McpAgentService.Deploy:output_type -> mcp.v1.DeployResponse - 13, // 37: mcp.v1.McpAgentService.UndeployService:output_type -> mcp.v1.UndeployServiceResponse - 7, // 38: mcp.v1.McpAgentService.StopService:output_type -> mcp.v1.StopServiceResponse - 9, // 39: mcp.v1.McpAgentService.StartService:output_type -> mcp.v1.StartServiceResponse - 11, // 40: mcp.v1.McpAgentService.RestartService:output_type -> mcp.v1.RestartServiceResponse - 15, // 41: mcp.v1.McpAgentService.SyncDesiredState:output_type -> mcp.v1.SyncDesiredStateResponse - 20, // 42: mcp.v1.McpAgentService.ListServices:output_type -> mcp.v1.ListServicesResponse - 24, // 43: mcp.v1.McpAgentService.GetServiceStatus:output_type -> mcp.v1.GetServiceStatusResponse - 26, // 44: mcp.v1.McpAgentService.LiveCheck:output_type -> mcp.v1.LiveCheckResponse - 29, // 45: mcp.v1.McpAgentService.AdoptContainers:output_type -> mcp.v1.AdoptContainersResponse - 37, // 46: mcp.v1.McpAgentService.PurgeComponent:output_type -> mcp.v1.PurgeResponse - 31, // 47: mcp.v1.McpAgentService.PushFile:output_type -> mcp.v1.PushFileResponse - 33, // 48: mcp.v1.McpAgentService.PullFile:output_type -> mcp.v1.PullFileResponse - 35, // 49: mcp.v1.McpAgentService.NodeStatus:output_type -> mcp.v1.NodeStatusResponse - 40, // 50: mcp.v1.McpAgentService.Logs:output_type -> mcp.v1.LogsResponse - 36, // [36:51] is the sub-list for method output_type - 21, // [21:36] is the sub-list for method input_type - 21, // [21:21] is the sub-list for extension type_name - 21, // [21:21] is the sub-list for extension extendee - 0, // [0:21] is the sub-list for field type_name + 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 + 49, // 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 + 39, // 42: mcp.v1.McpAgentService.Logs:input_type -> mcp.v1.LogsRequest + 4, // 43: mcp.v1.McpAgentService.Deploy:output_type -> mcp.v1.DeployResponse + 13, // 44: mcp.v1.McpAgentService.UndeployService:output_type -> mcp.v1.UndeployServiceResponse + 7, // 45: mcp.v1.McpAgentService.StopService:output_type -> mcp.v1.StopServiceResponse + 9, // 46: mcp.v1.McpAgentService.StartService:output_type -> mcp.v1.StartServiceResponse + 11, // 47: mcp.v1.McpAgentService.RestartService:output_type -> mcp.v1.RestartServiceResponse + 15, // 48: mcp.v1.McpAgentService.SyncDesiredState:output_type -> mcp.v1.SyncDesiredStateResponse + 20, // 49: mcp.v1.McpAgentService.ListServices:output_type -> mcp.v1.ListServicesResponse + 24, // 50: mcp.v1.McpAgentService.GetServiceStatus:output_type -> mcp.v1.GetServiceStatusResponse + 26, // 51: mcp.v1.McpAgentService.LiveCheck:output_type -> mcp.v1.LiveCheckResponse + 29, // 52: mcp.v1.McpAgentService.AdoptContainers:output_type -> mcp.v1.AdoptContainersResponse + 37, // 53: mcp.v1.McpAgentService.PurgeComponent:output_type -> mcp.v1.PurgeResponse + 31, // 54: mcp.v1.McpAgentService.PushFile:output_type -> mcp.v1.PushFileResponse + 33, // 55: mcp.v1.McpAgentService.PullFile:output_type -> mcp.v1.PullFileResponse + 35, // 56: mcp.v1.McpAgentService.NodeStatus:output_type -> mcp.v1.NodeStatusResponse + 44, // 57: mcp.v1.McpAgentService.ListDNSRecords:output_type -> mcp.v1.ListDNSRecordsResponse + 48, // 58: mcp.v1.McpAgentService.ListProxyRoutes:output_type -> mcp.v1.ListProxyRoutesResponse + 40, // 59: mcp.v1.McpAgentService.Logs:output_type -> mcp.v1.LogsResponse + 43, // [43:60] is the sub-list for method output_type + 26, // [26:43] 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 } func init() { file_proto_mcp_v1_mcp_proto_init() } @@ -2665,7 +3163,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: 41, + NumMessages: 49, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/mcp/v1/mcp_grpc.pb.go b/gen/mcp/v1/mcp_grpc.pb.go index 2f3c94d..559c3c5 100644 --- a/gen/mcp/v1/mcp_grpc.pb.go +++ b/gen/mcp/v1/mcp_grpc.pb.go @@ -33,6 +33,8 @@ const ( McpAgentService_PushFile_FullMethodName = "/mcp.v1.McpAgentService/PushFile" McpAgentService_PullFile_FullMethodName = "/mcp.v1.McpAgentService/PullFile" McpAgentService_NodeStatus_FullMethodName = "/mcp.v1.McpAgentService/NodeStatus" + McpAgentService_ListDNSRecords_FullMethodName = "/mcp.v1.McpAgentService/ListDNSRecords" + McpAgentService_ListProxyRoutes_FullMethodName = "/mcp.v1.McpAgentService/ListProxyRoutes" McpAgentService_Logs_FullMethodName = "/mcp.v1.McpAgentService/Logs" ) @@ -61,6 +63,10 @@ type McpAgentServiceClient interface { PullFile(ctx context.Context, in *PullFileRequest, opts ...grpc.CallOption) (*PullFileResponse, error) // Node NodeStatus(ctx context.Context, in *NodeStatusRequest, opts ...grpc.CallOption) (*NodeStatusResponse, error) + // DNS (query MCNS) + ListDNSRecords(ctx context.Context, in *ListDNSRecordsRequest, opts ...grpc.CallOption) (*ListDNSRecordsResponse, error) + // Proxy routes (query mc-proxy) + ListProxyRoutes(ctx context.Context, in *ListProxyRoutesRequest, opts ...grpc.CallOption) (*ListProxyRoutesResponse, error) // Logs Logs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogsResponse], error) } @@ -213,6 +219,26 @@ func (c *mcpAgentServiceClient) NodeStatus(ctx context.Context, in *NodeStatusRe return out, nil } +func (c *mcpAgentServiceClient) ListDNSRecords(ctx context.Context, in *ListDNSRecordsRequest, opts ...grpc.CallOption) (*ListDNSRecordsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListDNSRecordsResponse) + err := c.cc.Invoke(ctx, McpAgentService_ListDNSRecords_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *mcpAgentServiceClient) ListProxyRoutes(ctx context.Context, in *ListProxyRoutesRequest, opts ...grpc.CallOption) (*ListProxyRoutesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListProxyRoutesResponse) + err := c.cc.Invoke(ctx, McpAgentService_ListProxyRoutes_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...) @@ -257,6 +283,10 @@ type McpAgentServiceServer interface { PullFile(context.Context, *PullFileRequest) (*PullFileResponse, error) // Node NodeStatus(context.Context, *NodeStatusRequest) (*NodeStatusResponse, error) + // DNS (query MCNS) + ListDNSRecords(context.Context, *ListDNSRecordsRequest) (*ListDNSRecordsResponse, error) + // Proxy routes (query mc-proxy) + ListProxyRoutes(context.Context, *ListProxyRoutesRequest) (*ListProxyRoutesResponse, error) // Logs Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error mustEmbedUnimplementedMcpAgentServiceServer() @@ -311,6 +341,12 @@ func (UnimplementedMcpAgentServiceServer) PullFile(context.Context, *PullFileReq func (UnimplementedMcpAgentServiceServer) NodeStatus(context.Context, *NodeStatusRequest) (*NodeStatusResponse, error) { return nil, status.Error(codes.Unimplemented, "method NodeStatus not implemented") } +func (UnimplementedMcpAgentServiceServer) ListDNSRecords(context.Context, *ListDNSRecordsRequest) (*ListDNSRecordsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListDNSRecords not implemented") +} +func (UnimplementedMcpAgentServiceServer) ListProxyRoutes(context.Context, *ListProxyRoutesRequest) (*ListProxyRoutesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListProxyRoutes not implemented") +} func (UnimplementedMcpAgentServiceServer) Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error { return status.Error(codes.Unimplemented, "method Logs not implemented") } @@ -587,6 +623,42 @@ func _McpAgentService_NodeStatus_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _McpAgentService_ListDNSRecords_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListDNSRecordsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(McpAgentServiceServer).ListDNSRecords(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: McpAgentService_ListDNSRecords_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(McpAgentServiceServer).ListDNSRecords(ctx, req.(*ListDNSRecordsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _McpAgentService_ListProxyRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListProxyRoutesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(McpAgentServiceServer).ListProxyRoutes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: McpAgentService_ListProxyRoutes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(McpAgentServiceServer).ListProxyRoutes(ctx, req.(*ListProxyRoutesRequest)) + } + 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 { @@ -661,6 +733,14 @@ var McpAgentService_ServiceDesc = grpc.ServiceDesc{ MethodName: "NodeStatus", Handler: _McpAgentService_NodeStatus_Handler, }, + { + MethodName: "ListDNSRecords", + Handler: _McpAgentService_ListDNSRecords_Handler, + }, + { + MethodName: "ListProxyRoutes", + Handler: _McpAgentService_ListProxyRoutes_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/internal/agent/dns.go b/internal/agent/dns.go index 2f3fb6c..f7d2a21 100644 --- a/internal/agent/dns.go +++ b/internal/agent/dns.go @@ -26,8 +26,8 @@ type DNSRegistrar struct { logger *slog.Logger } -// dnsRecord is the JSON representation of an MCNS record. -type dnsRecord struct { +// DNSRecord is the JSON representation of an MCNS record. +type DNSRecord struct { ID int `json:"ID"` Name string `json:"Name"` Type string `json:"Type"` @@ -136,8 +136,87 @@ func (d *DNSRegistrar) RemoveRecord(ctx context.Context, serviceName string) err return nil } +// DNSZone is the JSON representation of an MCNS zone. +type DNSZone struct { + Name string `json:"Name"` +} + +// ListZones returns all zones from MCNS. +func (d *DNSRegistrar) ListZones(ctx context.Context) ([]DNSZone, error) { + if d == nil { + return nil, fmt.Errorf("DNS registrar not configured") + } + + url := fmt.Sprintf("%s/v1/zones", d.serverURL) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("create list zones request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+d.token) + + resp, err := d.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("list zones: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read list zones response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("list zones: mcns returned %d: %s", resp.StatusCode, string(body)) + } + + var envelope struct { + Zones []DNSZone `json:"zones"` + } + if err := json.Unmarshal(body, &envelope); err != nil { + return nil, fmt.Errorf("parse list zones response: %w", err) + } + return envelope.Zones, nil +} + +// ListZoneRecords returns all records in the given zone (no filters). +func (d *DNSRegistrar) ListZoneRecords(ctx context.Context, zone string) ([]DNSRecord, error) { + if d == nil { + return nil, fmt.Errorf("DNS registrar not configured") + } + + url := fmt.Sprintf("%s/v1/zones/%s/records", d.serverURL, zone) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("create list zone records request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+d.token) + + resp, err := d.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("list zone records: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read list zone records response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("list zone records: mcns returned %d: %s", resp.StatusCode, string(body)) + } + + var envelope struct { + Records []DNSRecord `json:"records"` + } + if err := json.Unmarshal(body, &envelope); err != nil { + return nil, fmt.Errorf("parse list zone records response: %w", err) + } + return envelope.Records, nil +} + // listRecords returns A records matching the service name in the zone. -func (d *DNSRegistrar) listRecords(ctx context.Context, serviceName string) ([]dnsRecord, error) { +func (d *DNSRegistrar) listRecords(ctx context.Context, serviceName string) ([]DNSRecord, error) { url := fmt.Sprintf("%s/v1/zones/%s/records?name=%s&type=A", d.serverURL, d.zone, serviceName) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { @@ -161,7 +240,7 @@ func (d *DNSRegistrar) listRecords(ctx context.Context, serviceName string) ([]d } var envelope struct { - Records []dnsRecord `json:"records"` + Records []DNSRecord `json:"records"` } if err := json.Unmarshal(body, &envelope); err != nil { return nil, fmt.Errorf("parse list response: %w", err) diff --git a/internal/agent/dns_rpc.go b/internal/agent/dns_rpc.go new file mode 100644 index 0000000..754b4d3 --- /dev/null +++ b/internal/agent/dns_rpc.go @@ -0,0 +1,40 @@ +package agent + +import ( + "context" + "fmt" + + mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1" +) + +// ListDNSRecords queries MCNS for all zones and their records. +func (a *Agent) ListDNSRecords(ctx context.Context, _ *mcpv1.ListDNSRecordsRequest) (*mcpv1.ListDNSRecordsResponse, error) { + a.Logger.Debug("ListDNSRecords called") + + zones, err := a.DNS.ListZones(ctx) + if err != nil { + return nil, fmt.Errorf("list zones: %w", err) + } + + resp := &mcpv1.ListDNSRecordsResponse{} + for _, z := range zones { + records, err := a.DNS.ListZoneRecords(ctx, z.Name) + if err != nil { + return nil, fmt.Errorf("list records for zone %q: %w", z.Name, err) + } + + zone := &mcpv1.DNSZone{Name: z.Name} + for _, r := range records { + zone.Records = append(zone.Records, &mcpv1.DNSRecord{ + Id: int64(r.ID), + Name: r.Name, + Type: r.Type, + Value: r.Value, + Ttl: int32(r.TTL), //nolint:gosec // TTL is bounded + }) + } + resp.Zones = append(resp.Zones, zone) + } + + return resp, nil +} diff --git a/internal/agent/dns_test.go b/internal/agent/dns_test.go index 5b57df0..b6bd5da 100644 --- a/internal/agent/dns_test.go +++ b/internal/agent/dns_test.go @@ -90,7 +90,7 @@ func TestEnsureRecordSkipsWhenExists(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { // Return an existing record with the correct value. - resp := map[string][]dnsRecord{"records": {{ID: 1, Name: "myservice", Type: "A", Value: "192.168.88.181", TTL: 300}}} + resp := map[string][]DNSRecord{"records": {{ID: 1, Name: "myservice", Type: "A", Value: "192.168.88.181", TTL: 300}}} w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(resp) return @@ -124,7 +124,7 @@ func TestEnsureRecordUpdatesWrongValue(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { // Return a record with a stale value. - resp := map[string][]dnsRecord{"records": {{ID: 42, Name: "myservice", Type: "A", Value: "10.0.0.1", TTL: 300}}} + resp := map[string][]DNSRecord{"records": {{ID: 42, Name: "myservice", Type: "A", Value: "10.0.0.1", TTL: 300}}} w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(resp) return @@ -160,7 +160,7 @@ func TestRemoveRecordDeletes(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { - resp := map[string][]dnsRecord{"records": {{ID: 7, Name: "myservice", Type: "A", Value: "192.168.88.181", TTL: 300}}} + resp := map[string][]DNSRecord{"records": {{ID: 7, Name: "myservice", Type: "A", Value: "192.168.88.181", TTL: 300}}} w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(resp) return diff --git a/internal/agent/proxy.go b/internal/agent/proxy.go index fb08de0..ff89cd3 100644 --- a/internal/agent/proxy.go +++ b/internal/agent/proxy.go @@ -48,6 +48,14 @@ func (p *ProxyRouter) Close() error { return p.client.Close() } +// GetStatus returns the mc-proxy server status. +func (p *ProxyRouter) GetStatus(ctx context.Context) (*mcproxy.Status, error) { + if p == nil { + return nil, fmt.Errorf("mc-proxy not configured") + } + return p.client.GetStatus(ctx) +} + // RegisterRoutes registers all routes for a service component with mc-proxy. // It uses the assigned host ports from the registry. func (p *ProxyRouter) RegisterRoutes(ctx context.Context, serviceName string, routes []registry.Route, hostPorts map[string]int) error { diff --git a/internal/agent/proxy_rpc.go b/internal/agent/proxy_rpc.go new file mode 100644 index 0000000..bcd6135 --- /dev/null +++ b/internal/agent/proxy_rpc.go @@ -0,0 +1,46 @@ +package agent + +import ( + "context" + "fmt" + + mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// ListProxyRoutes queries mc-proxy for its current status and routes. +func (a *Agent) ListProxyRoutes(ctx context.Context, _ *mcpv1.ListProxyRoutesRequest) (*mcpv1.ListProxyRoutesResponse, error) { + a.Logger.Debug("ListProxyRoutes called") + + status, err := a.Proxy.GetStatus(ctx) + if err != nil { + return nil, fmt.Errorf("get mc-proxy status: %w", err) + } + + resp := &mcpv1.ListProxyRoutesResponse{ + Version: status.Version, + TotalConnections: status.TotalConnections, + } + if !status.StartedAt.IsZero() { + resp.StartedAt = timestamppb.New(status.StartedAt) + } + + for _, ls := range status.Listeners { + listener := &mcpv1.ProxyListenerInfo{ + Addr: ls.Addr, + RouteCount: int32(ls.RouteCount), //nolint:gosec // bounded + ActiveConnections: ls.ActiveConnections, + } + for _, r := range ls.Routes { + listener.Routes = append(listener.Routes, &mcpv1.ProxyRouteInfo{ + Hostname: r.Hostname, + Backend: r.Backend, + Mode: r.Mode, + BackendTls: r.BackendTLS, + }) + } + resp.Listeners = append(resp.Listeners, listener) + } + + return resp, nil +} diff --git a/proto/mcp/v1/mcp.proto b/proto/mcp/v1/mcp.proto index 2f073ae..09d83ef 100644 --- a/proto/mcp/v1/mcp.proto +++ b/proto/mcp/v1/mcp.proto @@ -34,6 +34,12 @@ service McpAgentService { // Node rpc NodeStatus(NodeStatusRequest) returns (NodeStatusResponse); + // DNS (query MCNS) + rpc ListDNSRecords(ListDNSRecordsRequest) returns (ListDNSRecordsResponse); + + // Proxy routes (query mc-proxy) + rpc ListProxyRoutes(ListProxyRoutesRequest) returns (ListProxyRoutesResponse); + // Logs rpc Logs(LogsRequest) returns (stream LogsResponse); } @@ -301,3 +307,49 @@ message LogsRequest { message LogsResponse { bytes data = 1; } + +// --- DNS --- + +message ListDNSRecordsRequest {} + +message DNSZone { + string name = 1; + repeated DNSRecord records = 2; +} + +message DNSRecord { + int64 id = 1; + string name = 2; + string type = 3; + string value = 4; + int32 ttl = 5; +} + +message ListDNSRecordsResponse { + repeated DNSZone zones = 1; +} + +// --- Proxy routes --- + +message ListProxyRoutesRequest {} + +message ProxyRouteInfo { + string hostname = 1; + string backend = 2; + string mode = 3; + bool backend_tls = 4; +} + +message ProxyListenerInfo { + string addr = 1; + int32 route_count = 2; + int64 active_connections = 3; + repeated ProxyRouteInfo routes = 4; +} + +message ListProxyRoutesResponse { + string version = 1; + int64 total_connections = 2; + google.protobuf.Timestamp started_at = 3; + repeated ProxyListenerInfo listeners = 4; +}