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>
229 lines
5.8 KiB
Go
229 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
|
|
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
|
"git.wntrmute.dev/mc/mcp/internal/config"
|
|
"git.wntrmute.dev/mc/mcp/internal/servicedef"
|
|
toml "github.com/pelletier/go-toml/v2"
|
|
"github.com/spf13/cobra"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
func serviceCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "service",
|
|
Short: "Service definition management",
|
|
}
|
|
|
|
show := &cobra.Command{
|
|
Use: "show <service>",
|
|
Short: "Print current spec from agent registry",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runServiceShow,
|
|
}
|
|
|
|
edit := &cobra.Command{
|
|
Use: "edit <service>",
|
|
Short: "Open service definition in $EDITOR",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runServiceEdit,
|
|
}
|
|
|
|
export := &cobra.Command{
|
|
Use: "export <service>",
|
|
Short: "Write agent registry spec to local service file",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runServiceExport,
|
|
}
|
|
export.Flags().StringP("file", "f", "", "output file path")
|
|
|
|
cmd.AddCommand(show, edit, export)
|
|
return cmd
|
|
}
|
|
|
|
// findServiceNode checks the local service definition file first, then
|
|
// queries all agents to locate which node runs the named service.
|
|
func findServiceNode(cfg *config.CLIConfig, serviceName string) (string, string, error) {
|
|
defPath := filepath.Join(cfg.Services.Dir, serviceName+".toml")
|
|
if def, err := servicedef.Load(defPath); err == nil {
|
|
for _, n := range cfg.Nodes {
|
|
if n.Name == def.Node {
|
|
return n.Name, n.Address, nil
|
|
}
|
|
}
|
|
return "", "", fmt.Errorf("node %q from service def not found in config", def.Node)
|
|
}
|
|
|
|
for _, n := range cfg.Nodes {
|
|
client, conn, err := dialAgent(n.Address, cfg)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
resp, err := client.ListServices(context.Background(), &mcpv1.ListServicesRequest{})
|
|
_ = conn.Close()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, svc := range resp.GetServices() {
|
|
if svc.GetName() == serviceName {
|
|
return n.Name, n.Address, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", "", fmt.Errorf("service %q not found on any node", serviceName)
|
|
}
|
|
|
|
// fetchServiceInfo dials the agent at address, calls ListServices, and returns
|
|
// the matching ServiceInfo. The caller must close the returned connection.
|
|
func fetchServiceInfo(address string, cfg *config.CLIConfig, serviceName string) (*mcpv1.ServiceInfo, *grpc.ClientConn, error) {
|
|
client, conn, err := dialAgent(address, cfg)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("dial agent: %w", err)
|
|
}
|
|
|
|
resp, err := client.ListServices(context.Background(), &mcpv1.ListServicesRequest{})
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
return nil, nil, fmt.Errorf("list services: %w", err)
|
|
}
|
|
|
|
for _, svc := range resp.GetServices() {
|
|
if svc.GetName() == serviceName {
|
|
return svc, conn, nil
|
|
}
|
|
}
|
|
|
|
_ = conn.Close()
|
|
return nil, nil, fmt.Errorf("service %q not found on agent", serviceName)
|
|
}
|
|
|
|
// serviceInfoToSpec converts a ServiceInfo (runtime view) to a ServiceSpec
|
|
// (config view) for use with servicedef.FromProto.
|
|
func serviceInfoToSpec(info *mcpv1.ServiceInfo) *mcpv1.ServiceSpec {
|
|
spec := &mcpv1.ServiceSpec{
|
|
Name: info.GetName(),
|
|
Active: info.GetActive(),
|
|
}
|
|
for _, c := range info.GetComponents() {
|
|
spec.Components = append(spec.Components, &mcpv1.ComponentSpec{
|
|
Name: c.GetName(),
|
|
Image: c.GetImage(),
|
|
})
|
|
}
|
|
return spec
|
|
}
|
|
|
|
func runServiceShow(_ *cobra.Command, args []string) error {
|
|
serviceName := args[0]
|
|
|
|
cfg, err := config.LoadCLIConfig(cfgPath)
|
|
if err != nil {
|
|
return fmt.Errorf("load config: %w", err)
|
|
}
|
|
|
|
_, address, err := findServiceNode(cfg, serviceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
svc, conn, err := fetchServiceInfo(address, cfg, serviceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
|
|
def := servicedef.FromProto(serviceInfoToSpec(svc), "")
|
|
data, err := toml.Marshal(def)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal service: %w", err)
|
|
}
|
|
fmt.Print(string(data))
|
|
return nil
|
|
}
|
|
|
|
func runServiceEdit(_ *cobra.Command, args []string) error {
|
|
serviceName := args[0]
|
|
|
|
cfg, err := config.LoadCLIConfig(cfgPath)
|
|
if err != nil {
|
|
return fmt.Errorf("load config: %w", err)
|
|
}
|
|
|
|
filePath := filepath.Join(cfg.Services.Dir, serviceName+".toml")
|
|
|
|
// If local file does not exist, export from agent first.
|
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
|
nodeName, address, err := findServiceNode(cfg, serviceName)
|
|
if err != nil {
|
|
return fmt.Errorf("find service for export: %w", err)
|
|
}
|
|
|
|
svc, conn, err := fetchServiceInfo(address, cfg, serviceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
|
|
def := servicedef.FromProto(serviceInfoToSpec(svc), nodeName)
|
|
if err := servicedef.Write(filePath, def); err != nil {
|
|
return fmt.Errorf("write service def: %w", err)
|
|
}
|
|
_, _ = fmt.Fprintf(os.Stderr, "Exported %s from agent to %s\n", serviceName, filePath)
|
|
}
|
|
|
|
editor := os.Getenv("EDITOR")
|
|
if editor == "" {
|
|
editor = os.Getenv("VISUAL")
|
|
}
|
|
if editor == "" {
|
|
editor = "vi"
|
|
}
|
|
|
|
cmd := exec.Command(editor, filePath) //nolint:gosec // editor from trusted env
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
return cmd.Run()
|
|
}
|
|
|
|
func runServiceExport(cmd *cobra.Command, args []string) error {
|
|
serviceName := args[0]
|
|
|
|
cfg, err := config.LoadCLIConfig(cfgPath)
|
|
if err != nil {
|
|
return fmt.Errorf("load config: %w", err)
|
|
}
|
|
|
|
nodeName, address, err := findServiceNode(cfg, serviceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
svc, conn, err := fetchServiceInfo(address, cfg, serviceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
|
|
def := servicedef.FromProto(serviceInfoToSpec(svc), nodeName)
|
|
|
|
outPath, _ := cmd.Flags().GetString("file")
|
|
if outPath == "" {
|
|
outPath = filepath.Join(cfg.Services.Dir, serviceName+".toml")
|
|
}
|
|
|
|
if err := servicedef.Write(outPath, def); err != nil {
|
|
return fmt.Errorf("write service def: %w", err)
|
|
}
|
|
fmt.Printf("Exported %s to %s\n", serviceName, outPath)
|
|
return nil
|
|
}
|