package agent import ( "context" "fmt" mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1" "git.wntrmute.dev/mc/mcp/internal/registry" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // UndeployService fully tears down a service: removes routes, DNS records, // TLS certificates, stops and removes containers, releases ports, and marks // the service inactive. This is the inverse of Deploy. func (a *Agent) UndeployService(ctx context.Context, req *mcpv1.UndeployServiceRequest) (*mcpv1.UndeployServiceResponse, error) { a.Logger.Info("UndeployService", "service", req.GetName()) if req.GetName() == "" { return nil, status.Error(codes.InvalidArgument, "service name is required") } serviceName := req.GetName() components, err := registry.ListComponents(a.DB, serviceName) if err != nil { return nil, status.Errorf(codes.Internal, "list components: %v", err) } var results []*mcpv1.ComponentResult dnsRemoved := false for _, c := range components { r := a.undeployComponent(ctx, serviceName, &c, &dnsRemoved) results = append(results, r) } // Mark the service as inactive. if err := registry.UpdateServiceActive(a.DB, serviceName, false); err != nil { a.Logger.Warn("failed to mark service inactive", "service", serviceName, "err", err) } return &mcpv1.UndeployServiceResponse{Results: results}, nil } // undeployComponent tears down a single component. The dnsRemoved flag // tracks whether DNS has already been removed for this service (DNS is // per-service, not per-component). func (a *Agent) undeployComponent(ctx context.Context, serviceName string, c *registry.Component, dnsRemoved *bool) *mcpv1.ComponentResult { containerName := ContainerNameFor(serviceName, c.Name) r := &mcpv1.ComponentResult{Name: c.Name, Success: true} // 1. Remove mc-proxy routes. if len(c.Routes) > 0 && a.Proxy != nil { if err := a.Proxy.RemoveRoutes(ctx, serviceName, c.Routes); err != nil { a.Logger.Warn("failed to remove routes", "service", serviceName, "component", c.Name, "err", err) } } // 2. Remove DNS records (once per service). if len(c.Routes) > 0 && a.DNS != nil && !*dnsRemoved { if err := a.DNS.RemoveRecord(ctx, serviceName); err != nil { a.Logger.Warn("failed to remove DNS record", "service", serviceName, "err", err) } *dnsRemoved = true } // 3. Remove TLS certs (L7 routes only). if hasL7Routes(c.Routes) && a.Certs != nil { if err := a.Certs.RemoveCert(serviceName); err != nil { a.Logger.Warn("failed to remove TLS cert", "service", serviceName, "err", err) } } // 4. Stop and remove the container. if err := a.Runtime.Stop(ctx, containerName); err != nil { a.Logger.Info("stop container (ignored)", "container", containerName, "error", err) } if err := a.Runtime.Remove(ctx, containerName); err != nil { a.Logger.Info("remove container (ignored)", "container", containerName, "error", err) } // 5. Release allocated ports. if a.PortAlloc != nil { hostPorts, err := registry.GetRouteHostPorts(a.DB, serviceName, c.Name) if err == nil { for _, port := range hostPorts { a.PortAlloc.Release(port) } } } // 6. Update registry state. if err := registry.UpdateComponentState(a.DB, serviceName, c.Name, "removed", "removed"); err != nil { r.Success = false r.Error = fmt.Sprintf("update state: %v", err) } return r }