Implement mcp purge command for registry cleanup
Add PurgeComponent RPC to the agent service that removes stale registry entries for components that are both gone (observed state is removed, unknown, or exited) and unwanted (not in any current service definition). Refuses to purge components with running or stopped containers. When all components of a service are purged, the service row is deleted too. Supports --dry-run to preview without modifying the database. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,7 @@ func main() {
|
||||
root.AddCommand(pushCmd())
|
||||
root.AddCommand(pullCmd())
|
||||
root.AddCommand(nodeCmd())
|
||||
root.AddCommand(purgeCmd())
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
119
cmd/mcp/purge.go
Normal file
119
cmd/mcp/purge.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
mcpv1 "git.wntrmute.dev/kyle/mcp/gen/mcp/v1"
|
||||
"git.wntrmute.dev/kyle/mcp/internal/config"
|
||||
"git.wntrmute.dev/kyle/mcp/internal/servicedef"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func purgeCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "purge [service[/component]]",
|
||||
Short: "Remove stale registry entries for gone, undefined components",
|
||||
Long: `Purge removes registry entries that are both unwanted (not in any
|
||||
current service definition) and gone (no corresponding container in the
|
||||
runtime). It never stops or removes running containers.
|
||||
|
||||
Use --dry-run to preview what would be purged.`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.LoadCLIConfig(cfgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||
|
||||
var service, component string
|
||||
if len(args) == 1 {
|
||||
service, component = parseServiceArg(args[0])
|
||||
}
|
||||
|
||||
// Load all local service definitions to build the set of
|
||||
// currently-defined service/component pairs.
|
||||
definedComponents := buildDefinedComponents(cfg)
|
||||
|
||||
// Build node address lookup.
|
||||
nodeAddr := make(map[string]string, len(cfg.Nodes))
|
||||
for _, n := range cfg.Nodes {
|
||||
nodeAddr[n.Name] = n.Address
|
||||
}
|
||||
|
||||
// If a specific service was given and we can find its node,
|
||||
// only talk to that node. Otherwise, talk to all nodes.
|
||||
targetNodes := cfg.Nodes
|
||||
if service != "" {
|
||||
if nodeName, nodeAddr, err := findServiceNode(cfg, service); err == nil {
|
||||
targetNodes = []config.NodeConfig{{Name: nodeName, Address: nodeAddr}}
|
||||
}
|
||||
}
|
||||
|
||||
anyResults := false
|
||||
for _, node := range targetNodes {
|
||||
client, conn, err := dialAgent(node.Address, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial %s: %w", node.Name, err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
resp, err := client.PurgeComponent(context.Background(), &mcpv1.PurgeRequest{
|
||||
Service: service,
|
||||
Component: component,
|
||||
DryRun: dryRun,
|
||||
DefinedComponents: definedComponents,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("purge on %s: %w", node.Name, err)
|
||||
}
|
||||
|
||||
for _, r := range resp.GetResults() {
|
||||
anyResults = true
|
||||
if r.GetPurged() {
|
||||
if dryRun {
|
||||
fmt.Printf("would purge %s/%s (%s)\n", r.GetService(), r.GetComponent(), r.GetReason())
|
||||
} else {
|
||||
fmt.Printf("purged %s/%s (%s)\n", r.GetService(), r.GetComponent(), r.GetReason())
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("skipped %s/%s (%s)\n", r.GetService(), r.GetComponent(), r.GetReason())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !anyResults {
|
||||
fmt.Println("nothing to purge")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("dry-run", false, "preview what would be purged without modifying the registry")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// buildDefinedComponents reads all local service definition files and returns
|
||||
// a list of "service/component" strings for every defined component.
|
||||
func buildDefinedComponents(cfg *config.CLIConfig) []string {
|
||||
defs, err := servicedef.LoadAll(cfg.Services.Dir)
|
||||
if err != nil {
|
||||
// If we can't read service definitions, return an empty list.
|
||||
// The agent will treat every component as undefined, which is the
|
||||
// most conservative behavior (everything eligible gets purged).
|
||||
return nil
|
||||
}
|
||||
|
||||
var defined []string
|
||||
for _, def := range defs {
|
||||
for _, comp := range def.Components {
|
||||
defined = append(defined, def.Name+"/"+comp.Name)
|
||||
}
|
||||
}
|
||||
return defined
|
||||
}
|
||||
Reference in New Issue
Block a user