package grpcapi import ( "context" "fmt" "net" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "git.ipng.ch/ipng/vpp-maglev/internal/checker" "git.ipng.ch/ipng/vpp-maglev/internal/config" "git.ipng.ch/ipng/vpp-maglev/internal/health" ) // Server implements the HealthCheckerServer gRPC interface. type Server struct { UnimplementedHealthCheckerServer checker *checker.Checker } // NewServer creates a Server backed by the given Checker. func NewServer(c *checker.Checker) *Server { return &Server{checker: c} } // ListVIPs returns the names of all configured VIPs. func (s *Server) ListVIPs(_ context.Context, _ *ListVIPsRequest) (*ListVIPsResponse, error) { return &ListVIPsResponse{VipNames: s.checker.ListVIPs()}, nil } // GetVIP returns configuration details for a single VIP. func (s *Server) GetVIP(_ context.Context, req *GetVIPRequest) (*VIPInfo, error) { vip, ok := s.checker.GetVIP(req.VipName) if !ok { return nil, status.Errorf(codes.NotFound, "vip %q not found", req.VipName) } return vipToProto(req.VipName, vip), nil } // ListBackends returns health state for all backends of a VIP. func (s *Server) ListBackends(_ context.Context, req *ListBackendsRequest) (*ListBackendsResponse, error) { if _, ok := s.checker.GetVIP(req.VipName); !ok { return nil, status.Errorf(codes.NotFound, "vip %q not found", req.VipName) } backends := s.checker.ListBackends(req.VipName) resp := &ListBackendsResponse{} for _, b := range backends { resp.Backends = append(resp.Backends, backendToProto(b)) } return resp, nil } // GetBackend returns health state for a specific VIP:backend tuple. func (s *Server) GetBackend(_ context.Context, req *GetBackendRequest) (*BackendInfo, error) { b, ok := s.checker.GetBackend(req.VipName, req.BackendAddress) if !ok { return nil, status.Errorf(codes.NotFound, "backend %q in vip %q not found", req.BackendAddress, req.VipName) } return backendToProto(b), nil } // PauseBackend pauses health checking for a specific backend. func (s *Server) PauseBackend(_ context.Context, req *PauseResumeRequest) (*BackendInfo, error) { b, ok := s.checker.PauseBackend(req.VipName, req.BackendAddress) if !ok { return nil, status.Errorf(codes.NotFound, "backend %q in vip %q not found", req.BackendAddress, req.VipName) } return backendToProto(b), nil } // ResumeBackend resumes health checking for a specific backend. func (s *Server) ResumeBackend(_ context.Context, req *PauseResumeRequest) (*BackendInfo, error) { b, ok := s.checker.ResumeBackend(req.VipName, req.BackendAddress) if !ok { return nil, status.Errorf(codes.NotFound, "backend %q in vip %q not found", req.BackendAddress, req.VipName) } return backendToProto(b), nil } // WatchTransitions streams the current state of all backends on connect, then // streams live state transitions until the client disconnects. func (s *Server) WatchTransitions(_ *WatchRequest, stream HealthChecker_WatchTransitionsServer) error { // Send current state of all backends as synthetic events. for _, vipName := range s.checker.ListVIPs() { for _, b := range s.checker.ListBackends(vipName) { ev := &TransitionEvent{ VipName: vipName, BackendAddress: b.Address.String(), Transition: &TransitionRecord{ From: b.State.String(), To: b.State.String(), AtUnixNs: 0, }, } if err := stream.Send(ev); err != nil { return err } } } // Subscribe to live transitions. ch, unsub := s.checker.Subscribe() defer unsub() for { select { case <-stream.Context().Done(): return nil case e, ok := <-ch: if !ok { return nil } ev := &TransitionEvent{ VipName: e.VIPName, BackendAddress: e.Backend.String(), Transition: transitionToProto(e.Transition), } if err := stream.Send(ev); err != nil { return err } } } } // ---- conversion helpers ---------------------------------------------------- func vipToProto(name string, v config.VIP) *VIPInfo { info := &VIPInfo{ Name: name, Address: v.Address.String(), Protocol: v.Protocol, Port: uint32(v.Port), Description: v.Description, } for _, b := range v.Backends { info.Backends = append(info.Backends, b.String()) } return info } func backendToProto(b *health.Backend) *BackendInfo { info := &BackendInfo{ VipName: b.VIPName, Address: b.Address.String(), State: b.State.String(), } for _, t := range b.Transitions { info.Transitions = append(info.Transitions, transitionToProto(t)) } return info } func transitionToProto(t health.Transition) *TransitionRecord { return &TransitionRecord{ From: t.From.String(), To: t.To.String(), AtUnixNs: t.At.UnixNano(), } } // Ensure net.IP is imported (used via b.Address.String()). var _ = net.IP{} var _ = fmt.Sprintf