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  }