From 95f86157b471fdce6cf5864b70523022e9f3c58f Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sun, 29 Mar 2026 19:10:11 -0700 Subject: [PATCH] Add mcp route command for managing mc-proxy routes New top-level command with list, add, remove subcommands. Supports -n/--node to target a specific node. Adds AddProxyRoute and RemoveProxyRoute RPCs to the agent. Moves route listing from mcp node routes to mcp route list. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/mcp/main.go | 1 + cmd/mcp/node.go | 54 +------ cmd/mcp/route.go | 212 +++++++++++++++++++++++++++ gen/mcp/v1/mcp.pb.go | 280 ++++++++++++++++++++++++++++++++---- gen/mcp/v1/mcp_grpc.pb.go | 76 ++++++++++ internal/agent/proxy.go | 16 +++ internal/agent/proxy_rpc.go | 65 +++++++++ proto/mcp/v1/mcp.proto | 19 +++ 8 files changed, 641 insertions(+), 82 deletions(-) create mode 100644 cmd/mcp/route.go diff --git a/cmd/mcp/main.go b/cmd/mcp/main.go index fee1783..0937a77 100644 --- a/cmd/mcp/main.go +++ b/cmd/mcp/main.go @@ -53,6 +53,7 @@ func main() { root.AddCommand(logsCmd()) root.AddCommand(editCmd()) root.AddCommand(dnsCmd()) + root.AddCommand(routeCmd()) if err := root.Execute(); err != nil { log.Fatal(err) diff --git a/cmd/mcp/node.go b/cmd/mcp/node.go index 5e530f0..25113d3 100644 --- a/cmd/mcp/node.go +++ b/cmd/mcp/node.go @@ -40,62 +40,10 @@ func nodeCmd() *cobra.Command { RunE: runNodeRemove, } - routes := &cobra.Command{ - Use: "routes", - Short: "List mc-proxy routes on all nodes", - RunE: runNodeRoutes, - } - - cmd.AddCommand(list, add, remove, routes) + cmd.AddCommand(list, add, remove) 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/cmd/mcp/route.go b/cmd/mcp/route.go new file mode 100644 index 0000000..c1876af --- /dev/null +++ b/cmd/mcp/route.go @@ -0,0 +1,212 @@ +package main + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/spf13/cobra" + + mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1" + "git.wntrmute.dev/mc/mcp/internal/config" +) + +func routeCmd() *cobra.Command { + var nodeName string + + cmd := &cobra.Command{ + Use: "route", + Short: "Manage mc-proxy routes", + } + + list := &cobra.Command{ + Use: "list", + Short: "List mc-proxy routes", + RunE: func(_ *cobra.Command, _ []string) error { + return runRouteList(nodeName) + }, + } + + add := &cobra.Command{ + Use: "add ", + Short: "Add a route to mc-proxy", + Long: "Add a route. Example: mcp route add -n rift :443 mcq.metacircular.net 100.95.252.120:443", + Args: cobra.ExactArgs(3), + RunE: func(_ *cobra.Command, args []string) error { + return runRouteAdd(nodeName, args) + }, + } + add.Flags().String("mode", "l4", "route mode (l4 or l7)") + add.Flags().Bool("backend-tls", false, "re-encrypt traffic to backend") + + remove := &cobra.Command{ + Use: "remove ", + Short: "Remove a route from mc-proxy", + Long: "Remove a route. Example: mcp route remove -n rift :443 mcq.metacircular.net", + Args: cobra.ExactArgs(2), + RunE: func(_ *cobra.Command, args []string) error { + return runRouteRemove(nodeName, args) + }, + } + + cmd.PersistentFlags().StringVarP(&nodeName, "node", "n", "", "target node (required)") + + cmd.AddCommand(list, add, remove) + return cmd +} + +func runRouteList(nodeName string) error { + if nodeName == "" { + return runRouteListAll() + } + + cfg, err := config.LoadCLIConfig(cfgPath) + if err != nil { + return fmt.Errorf("load config: %w", err) + } + + address, err := findNodeAddress(cfg, nodeName) + if err != nil { + return err + } + + client, conn, err := dialAgent(address, cfg) + if err != nil { + return fmt.Errorf("dial agent: %w", err) + } + defer func() { _ = conn.Close() }() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + resp, err := client.ListProxyRoutes(ctx, &mcpv1.ListProxyRoutesRequest{}) + if err != nil { + return fmt.Errorf("list routes: %w", err) + } + + printRoutes(nodeName, resp) + return nil +} + +func runRouteListAll() 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 + + printRoutes(node.Name, resp) + return nil + }) +} + +func printRoutes(nodeName string, resp *mcpv1.ListProxyRoutesResponse) { + fmt.Printf("NODE: %s\n", nodeName) + 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) + } + } +} + +func runRouteAdd(nodeName string, args []string) error { + if nodeName == "" { + return fmt.Errorf("--node is required") + } + + cfg, err := config.LoadCLIConfig(cfgPath) + if err != nil { + return fmt.Errorf("load config: %w", err) + } + + address, err := findNodeAddress(cfg, nodeName) + if err != nil { + return err + } + + client, conn, err := dialAgent(address, cfg) + if err != nil { + return fmt.Errorf("dial agent: %w", err) + } + defer func() { _ = conn.Close() }() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err = client.AddProxyRoute(ctx, &mcpv1.AddProxyRouteRequest{ + ListenerAddr: args[0], + Hostname: args[1], + Backend: args[2], + }) + if err != nil { + return fmt.Errorf("add route: %w", err) + } + + fmt.Printf("Added route: %s → %s on %s (%s)\n", args[1], args[2], args[0], nodeName) + return nil +} + +func runRouteRemove(nodeName string, args []string) error { + if nodeName == "" { + return fmt.Errorf("--node is required") + } + + cfg, err := config.LoadCLIConfig(cfgPath) + if err != nil { + return fmt.Errorf("load config: %w", err) + } + + address, err := findNodeAddress(cfg, nodeName) + if err != nil { + return err + } + + client, conn, err := dialAgent(address, cfg) + if err != nil { + return fmt.Errorf("dial agent: %w", err) + } + defer func() { _ = conn.Close() }() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err = client.RemoveProxyRoute(ctx, &mcpv1.RemoveProxyRouteRequest{ + ListenerAddr: args[0], + Hostname: args[1], + }) + if err != nil { + return fmt.Errorf("remove route: %w", err) + } + + fmt.Printf("Removed route: %s from %s (%s)\n", args[1], args[0], nodeName) + return nil +} diff --git a/gen/mcp/v1/mcp.pb.go b/gen/mcp/v1/mcp.pb.go index 6d0b89c..77ba05e 100644 --- a/gen/mcp/v1/mcp.pb.go +++ b/gen/mcp/v1/mcp.pb.go @@ -2808,6 +2808,206 @@ func (x *ListProxyRoutesResponse) GetListeners() []*ProxyListenerInfo { return nil } +type AddProxyRouteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ListenerAddr string `protobuf:"bytes,1,opt,name=listener_addr,json=listenerAddr,proto3" json:"listener_addr,omitempty"` // e.g. ":443" + Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` + Backend string `protobuf:"bytes,3,opt,name=backend,proto3" json:"backend,omitempty"` + Mode string `protobuf:"bytes,4,opt,name=mode,proto3" json:"mode,omitempty"` // "l4" or "l7" + BackendTls bool `protobuf:"varint,5,opt,name=backend_tls,json=backendTls,proto3" json:"backend_tls,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddProxyRouteRequest) Reset() { + *x = AddProxyRouteRequest{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddProxyRouteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddProxyRouteRequest) ProtoMessage() {} + +func (x *AddProxyRouteRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[49] + 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 AddProxyRouteRequest.ProtoReflect.Descriptor instead. +func (*AddProxyRouteRequest) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{49} +} + +func (x *AddProxyRouteRequest) GetListenerAddr() string { + if x != nil { + return x.ListenerAddr + } + return "" +} + +func (x *AddProxyRouteRequest) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *AddProxyRouteRequest) GetBackend() string { + if x != nil { + return x.Backend + } + return "" +} + +func (x *AddProxyRouteRequest) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *AddProxyRouteRequest) GetBackendTls() bool { + if x != nil { + return x.BackendTls + } + return false +} + +type AddProxyRouteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddProxyRouteResponse) Reset() { + *x = AddProxyRouteResponse{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddProxyRouteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddProxyRouteResponse) ProtoMessage() {} + +func (x *AddProxyRouteResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[50] + 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 AddProxyRouteResponse.ProtoReflect.Descriptor instead. +func (*AddProxyRouteResponse) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{50} +} + +type RemoveProxyRouteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ListenerAddr string `protobuf:"bytes,1,opt,name=listener_addr,json=listenerAddr,proto3" json:"listener_addr,omitempty"` // e.g. ":443" + Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveProxyRouteRequest) Reset() { + *x = RemoveProxyRouteRequest{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveProxyRouteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveProxyRouteRequest) ProtoMessage() {} + +func (x *RemoveProxyRouteRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[51] + 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 RemoveProxyRouteRequest.ProtoReflect.Descriptor instead. +func (*RemoveProxyRouteRequest) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{51} +} + +func (x *RemoveProxyRouteRequest) GetListenerAddr() string { + if x != nil { + return x.ListenerAddr + } + return "" +} + +func (x *RemoveProxyRouteRequest) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +type RemoveProxyRouteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveProxyRouteResponse) Reset() { + *x = RemoveProxyRouteResponse{} + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveProxyRouteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveProxyRouteResponse) ProtoMessage() {} + +func (x *RemoveProxyRouteResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcp_v1_mcp_proto_msgTypes[52] + 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 RemoveProxyRouteResponse.ProtoReflect.Descriptor instead. +func (*RemoveProxyRouteResponse) Descriptor() ([]byte, []int) { + return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{52} +} + var File_proto_mcp_v1_mcp_proto protoreflect.FileDescriptor const file_proto_mcp_v1_mcp_proto_rawDesc = "" + @@ -2998,7 +3198,19 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" + "\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" + + "\tlisteners\x18\x04 \x03(\v2\x19.mcp.v1.ProxyListenerInfoR\tlisteners\"\xa6\x01\n" + + "\x14AddProxyRouteRequest\x12#\n" + + "\rlistener_addr\x18\x01 \x01(\tR\flistenerAddr\x12\x1a\n" + + "\bhostname\x18\x02 \x01(\tR\bhostname\x12\x18\n" + + "\abackend\x18\x03 \x01(\tR\abackend\x12\x12\n" + + "\x04mode\x18\x04 \x01(\tR\x04mode\x12\x1f\n" + + "\vbackend_tls\x18\x05 \x01(\bR\n" + + "backendTls\"\x17\n" + + "\x15AddProxyRouteResponse\"Z\n" + + "\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" + "\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" + @@ -3016,7 +3228,9 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" + "\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" + + "\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" + "\x04Logs\x12\x13.mcp.v1.LogsRequest\x1a\x14.mcp.v1.LogsResponse0\x01B*Z(git.wntrmute.dev/mc/mcp/gen/mcp/v1;mcpv1b\x06proto3" var ( @@ -3031,7 +3245,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, 49) +var file_proto_mcp_v1_mcp_proto_msgTypes = make([]protoimpl.MessageInfo, 53) var file_proto_mcp_v1_mcp_proto_goTypes = []any{ (*RouteSpec)(nil), // 0: mcp.v1.RouteSpec (*ComponentSpec)(nil), // 1: mcp.v1.ComponentSpec @@ -3082,7 +3296,11 @@ var file_proto_mcp_v1_mcp_proto_goTypes = []any{ (*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 + (*AddProxyRouteRequest)(nil), // 49: mcp.v1.AddProxyRouteRequest + (*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 } var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{ 0, // 0: mcp.v1.ComponentSpec.routes:type_name -> mcp.v1.RouteSpec @@ -3096,20 +3314,20 @@ 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 - 49, // 11: mcp.v1.ComponentInfo.started:type_name -> google.protobuf.Timestamp + 53, // 11: mcp.v1.ComponentInfo.started:type_name -> google.protobuf.Timestamp 18, // 12: mcp.v1.ListServicesResponse.services:type_name -> mcp.v1.ServiceInfo - 49, // 13: mcp.v1.EventInfo.timestamp:type_name -> google.protobuf.Timestamp + 53, // 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 - 49, // 19: mcp.v1.NodeStatusResponse.uptime_since:type_name -> google.protobuf.Timestamp + 53, // 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 - 49, // 24: mcp.v1.ListProxyRoutesResponse.started_at:type_name -> google.protobuf.Timestamp + 53, // 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 @@ -3127,26 +3345,30 @@ var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{ 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 + 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 @@ -3163,7 +3385,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: 49, + NumMessages: 53, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/mcp/v1/mcp_grpc.pb.go b/gen/mcp/v1/mcp_grpc.pb.go index 559c3c5..5c5fddc 100644 --- a/gen/mcp/v1/mcp_grpc.pb.go +++ b/gen/mcp/v1/mcp_grpc.pb.go @@ -35,6 +35,8 @@ const ( McpAgentService_NodeStatus_FullMethodName = "/mcp.v1.McpAgentService/NodeStatus" McpAgentService_ListDNSRecords_FullMethodName = "/mcp.v1.McpAgentService/ListDNSRecords" McpAgentService_ListProxyRoutes_FullMethodName = "/mcp.v1.McpAgentService/ListProxyRoutes" + McpAgentService_AddProxyRoute_FullMethodName = "/mcp.v1.McpAgentService/AddProxyRoute" + McpAgentService_RemoveProxyRoute_FullMethodName = "/mcp.v1.McpAgentService/RemoveProxyRoute" McpAgentService_Logs_FullMethodName = "/mcp.v1.McpAgentService/Logs" ) @@ -67,6 +69,8 @@ type McpAgentServiceClient interface { 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) + AddProxyRoute(ctx context.Context, in *AddProxyRouteRequest, opts ...grpc.CallOption) (*AddProxyRouteResponse, error) + RemoveProxyRoute(ctx context.Context, in *RemoveProxyRouteRequest, opts ...grpc.CallOption) (*RemoveProxyRouteResponse, error) // Logs Logs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogsResponse], error) } @@ -239,6 +243,26 @@ func (c *mcpAgentServiceClient) ListProxyRoutes(ctx context.Context, in *ListPro return out, nil } +func (c *mcpAgentServiceClient) AddProxyRoute(ctx context.Context, in *AddProxyRouteRequest, opts ...grpc.CallOption) (*AddProxyRouteResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AddProxyRouteResponse) + err := c.cc.Invoke(ctx, McpAgentService_AddProxyRoute_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *mcpAgentServiceClient) RemoveProxyRoute(ctx context.Context, in *RemoveProxyRouteRequest, opts ...grpc.CallOption) (*RemoveProxyRouteResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RemoveProxyRouteResponse) + err := c.cc.Invoke(ctx, McpAgentService_RemoveProxyRoute_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...) @@ -287,6 +311,8 @@ type McpAgentServiceServer interface { ListDNSRecords(context.Context, *ListDNSRecordsRequest) (*ListDNSRecordsResponse, error) // Proxy routes (query mc-proxy) ListProxyRoutes(context.Context, *ListProxyRoutesRequest) (*ListProxyRoutesResponse, error) + AddProxyRoute(context.Context, *AddProxyRouteRequest) (*AddProxyRouteResponse, error) + RemoveProxyRoute(context.Context, *RemoveProxyRouteRequest) (*RemoveProxyRouteResponse, error) // Logs Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error mustEmbedUnimplementedMcpAgentServiceServer() @@ -347,6 +373,12 @@ func (UnimplementedMcpAgentServiceServer) ListDNSRecords(context.Context, *ListD func (UnimplementedMcpAgentServiceServer) ListProxyRoutes(context.Context, *ListProxyRoutesRequest) (*ListProxyRoutesResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListProxyRoutes not implemented") } +func (UnimplementedMcpAgentServiceServer) AddProxyRoute(context.Context, *AddProxyRouteRequest) (*AddProxyRouteResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AddProxyRoute not implemented") +} +func (UnimplementedMcpAgentServiceServer) RemoveProxyRoute(context.Context, *RemoveProxyRouteRequest) (*RemoveProxyRouteResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveProxyRoute not implemented") +} func (UnimplementedMcpAgentServiceServer) Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error { return status.Error(codes.Unimplemented, "method Logs not implemented") } @@ -659,6 +691,42 @@ func _McpAgentService_ListProxyRoutes_Handler(srv interface{}, ctx context.Conte return interceptor(ctx, in, info, handler) } +func _McpAgentService_AddProxyRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddProxyRouteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(McpAgentServiceServer).AddProxyRoute(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: McpAgentService_AddProxyRoute_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(McpAgentServiceServer).AddProxyRoute(ctx, req.(*AddProxyRouteRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _McpAgentService_RemoveProxyRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveProxyRouteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(McpAgentServiceServer).RemoveProxyRoute(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: McpAgentService_RemoveProxyRoute_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(McpAgentServiceServer).RemoveProxyRoute(ctx, req.(*RemoveProxyRouteRequest)) + } + 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 { @@ -741,6 +809,14 @@ var McpAgentService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListProxyRoutes", Handler: _McpAgentService_ListProxyRoutes_Handler, }, + { + MethodName: "AddProxyRoute", + Handler: _McpAgentService_AddProxyRoute_Handler, + }, + { + MethodName: "RemoveProxyRoute", + Handler: _McpAgentService_RemoveProxyRoute_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/internal/agent/proxy.go b/internal/agent/proxy.go index ff89cd3..b0ef2cd 100644 --- a/internal/agent/proxy.go +++ b/internal/agent/proxy.go @@ -56,6 +56,22 @@ func (p *ProxyRouter) GetStatus(ctx context.Context) (*mcproxy.Status, error) { return p.client.GetStatus(ctx) } +// AddRoute adds a single route to mc-proxy. +func (p *ProxyRouter) AddRoute(ctx context.Context, listenerAddr string, route mcproxy.Route) error { + if p == nil { + return fmt.Errorf("mc-proxy not configured") + } + return p.client.AddRoute(ctx, listenerAddr, route) +} + +// RemoveRoute removes a single route from mc-proxy. +func (p *ProxyRouter) RemoveRoute(ctx context.Context, listenerAddr, hostname string) error { + if p == nil { + return fmt.Errorf("mc-proxy not configured") + } + return p.client.RemoveRoute(ctx, listenerAddr, hostname) +} + // 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 index bcd6135..978cafa 100644 --- a/internal/agent/proxy_rpc.go +++ b/internal/agent/proxy_rpc.go @@ -4,7 +4,10 @@ import ( "context" "fmt" + "git.wntrmute.dev/mc/mc-proxy/client/mcproxy" mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -44,3 +47,65 @@ func (a *Agent) ListProxyRoutes(ctx context.Context, _ *mcpv1.ListProxyRoutesReq return resp, nil } + +// AddProxyRoute adds a route to mc-proxy. +func (a *Agent) AddProxyRoute(ctx context.Context, req *mcpv1.AddProxyRouteRequest) (*mcpv1.AddProxyRouteResponse, error) { + if req.GetListenerAddr() == "" { + return nil, status.Error(codes.InvalidArgument, "listener_addr is required") + } + if req.GetHostname() == "" { + return nil, status.Error(codes.InvalidArgument, "hostname is required") + } + if req.GetBackend() == "" { + return nil, status.Error(codes.InvalidArgument, "backend is required") + } + + if a.Proxy == nil { + return nil, status.Error(codes.FailedPrecondition, "mc-proxy not configured") + } + + route := mcproxy.Route{ + Hostname: req.GetHostname(), + Backend: req.GetBackend(), + Mode: req.GetMode(), + BackendTLS: req.GetBackendTls(), + } + + if err := a.Proxy.AddRoute(ctx, req.GetListenerAddr(), route); err != nil { + return nil, fmt.Errorf("add route: %w", err) + } + + a.Logger.Info("route added", + "listener", req.GetListenerAddr(), + "hostname", req.GetHostname(), + "backend", req.GetBackend(), + "mode", req.GetMode(), + ) + + return &mcpv1.AddProxyRouteResponse{}, nil +} + +// RemoveProxyRoute removes a route from mc-proxy. +func (a *Agent) RemoveProxyRoute(ctx context.Context, req *mcpv1.RemoveProxyRouteRequest) (*mcpv1.RemoveProxyRouteResponse, error) { + if req.GetListenerAddr() == "" { + return nil, status.Error(codes.InvalidArgument, "listener_addr is required") + } + 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") + } + + if err := a.Proxy.RemoveRoute(ctx, req.GetListenerAddr(), req.GetHostname()); err != nil { + return nil, fmt.Errorf("remove route: %w", err) + } + + a.Logger.Info("route removed", + "listener", req.GetListenerAddr(), + "hostname", req.GetHostname(), + ) + + return &mcpv1.RemoveProxyRouteResponse{}, nil +} diff --git a/proto/mcp/v1/mcp.proto b/proto/mcp/v1/mcp.proto index 09d83ef..f2790e1 100644 --- a/proto/mcp/v1/mcp.proto +++ b/proto/mcp/v1/mcp.proto @@ -39,6 +39,8 @@ service McpAgentService { // Proxy routes (query mc-proxy) rpc ListProxyRoutes(ListProxyRoutesRequest) returns (ListProxyRoutesResponse); + rpc AddProxyRoute(AddProxyRouteRequest) returns (AddProxyRouteResponse); + rpc RemoveProxyRoute(RemoveProxyRouteRequest) returns (RemoveProxyRouteResponse); // Logs rpc Logs(LogsRequest) returns (stream LogsResponse); @@ -353,3 +355,20 @@ message ListProxyRoutesResponse { google.protobuf.Timestamp started_at = 3; repeated ProxyListenerInfo listeners = 4; } + +message AddProxyRouteRequest { + string listener_addr = 1; // e.g. ":443" + string hostname = 2; + string backend = 3; + string mode = 4; // "l4" or "l7" + bool backend_tls = 5; +} + +message AddProxyRouteResponse {} + +message RemoveProxyRouteRequest { + string listener_addr = 1; // e.g. ":443" + string hostname = 2; +} + +message RemoveProxyRouteResponse {}