package agent import ( "context" "fmt" mcpv1 "git.wntrmute.dev/kyle/mcp/gen/mcp/v1" "git.wntrmute.dev/kyle/mcp/internal/registry" ) // PurgeComponent removes stale registry entries for components that are both // gone (observed state is removed/unknown/exited) and unwanted (not in any // current service definition). It never touches running containers. func (a *Agent) PurgeComponent(ctx context.Context, req *mcpv1.PurgeRequest) (*mcpv1.PurgeResponse, error) { a.Logger.Info("PurgeComponent", "service", req.GetService(), "component", req.GetComponent(), "dry_run", req.GetDryRun(), ) // Build a set of defined service/component pairs for quick lookup. defined := make(map[string]bool, len(req.GetDefinedComponents())) for _, dc := range req.GetDefinedComponents() { defined[dc] = true } // Determine which services to examine. var services []registry.Service if req.GetService() != "" { svc, err := registry.GetService(a.DB, req.GetService()) if err != nil { return nil, fmt.Errorf("get service %q: %w", req.GetService(), err) } services = []registry.Service{*svc} } else { var err error services, err = registry.ListServices(a.DB) if err != nil { return nil, fmt.Errorf("list services: %w", err) } } var results []*mcpv1.PurgeResult for _, svc := range services { components, err := registry.ListComponents(a.DB, svc.Name) if err != nil { return nil, fmt.Errorf("list components for %q: %w", svc.Name, err) } // If a specific component was requested, filter to just that one. if req.GetComponent() != "" { var filtered []registry.Component for _, c := range components { if c.Name == req.GetComponent() { filtered = append(filtered, c) } } components = filtered } for _, comp := range components { result := a.evaluatePurge(svc.Name, &comp, defined, req.GetDryRun()) results = append(results, result) } // If all components of this service were purged (not dry-run), // check if the service should be cleaned up too. if !req.GetDryRun() { remaining, err := registry.ListComponents(a.DB, svc.Name) if err != nil { a.Logger.Warn("failed to check remaining components", "service", svc.Name, "err", err) continue } if len(remaining) == 0 { if err := registry.DeleteService(a.DB, svc.Name); err != nil { a.Logger.Warn("failed to delete empty service", "service", svc.Name, "err", err) } else { a.Logger.Info("purged empty service", "service", svc.Name) } } } } return &mcpv1.PurgeResponse{Results: results}, nil } // purgeableStates are observed states that indicate a component's container // is gone and the registry entry can be safely removed. var purgeableStates = map[string]bool{ "removed": true, "unknown": true, "exited": true, } // evaluatePurge checks whether a single component is eligible for purge and, // if not in dry-run mode, deletes it. func (a *Agent) evaluatePurge(service string, comp *registry.Component, defined map[string]bool, dryRun bool) *mcpv1.PurgeResult { key := service + "/" + comp.Name // Safety: refuse to purge components with a live container. if !purgeableStates[comp.ObservedState] { return &mcpv1.PurgeResult{ Service: service, Component: comp.Name, Purged: false, Reason: fmt.Sprintf("observed=%s, container still exists", comp.ObservedState), } } // Don't purge components that are still in service definitions. if defined[key] { return &mcpv1.PurgeResult{ Service: service, Component: comp.Name, Purged: false, Reason: "still in service definitions", } } reason := fmt.Sprintf("observed=%s, not in service definitions", comp.ObservedState) if dryRun { return &mcpv1.PurgeResult{ Service: service, Component: comp.Name, Purged: true, Reason: reason, } } // Delete events first (events table has no FK to components). if err := registry.DeleteComponentEvents(a.DB, service, comp.Name); err != nil { a.Logger.Warn("failed to delete events during purge", "service", service, "component", comp.Name, "err", err) } // Delete the component (CASCADE handles ports, volumes, cmd). if err := registry.DeleteComponent(a.DB, service, comp.Name); err != nil { return &mcpv1.PurgeResult{ Service: service, Component: comp.Name, Purged: false, Reason: fmt.Sprintf("delete failed: %v", err), } } a.Logger.Info("purged component", "service", service, "component", comp.Name, "reason", reason) return &mcpv1.PurgeResult{ Service: service, Component: comp.Name, Purged: true, Reason: reason, } }