package agent import ( "context" "database/sql" "fmt" mcpv1 "git.wntrmute.dev/kyle/mcp/gen/mcp/v1" "git.wntrmute.dev/kyle/mcp/internal/registry" "git.wntrmute.dev/kyle/mcp/internal/runtime" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // StopService stops all components of a service. func (a *Agent) StopService(ctx context.Context, req *mcpv1.StopServiceRequest) (*mcpv1.StopServiceResponse, error) { a.Logger.Info("StopService", "service", req.GetName()) if req.GetName() == "" { return nil, status.Error(codes.InvalidArgument, "service name is required") } components, err := registry.ListComponents(a.DB, req.GetName()) if err != nil { return nil, status.Errorf(codes.Internal, "list components: %v", err) } var results []*mcpv1.ComponentResult for _, c := range components { containerName := ContainerNameFor(req.GetName(), c.Name) r := &mcpv1.ComponentResult{Name: c.Name, Success: true} if err := a.Runtime.Stop(ctx, containerName); err != nil { a.Logger.Info("stop container (ignored)", "container", containerName, "error", err) } if err := registry.UpdateComponentState(a.DB, req.GetName(), c.Name, "stopped", "stopped"); err != nil { r.Success = false r.Error = fmt.Sprintf("update state: %v", err) } results = append(results, r) } return &mcpv1.StopServiceResponse{Results: results}, nil } // StartService starts all components of a service. If a container already // exists but is stopped, it is removed first so a fresh one can be created. func (a *Agent) StartService(ctx context.Context, req *mcpv1.StartServiceRequest) (*mcpv1.StartServiceResponse, error) { a.Logger.Info("StartService", "service", req.GetName()) if req.GetName() == "" { return nil, status.Error(codes.InvalidArgument, "service name is required") } components, err := registry.ListComponents(a.DB, req.GetName()) if err != nil { return nil, status.Errorf(codes.Internal, "list components: %v", err) } var results []*mcpv1.ComponentResult for _, c := range components { r := startComponent(ctx, a, req.GetName(), &c) results = append(results, r) } return &mcpv1.StartServiceResponse{Results: results}, nil } // RestartService restarts all components of a service by stopping, removing, // and re-creating each container. The desired_state is not changed. func (a *Agent) RestartService(ctx context.Context, req *mcpv1.RestartServiceRequest) (*mcpv1.RestartServiceResponse, error) { a.Logger.Info("RestartService", "service", req.GetName()) if req.GetName() == "" { return nil, status.Error(codes.InvalidArgument, "service name is required") } components, err := registry.ListComponents(a.DB, req.GetName()) if err != nil { return nil, status.Errorf(codes.Internal, "list components: %v", err) } var results []*mcpv1.ComponentResult for _, c := range components { r := restartComponent(ctx, a, req.GetName(), &c) results = append(results, r) } return &mcpv1.RestartServiceResponse{Results: results}, nil } // startComponent removes any existing container and runs a fresh one from // the registry spec, then updates state to running. func startComponent(ctx context.Context, a *Agent, service string, c *registry.Component) *mcpv1.ComponentResult { containerName := ContainerNameFor(service, c.Name) r := &mcpv1.ComponentResult{Name: c.Name, Success: true} // Remove any pre-existing container; ignore errors for non-existent ones. _ = a.Runtime.Stop(ctx, containerName) _ = a.Runtime.Remove(ctx, containerName) spec := componentToSpec(service, c) if err := a.Runtime.Run(ctx, spec); err != nil { r.Success = false r.Error = fmt.Sprintf("run container: %v", err) return r } if err := registry.UpdateComponentState(a.DB, service, c.Name, "running", "running"); err != nil { r.Success = false r.Error = fmt.Sprintf("update state: %v", err) } return r } // restartComponent stops, removes, and re-creates a container without // changing the desired_state in the registry. func restartComponent(ctx context.Context, a *Agent, service string, c *registry.Component) *mcpv1.ComponentResult { containerName := ContainerNameFor(service, c.Name) r := &mcpv1.ComponentResult{Name: c.Name, Success: true} _ = a.Runtime.Stop(ctx, containerName) _ = a.Runtime.Remove(ctx, containerName) spec := componentToSpec(service, c) if err := a.Runtime.Run(ctx, spec); err != nil { r.Success = false r.Error = fmt.Sprintf("run container: %v", err) _ = registry.UpdateComponentState(a.DB, service, c.Name, "", "stopped") return r } if err := registry.UpdateComponentState(a.DB, service, c.Name, "", "running"); err != nil { r.Success = false r.Error = fmt.Sprintf("update state: %v", err) } return r } // componentToSpec builds a runtime.ContainerSpec from a registry Component. func componentToSpec(service string, c *registry.Component) runtime.ContainerSpec { return runtime.ContainerSpec{ Name: ContainerNameFor(service, c.Name), Image: c.Image, Network: c.Network, User: c.UserSpec, Restart: c.Restart, Ports: c.Ports, Volumes: c.Volumes, Cmd: c.Cmd, } } // componentExists checks whether a component already exists in the registry. func componentExists(db *sql.DB, service, name string) bool { _, err := registry.GetComponent(db, service, name) return err == nil }