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>
156 lines
4.5 KiB
Go
156 lines
4.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
|
"git.wntrmute.dev/mc/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,
|
|
}
|
|
}
|