All import paths updated to git.wntrmute.dev/mc/. Bumps mcdsl to v1.2.0, mc-proxy to v1.1.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
238 lines
6.2 KiB
Go
238 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
|
"git.wntrmute.dev/mc/mcp/internal/config"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// newTable returns a tabwriter configured for CLI table output.
|
|
func newTable() *tabwriter.Writer {
|
|
return tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
|
}
|
|
|
|
// forEachNode loads the CLI config, then calls fn for every configured node.
|
|
// Dial errors are printed as warnings and the node is skipped. Connections
|
|
// are closed before moving to the next node, avoiding defer-in-loop leaks.
|
|
func forEachNode(fn func(node config.NodeConfig, client mcpv1.McpAgentServiceClient) error) error {
|
|
cfg, err := config.LoadCLIConfig(cfgPath)
|
|
if err != nil {
|
|
return fmt.Errorf("load config: %w", err)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
fnErr := fn(node, client)
|
|
_ = conn.Close()
|
|
if fnErr != nil {
|
|
return fnErr
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func listCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "list",
|
|
Short: "List services from all agents (registry, no runtime query)",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
w := newTable()
|
|
_, _ = fmt.Fprintln(w, "SERVICE\tCOMPONENT\tDESIRED\tOBSERVED\tVERSION")
|
|
|
|
if err := forEachNode(func(node config.NodeConfig, client mcpv1.McpAgentServiceClient) error {
|
|
resp, err := client.ListServices(context.Background(), &mcpv1.ListServicesRequest{})
|
|
if err != nil {
|
|
_, _ = fmt.Fprintf(os.Stderr, "warning: %s: list services: %v\n", node.Name, err)
|
|
return nil
|
|
}
|
|
|
|
for _, svc := range resp.GetServices() {
|
|
for _, comp := range svc.GetComponents() {
|
|
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
|
|
svc.GetName(),
|
|
comp.GetName(),
|
|
comp.GetDesiredState(),
|
|
comp.GetObservedState(),
|
|
comp.GetVersion(),
|
|
)
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return w.Flush()
|
|
},
|
|
}
|
|
}
|
|
|
|
func psCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "ps",
|
|
Short: "Live check: query runtime on all agents",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
w := newTable()
|
|
_, _ = fmt.Fprintln(w, "SERVICE\tCOMPONENT\tNODE\tSTATE\tVERSION\tUPTIME")
|
|
|
|
now := time.Now()
|
|
if err := forEachNode(func(node config.NodeConfig, client mcpv1.McpAgentServiceClient) error {
|
|
resp, err := client.LiveCheck(context.Background(), &mcpv1.LiveCheckRequest{})
|
|
if err != nil {
|
|
_, _ = fmt.Fprintf(os.Stderr, "warning: %s: live check: %v\n", node.Name, err)
|
|
return nil
|
|
}
|
|
|
|
for _, svc := range resp.GetServices() {
|
|
for _, comp := range svc.GetComponents() {
|
|
uptime := "-"
|
|
if comp.GetStarted() != nil {
|
|
d := now.Sub(comp.GetStarted().AsTime())
|
|
uptime = formatDuration(d)
|
|
}
|
|
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
|
|
svc.GetName(),
|
|
comp.GetName(),
|
|
node.Name,
|
|
comp.GetObservedState(),
|
|
comp.GetVersion(),
|
|
uptime,
|
|
)
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return w.Flush()
|
|
},
|
|
}
|
|
}
|
|
|
|
func statusCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "status [service]",
|
|
Short: "Full picture: live query + drift + recent events",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
var serviceName string
|
|
if len(args) > 0 {
|
|
serviceName = args[0]
|
|
}
|
|
|
|
var allServices []*mcpv1.ServiceInfo
|
|
var allDrift []*mcpv1.DriftInfo
|
|
var allEvents []*mcpv1.EventInfo
|
|
|
|
if err := forEachNode(func(node config.NodeConfig, client mcpv1.McpAgentServiceClient) error {
|
|
resp, err := client.GetServiceStatus(context.Background(), &mcpv1.GetServiceStatusRequest{
|
|
Name: serviceName,
|
|
})
|
|
if err != nil {
|
|
_, _ = fmt.Fprintf(os.Stderr, "warning: %s: get service status: %v\n", node.Name, err)
|
|
return nil
|
|
}
|
|
|
|
allServices = append(allServices, resp.GetServices()...)
|
|
allDrift = append(allDrift, resp.GetDrift()...)
|
|
allEvents = append(allEvents, resp.GetRecentEvents()...)
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Services table.
|
|
w := newTable()
|
|
_, _ = fmt.Fprintln(w, "SERVICE\tCOMPONENT\tDESIRED\tOBSERVED\tVERSION")
|
|
for _, svc := range allServices {
|
|
for _, comp := range svc.GetComponents() {
|
|
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
|
|
svc.GetName(),
|
|
comp.GetName(),
|
|
comp.GetDesiredState(),
|
|
comp.GetObservedState(),
|
|
comp.GetVersion(),
|
|
)
|
|
}
|
|
}
|
|
if err := w.Flush(); err != nil {
|
|
return fmt.Errorf("flush services table: %w", err)
|
|
}
|
|
|
|
// Drift section.
|
|
if len(allDrift) > 0 {
|
|
fmt.Println()
|
|
fmt.Println("DRIFT:")
|
|
dw := newTable()
|
|
_, _ = fmt.Fprintln(dw, "SERVICE\tCOMPONENT\tDESIRED\tOBSERVED")
|
|
for _, d := range allDrift {
|
|
_, _ = fmt.Fprintf(dw, "%s\t%s\t%s\t%s\n",
|
|
d.GetService(),
|
|
d.GetComponent(),
|
|
d.GetDesiredState(),
|
|
d.GetObservedState(),
|
|
)
|
|
}
|
|
if err := dw.Flush(); err != nil {
|
|
return fmt.Errorf("flush drift table: %w", err)
|
|
}
|
|
}
|
|
|
|
// Recent events section.
|
|
if len(allEvents) > 0 {
|
|
fmt.Println()
|
|
fmt.Println("RECENT EVENTS:")
|
|
ew := newTable()
|
|
_, _ = fmt.Fprintln(ew, "SERVICE\tCOMPONENT\tPREV\tNEW\tTIME")
|
|
for _, e := range allEvents {
|
|
ts := "-"
|
|
if e.GetTimestamp() != nil {
|
|
ts = e.GetTimestamp().AsTime().Format(time.RFC3339)
|
|
}
|
|
_, _ = fmt.Fprintf(ew, "%s\t%s\t%s\t%s\t%s\n",
|
|
e.GetService(),
|
|
e.GetComponent(),
|
|
e.GetPrevState(),
|
|
e.GetNewState(),
|
|
ts,
|
|
)
|
|
}
|
|
if err := ew.Flush(); err != nil {
|
|
return fmt.Errorf("flush events table: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// formatDuration returns a human-readable duration string.
|
|
func formatDuration(d time.Duration) string {
|
|
if d < time.Minute {
|
|
return fmt.Sprintf("%ds", int(d.Seconds()))
|
|
}
|
|
if d < time.Hour {
|
|
return fmt.Sprintf("%dm%ds", int(d.Minutes()), int(d.Seconds())%60)
|
|
}
|
|
if d < 24*time.Hour {
|
|
return fmt.Sprintf("%dh%dm", int(d.Hours()), int(d.Minutes())%60)
|
|
}
|
|
days := int(d.Hours()) / 24
|
|
hours := int(d.Hours()) % 24
|
|
return fmt.Sprintf("%dd%dh", days, hours)
|
|
}
|