package main import ( "context" "fmt" "path/filepath" "strings" "github.com/spf13/cobra" mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1" "git.wntrmute.dev/mc/mcp/internal/config" "git.wntrmute.dev/mc/mcp/internal/runtime" "git.wntrmute.dev/mc/mcp/internal/servicedef" ) func deployCmd() *cobra.Command { var direct bool cmd := &cobra.Command{ Use: "deploy [/]", Short: "Deploy service from service definition", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cfg, err := config.LoadCLIConfig(cfgPath) if err != nil { return fmt.Errorf("load config: %w", err) } serviceName, component := parseServiceArg(args[0]) def, err := loadServiceDef(cmd, cfg, serviceName) if err != nil { return err } // Auto-build missing images if the service has build config. rt := &runtime.Podman{} if err := ensureImages(cmd.Context(), cfg, def, rt, component); err != nil { return err } spec := servicedef.ToProto(def) // Route through master if configured and not in direct mode. if cfg.Master != nil && cfg.Master.Address != "" && !direct { return deployViaMaster(cfg, spec) } // Direct mode: deploy to agent. address, err := findNodeAddress(cfg, def.Node) 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() }() resp, err := client.Deploy(context.Background(), &mcpv1.DeployRequest{ Service: spec, Component: component, }) if err != nil { return fmt.Errorf("deploy: %w", err) } printComponentResults(resp.GetResults()) return nil }, } cmd.Flags().StringP("file", "f", "", "service definition file") cmd.Flags().BoolVar(&direct, "direct", false, "bypass master, deploy directly to agent (v1 mode)") return cmd } func deployViaMaster(cfg *config.CLIConfig, spec *mcpv1.ServiceSpec) error { client, conn, err := dialMaster(cfg.Master.Address, cfg) if err != nil { return fmt.Errorf("dial master: %w", err) } defer func() { _ = conn.Close() }() resp, err := client.Deploy(context.Background(), &mcpv1.MasterDeployRequest{ Service: spec, }) if err != nil { return fmt.Errorf("master deploy: %w", err) } fmt.Printf(" %s: placed on %s\n", spec.GetName(), resp.GetNode()) if r := resp.GetDeployResult(); r != nil { printStepResult("deploy", r) } if r := resp.GetDnsResult(); r != nil { printStepResult("dns", r) } if r := resp.GetEdgeRouteResult(); r != nil { printStepResult("edge", r) } if !resp.GetSuccess() { return fmt.Errorf("deploy failed: %s", resp.GetError()) } return nil } func printStepResult(name string, r *mcpv1.StepResult) { if r.GetSuccess() { fmt.Printf(" %s: ok\n", name) } else { fmt.Printf(" %s: FAILED — %s\n", name, r.GetError()) } } // parseServiceArg splits a "service/component" argument into its parts. func parseServiceArg(arg string) (service, component string) { parts := strings.SplitN(arg, "/", 2) service = parts[0] if len(parts) == 2 { component = parts[1] } return service, component } // loadServiceDef attempts to load a service definition from the -f flag, // the configured services directory, or by falling back to the agent registry. func loadServiceDef(cmd *cobra.Command, cfg *config.CLIConfig, serviceName string) (*servicedef.ServiceDef, error) { // Check -f flag first. filePath, _ := cmd.Flags().GetString("file") if filePath != "" { def, err := servicedef.Load(filePath) if err != nil { return nil, fmt.Errorf("load service def from %q: %w", filePath, err) } return def, nil } // Try services directory. dirPath := filepath.Join(cfg.Services.Dir, serviceName+".toml") def, err := servicedef.Load(dirPath) if err == nil { return def, nil } // Fall back to agent registry: query each node for the service. for _, node := range cfg.Nodes { client, conn, dialErr := dialAgent(node.Address, cfg) if dialErr != nil { continue } resp, listErr := client.ListServices(context.Background(), &mcpv1.ListServicesRequest{}) _ = conn.Close() if listErr != nil { continue } for _, svc := range resp.GetServices() { if svc.GetName() == serviceName { return servicedef.FromProto(serviceSpecFromInfo(svc), node.Name), nil } } } return nil, fmt.Errorf("service definition %q not found in %q or agent registry", serviceName, cfg.Services.Dir) } // serviceSpecFromInfo converts a ServiceInfo to a ServiceSpec for use with // servicedef.FromProto. This is needed because the agent registry returns // ServiceInfo, not ServiceSpec. func serviceSpecFromInfo(info *mcpv1.ServiceInfo) *mcpv1.ServiceSpec { spec := &mcpv1.ServiceSpec{ Name: info.GetName(), Active: info.GetActive(), Comment: info.GetComment(), } for _, c := range info.GetComponents() { spec.Components = append(spec.Components, &mcpv1.ComponentSpec{ Name: c.GetName(), Image: c.GetImage(), }) } return spec }