github.com/cilium/cilium@v1.16.2/pkg/service/healthserver/healthserver.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package healthserver 5 6 import ( 7 "context" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "net/http" 12 "strconv" 13 "sync/atomic" 14 15 "github.com/sirupsen/logrus" 16 "golang.org/x/sys/unix" 17 18 "github.com/cilium/cilium/pkg/counter" 19 lb "github.com/cilium/cilium/pkg/loadbalancer" 20 "github.com/cilium/cilium/pkg/logging" 21 "github.com/cilium/cilium/pkg/logging/logfields" 22 "github.com/cilium/cilium/pkg/option" 23 ) 24 25 var log = logging.DefaultLogger.WithField(logfields.LogSubsys, "service-healthserver") 26 27 // ServiceName is the name and namespace of the service 28 type ServiceName struct { 29 Namespace string `json:"namespace"` 30 Name string `json:"name"` 31 } 32 33 // Service represents the object returned by the health server 34 type Service struct { 35 Service ServiceName `json:"service"` 36 LocalEndpoints int `json:"localEndpoints"` 37 } 38 39 // NewService creates a new service 40 func NewService(ns, name string, localEndpoints int) *Service { 41 return &Service{ 42 Service: ServiceName{ 43 Namespace: ns, 44 Name: name, 45 }, 46 LocalEndpoints: localEndpoints, 47 } 48 } 49 50 // healthHTTPServer is a running HTTP health server for a certain service 51 type healthHTTPServer interface { 52 updateService(*Service) 53 shutdown() 54 } 55 56 // healthHTTPServerFactory creates a new HTTP health server, used for mocking 57 type healthHTTPServerFactory interface { 58 newHTTPHealthServer(port uint16, svc *Service) healthHTTPServer 59 } 60 61 // ServiceHealthServer manages HTTP health check ports. For each added service, 62 // it opens a HTTP server on the specified HealthCheckNodePort and either 63 // responds with 200 OK if there are local endpoints for the service, or with 64 // 503 Service Unavailable if the service does not have any local endpoints. 65 type ServiceHealthServer struct { 66 healthHTTPServerByPort map[uint16]healthHTTPServer 67 portRefCount counter.IntCounter 68 portByServiceID map[lb.ID]uint16 69 healthHTTPServerFactory healthHTTPServerFactory 70 } 71 72 // New creates a new health service server which services health checks by 73 // serving an HTTP endpoint for each service on the given HealthCheckNodePort. 74 func New() *ServiceHealthServer { 75 return WithHealthHTTPServerFactory(&httpHealthHTTPServerFactory{}) 76 } 77 78 // WithHealthHTTPServerFactory creates a new health server with a specific health 79 // server factory for testing purposes. 80 func WithHealthHTTPServerFactory(healthHTTPServerFactory healthHTTPServerFactory) *ServiceHealthServer { 81 return &ServiceHealthServer{ 82 healthHTTPServerByPort: map[uint16]healthHTTPServer{}, 83 portRefCount: counter.IntCounter{}, 84 portByServiceID: map[lb.ID]uint16{}, 85 healthHTTPServerFactory: healthHTTPServerFactory, 86 } 87 } 88 89 func (s *ServiceHealthServer) removeHTTPListener(port uint16) { 90 if s.portRefCount.Delete(int(port)) { 91 srv, ok := s.healthHTTPServerByPort[port] 92 if !ok { 93 log.WithField(logfields.Port, port).Warn("Invalid refcount for service health check port") 94 return 95 } 96 srv.shutdown() 97 delete(s.healthHTTPServerByPort, port) 98 } 99 } 100 101 // UpsertService inserts or updates a service health check server on 'port'. If 102 // 'port' is zero, the listener for the added service is stopped. 103 // Access to this method is not synchronized. It is the caller's responsibility 104 // to ensure this method is called in a thread-safe manner. 105 func (s *ServiceHealthServer) UpsertService(svcID lb.ID, ns, name string, localEndpoints int, port uint16) { 106 oldPort, foundSvc := s.portByServiceID[svcID] 107 if foundSvc && oldPort != port { 108 // HealthCheckNodePort has changed, we treat this as a DeleteService 109 // followed by an Insert. 110 s.removeHTTPListener(oldPort) 111 delete(s.portByServiceID, svcID) 112 foundSvc = false 113 } 114 115 // Nothing to do for services without a health check port 116 if port == 0 { 117 return 118 } 119 120 // Since we have one lb.SVC per frontend, we may end up receiving 121 // multiple identical services per port. The following code assumes that 122 // two services with the same port also have the same name and amount of 123 // endpoints. We reference count the listeners to make sure we only have 124 // a single listener per port. 125 126 svc := NewService(ns, name, localEndpoints) 127 if !foundSvc { 128 // We only bump the reference count if this is a service ID we have 129 // not seen before 130 if s.portRefCount.Add(int(port)) { 131 s.healthHTTPServerByPort[port] = s.healthHTTPServerFactory.newHTTPHealthServer(port, svc) 132 } 133 } 134 135 srv, foundSrv := s.healthHTTPServerByPort[port] 136 if !foundSrv { 137 log.WithFields(logrus.Fields{ 138 logfields.ServiceID: svcID, 139 logfields.ServiceNamespace: ns, 140 logfields.ServiceName: name, 141 logfields.ServiceHealthCheckNodePort: port, 142 }).Warn("Unable to find service health check listener") 143 return 144 } 145 146 srv.updateService(svc) 147 s.portByServiceID[svcID] = port 148 } 149 150 // DeleteService stops the health server for the given service with 'svcID'. 151 // Access to this method is not synchronized. It is the caller's responsibility 152 // to ensure this method is called in a thread-safe manner. 153 func (s *ServiceHealthServer) DeleteService(svcID lb.ID) { 154 if port, ok := s.portByServiceID[svcID]; ok { 155 s.removeHTTPListener(port) 156 delete(s.portByServiceID, svcID) 157 } 158 } 159 160 type httpHealthServer struct { 161 http.Server 162 service atomic.Value 163 } 164 165 type httpHealthHTTPServerFactory struct{} 166 167 func (h *httpHealthHTTPServerFactory) newHTTPHealthServer(port uint16, svc *Service) healthHTTPServer { 168 srv := &httpHealthServer{} 169 srv.service.Store(svc) 170 srv.Server = http.Server{ 171 Addr: fmt.Sprintf(":%d", port), 172 Handler: srv, 173 } 174 175 go func() { 176 log.WithFields(logrus.Fields{ 177 logfields.ServiceName: svc.Service.Name, 178 logfields.ServiceNamespace: svc.Service.Namespace, 179 logfields.ServiceHealthCheckNodePort: port, 180 }).Debug("Starting new service health server") 181 182 if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { 183 svc := srv.loadService() 184 if errors.Is(err, unix.EADDRINUSE) { 185 log.WithError(err).WithFields(logrus.Fields{ 186 logfields.ServiceName: svc.Service.Name, 187 logfields.ServiceNamespace: svc.Service.Namespace, 188 logfields.ServiceHealthCheckNodePort: port, 189 }).Errorf("ListenAndServe failed for service health server, since the user might be running with kube-proxy. Please ensure that '--%s' option is set to false if kube-proxy is running.", option.EnableHealthCheckNodePort) 190 } 191 log.WithError(err).WithFields(logrus.Fields{ 192 logfields.ServiceName: svc.Service.Name, 193 logfields.ServiceNamespace: svc.Service.Namespace, 194 logfields.ServiceHealthCheckNodePort: port, 195 }).Error("ListenAndServe failed for service health server") 196 } 197 }() 198 199 return srv 200 } 201 202 func (h *httpHealthServer) loadService() *Service { 203 return h.service.Load().(*Service) 204 } 205 206 func (h *httpHealthServer) updateService(svc *Service) { 207 h.service.Store(svc) 208 } 209 210 func (h *httpHealthServer) shutdown() { 211 h.Server.Shutdown(context.Background()) 212 } 213 214 func (h *httpHealthServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 215 // Use headers and JSON output compatible with kube-proxy 216 svc := h.loadService() 217 w.Header().Set("Content-Type", "application/json") 218 w.Header().Set("X-Content-Type-Options", "nosniff") 219 w.Header().Set("X-Load-Balancing-Endpoint-Weight", strconv.Itoa(svc.LocalEndpoints)) 220 221 if svc.LocalEndpoints == 0 { 222 w.WriteHeader(http.StatusServiceUnavailable) 223 } else { 224 w.WriteHeader(http.StatusOK) 225 } 226 if err := json.NewEncoder(w).Encode(&svc); err != nil { 227 http.Error(w, err.Error(), http.StatusInternalServerError) 228 } 229 }