diff --git a/cmd/mcp/edge.go b/cmd/mcp/edge.go new file mode 100644 index 0000000..d3a7aac --- /dev/null +++ b/cmd/mcp/edge.go @@ -0,0 +1,180 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/spf13/cobra" + + mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1" + "git.wntrmute.dev/mc/mcp/internal/config" +) + +func edgeCmd() *cobra.Command { + var nodeName string + + cmd := &cobra.Command{ + Use: "edge", + Short: "Manage edge routes (scaffolding — will be replaced by master)", + } + + list := &cobra.Command{ + Use: "list", + Short: "List edge routes on a node", + RunE: func(_ *cobra.Command, _ []string) error { + if nodeName == "" { + return fmt.Errorf("--node is required") + } + return runEdgeList(nodeName) + }, + } + + var ( + backendHostname string + backendPort int + ) + + setup := &cobra.Command{ + Use: "setup ", + Short: "Set up an edge route (provisions cert, registers mc-proxy route)", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + if nodeName == "" { + return fmt.Errorf("--node is required") + } + if backendHostname == "" { + return fmt.Errorf("--backend-hostname is required") + } + if backendPort == 0 { + return fmt.Errorf("--backend-port is required") + } + return runEdgeSetup(nodeName, args[0], backendHostname, backendPort) + }, + } + setup.Flags().StringVar(&backendHostname, "backend-hostname", "", "internal .svc.mcp hostname") + setup.Flags().IntVar(&backendPort, "backend-port", 0, "port on worker's mc-proxy") + + remove := &cobra.Command{ + Use: "remove ", + Short: "Remove an edge route", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + if nodeName == "" { + return fmt.Errorf("--node is required") + } + return runEdgeRemove(nodeName, args[0]) + }, + } + + cmd.PersistentFlags().StringVarP(&nodeName, "node", "n", "", "target node (required)") + cmd.AddCommand(list, setup, remove) + return cmd +} + +func runEdgeList(nodeName string) error { + 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.ListEdgeRoutes(ctx, &mcpv1.ListEdgeRoutesRequest{}) + if err != nil { + return fmt.Errorf("list edge routes: %w", err) + } + + if len(resp.GetRoutes()) == 0 { + fmt.Printf("No edge routes on %s\n", nodeName) + return nil + } + + fmt.Printf("Edge routes on %s:\n", nodeName) + for _, r := range resp.GetRoutes() { + expires := r.GetCertExpires() + if expires == "" { + expires = "unknown" + } + fmt.Printf(" %s → %s:%d cert_expires=%s\n", + r.GetHostname(), r.GetBackendHostname(), r.GetBackendPort(), expires) + } + return nil +} + +func runEdgeSetup(nodeName, hostname, backendHostname string, backendPort int) error { + 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(), 30*time.Second) + defer cancel() + + _, err = client.SetupEdgeRoute(ctx, &mcpv1.SetupEdgeRouteRequest{ + Hostname: hostname, + BackendHostname: backendHostname, + BackendPort: int32(backendPort), //nolint:gosec // port is a small positive integer + BackendTls: true, + }) + if err != nil { + return fmt.Errorf("setup edge route: %w", err) + } + + fmt.Printf("edge route established: %s → %s:%d on %s\n", hostname, backendHostname, backendPort, nodeName) + return nil +} + +func runEdgeRemove(nodeName, hostname string) error { + 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(), 10*time.Second) + defer cancel() + + _, err = client.RemoveEdgeRoute(ctx, &mcpv1.RemoveEdgeRouteRequest{ + Hostname: hostname, + }) + if err != nil { + return fmt.Errorf("remove edge route: %w", err) + } + + fmt.Printf("edge route removed: %s on %s\n", hostname, nodeName) + return nil +} diff --git a/cmd/mcp/main.go b/cmd/mcp/main.go index 0937a77..48f0cc0 100644 --- a/cmd/mcp/main.go +++ b/cmd/mcp/main.go @@ -54,6 +54,7 @@ func main() { root.AddCommand(editCmd()) root.AddCommand(dnsCmd()) root.AddCommand(routeCmd()) + root.AddCommand(edgeCmd()) if err := root.Execute(); err != nil { log.Fatal(err)