Files
mcp/internal/agent/lifecycle.go
Kyle Isom 08b3e2a472 Migrate module path from kyle/ to mc/ org
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>
2026-03-27 02:07:42 -07:00

168 lines
5.5 KiB
Go

package agent
import (
"context"
"database/sql"
"fmt"
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
"git.wntrmute.dev/mc/mcp/internal/registry"
"git.wntrmute.dev/mc/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}
// Remove routes from mc-proxy before stopping the container.
if len(c.Routes) > 0 && a.Proxy != nil {
if err := a.Proxy.RemoveRoutes(ctx, req.GetName(), c.Routes); err != nil {
a.Logger.Warn("failed to remove routes", "service", req.GetName(), "component", c.Name, "err", err)
}
}
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
}