sigs.k8s.io/controller-runtime@v0.18.2/pkg/metrics/server/server.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package server
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"fmt"
    23  	"net"
    24  	"net/http"
    25  	"os"
    26  	"path/filepath"
    27  	"sync"
    28  	"time"
    29  
    30  	"github.com/go-logr/logr"
    31  	"github.com/prometheus/client_golang/prometheus/promhttp"
    32  	"k8s.io/client-go/rest"
    33  	certutil "k8s.io/client-go/util/cert"
    34  
    35  	"sigs.k8s.io/controller-runtime/pkg/certwatcher"
    36  	"sigs.k8s.io/controller-runtime/pkg/internal/httpserver"
    37  	"sigs.k8s.io/controller-runtime/pkg/metrics"
    38  )
    39  
    40  const (
    41  	defaultMetricsEndpoint = "/metrics"
    42  )
    43  
    44  // DefaultBindAddress is the default bind address for the metrics server.
    45  var DefaultBindAddress = ":8080"
    46  
    47  // Server is a server that serves metrics.
    48  type Server interface {
    49  	// AddExtraHandler adds extra handler served on path to the http server that serves metrics.
    50  	AddExtraHandler(path string, handler http.Handler) error
    51  
    52  	// NeedLeaderElection implements the LeaderElectionRunnable interface, which indicates
    53  	// the metrics server doesn't need leader election.
    54  	NeedLeaderElection() bool
    55  
    56  	// Start runs the server.
    57  	// It will install the metrics related resources depending on the server configuration.
    58  	Start(ctx context.Context) error
    59  }
    60  
    61  // Options are all available options for the metrics.Server
    62  type Options struct {
    63  	// SecureServing enables serving metrics via https.
    64  	// Per default metrics will be served via http.
    65  	SecureServing bool
    66  
    67  	// BindAddress is the bind address for the metrics server.
    68  	// It will be defaulted to ":8080" if unspecified.
    69  	// Set this to "0" to disable the metrics server.
    70  	BindAddress string
    71  
    72  	// ExtraHandlers contains a map of handlers (by path) which will be added to the metrics server.
    73  	// This might be useful to register diagnostic endpoints e.g. pprof.
    74  	// Note that pprof endpoints are meant to be sensitive and shouldn't be exposed publicly.
    75  	// If the simple path -> handler mapping offered here is not enough, a new http
    76  	// server/listener should be added as Runnable to the manager via the Add method.
    77  	ExtraHandlers map[string]http.Handler
    78  
    79  	// FilterProvider provides a filter which is a func that is added around
    80  	// the metrics and the extra handlers on the metrics server.
    81  	// This can be e.g. used to enforce authentication and authorization on the handlers
    82  	// endpoint by setting this field to filters.WithAuthenticationAndAuthorization.
    83  	FilterProvider func(c *rest.Config, httpClient *http.Client) (Filter, error)
    84  
    85  	// CertDir is the directory that contains the server key and certificate. Defaults to
    86  	// <temp-dir>/k8s-metrics-server/serving-certs.
    87  	//
    88  	// Note: This option is only used when TLSOpts does not set GetCertificate.
    89  	// Note: If certificate or key doesn't exist a self-signed certificate will be used.
    90  	CertDir string
    91  
    92  	// CertName is the server certificate name. Defaults to tls.crt.
    93  	//
    94  	// Note: This option is only used when TLSOpts does not set GetCertificate.
    95  	// Note: If certificate or key doesn't exist a self-signed certificate will be used.
    96  	CertName string
    97  
    98  	// KeyName is the server key name. Defaults to tls.key.
    99  	//
   100  	// Note: This option is only used when TLSOpts does not set GetCertificate.
   101  	// Note: If certificate or key doesn't exist a self-signed certificate will be used.
   102  	KeyName string
   103  
   104  	// TLSOpts is used to allow configuring the TLS config used for the server.
   105  	// This also allows providing a certificate via GetCertificate.
   106  	TLSOpts []func(*tls.Config)
   107  
   108  	// ListenConfig contains options for listening to an address on the metric server.
   109  	ListenConfig net.ListenConfig
   110  }
   111  
   112  // Filter is a func that is added around metrics and extra handlers on the metrics server.
   113  type Filter func(log logr.Logger, handler http.Handler) (http.Handler, error)
   114  
   115  // NewServer constructs a new metrics.Server from the provided options.
   116  func NewServer(o Options, config *rest.Config, httpClient *http.Client) (Server, error) {
   117  	o.setDefaults()
   118  
   119  	// Skip server creation if metrics are disabled.
   120  	if o.BindAddress == "0" {
   121  		return nil, nil
   122  	}
   123  
   124  	// Validate that ExtraHandlers is not overwriting the default /metrics endpoint.
   125  	if o.ExtraHandlers != nil {
   126  		if _, ok := o.ExtraHandlers[defaultMetricsEndpoint]; ok {
   127  			return nil, fmt.Errorf("overriding builtin %s endpoint is not allowed", defaultMetricsEndpoint)
   128  		}
   129  	}
   130  
   131  	// Create the metrics filter if a FilterProvider is set.
   132  	var metricsFilter Filter
   133  	if o.FilterProvider != nil {
   134  		var err error
   135  		metricsFilter, err = o.FilterProvider(config, httpClient)
   136  		if err != nil {
   137  			return nil, fmt.Errorf("filter provider failed to create filter for the metrics server: %w", err)
   138  		}
   139  	}
   140  
   141  	return &defaultServer{
   142  		metricsFilter: metricsFilter,
   143  		options:       o,
   144  	}, nil
   145  }
   146  
   147  // defaultServer is the default implementation used for Server.
   148  type defaultServer struct {
   149  	options Options
   150  
   151  	// metricsFilter is a filter which is added around
   152  	// the metrics and the extra handlers on the metrics server.
   153  	metricsFilter Filter
   154  
   155  	// mu protects access to the bindAddr field.
   156  	mu sync.RWMutex
   157  
   158  	// bindAddr is used to store the bindAddr after the listener has been created.
   159  	// This is used during testing to figure out the port that has been chosen randomly.
   160  	bindAddr string
   161  }
   162  
   163  // setDefaults does defaulting for the Server.
   164  func (o *Options) setDefaults() {
   165  	if o.BindAddress == "" {
   166  		o.BindAddress = DefaultBindAddress
   167  	}
   168  
   169  	if len(o.CertDir) == 0 {
   170  		o.CertDir = filepath.Join(os.TempDir(), "k8s-metrics-server", "serving-certs")
   171  	}
   172  
   173  	if len(o.CertName) == 0 {
   174  		o.CertName = "tls.crt"
   175  	}
   176  
   177  	if len(o.KeyName) == 0 {
   178  		o.KeyName = "tls.key"
   179  	}
   180  }
   181  
   182  // NeedLeaderElection implements the LeaderElectionRunnable interface, which indicates
   183  // the metrics server doesn't need leader election.
   184  func (*defaultServer) NeedLeaderElection() bool {
   185  	return false
   186  }
   187  
   188  // AddExtraHandler adds extra handler served on path to the http server that serves metrics.
   189  func (s *defaultServer) AddExtraHandler(path string, handler http.Handler) error {
   190  	s.mu.Lock()
   191  	defer s.mu.Unlock()
   192  	if s.options.ExtraHandlers == nil {
   193  		s.options.ExtraHandlers = make(map[string]http.Handler)
   194  	}
   195  	if path == defaultMetricsEndpoint {
   196  		return fmt.Errorf("overriding builtin %s endpoint is not allowed", defaultMetricsEndpoint)
   197  	}
   198  	if _, found := s.options.ExtraHandlers[path]; found {
   199  		return fmt.Errorf("can't register extra handler by duplicate path %q on metrics http server", path)
   200  	}
   201  	s.options.ExtraHandlers[path] = handler
   202  	return nil
   203  }
   204  
   205  // Start runs the server.
   206  // It will install the metrics related resources depend on the server configuration.
   207  func (s *defaultServer) Start(ctx context.Context) error {
   208  	log.Info("Starting metrics server")
   209  
   210  	listener, err := s.createListener(ctx, log)
   211  	if err != nil {
   212  		return fmt.Errorf("failed to start metrics server: failed to create listener: %w", err)
   213  	}
   214  	// Storing bindAddr here so we can retrieve it during testing via GetBindAddr.
   215  	s.mu.Lock()
   216  	s.bindAddr = listener.Addr().String()
   217  	s.mu.Unlock()
   218  
   219  	mux := http.NewServeMux()
   220  
   221  	handler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{
   222  		ErrorHandling: promhttp.HTTPErrorOnError,
   223  	})
   224  	if s.metricsFilter != nil {
   225  		log := log.WithValues("path", defaultMetricsEndpoint)
   226  		var err error
   227  		handler, err = s.metricsFilter(log, handler)
   228  		if err != nil {
   229  			return fmt.Errorf("failed to start metrics server: failed to add metrics filter: %w", err)
   230  		}
   231  	}
   232  	// TODO(JoelSpeed): Use existing Kubernetes machinery for serving metrics
   233  	mux.Handle(defaultMetricsEndpoint, handler)
   234  
   235  	for path, extraHandler := range s.options.ExtraHandlers {
   236  		if s.metricsFilter != nil {
   237  			log := log.WithValues("path", path)
   238  			var err error
   239  			extraHandler, err = s.metricsFilter(log, extraHandler)
   240  			if err != nil {
   241  				return fmt.Errorf("failed to start metrics server: failed to add metrics filter to extra handler for path %s: %w", path, err)
   242  			}
   243  		}
   244  		mux.Handle(path, extraHandler)
   245  	}
   246  
   247  	log.Info("Serving metrics server", "bindAddress", s.options.BindAddress, "secure", s.options.SecureServing)
   248  
   249  	srv := httpserver.New(mux)
   250  
   251  	idleConnsClosed := make(chan struct{})
   252  	go func() {
   253  		<-ctx.Done()
   254  		log.Info("Shutting down metrics server with timeout of 1 minute")
   255  
   256  		ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
   257  		defer cancel()
   258  		if err := srv.Shutdown(ctx); err != nil {
   259  			// Error from closing listeners, or context timeout
   260  			log.Error(err, "error shutting down the HTTP server")
   261  		}
   262  		close(idleConnsClosed)
   263  	}()
   264  
   265  	if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
   266  		return err
   267  	}
   268  
   269  	<-idleConnsClosed
   270  	return nil
   271  }
   272  
   273  func (s *defaultServer) createListener(ctx context.Context, log logr.Logger) (net.Listener, error) {
   274  	if !s.options.SecureServing {
   275  		return s.options.ListenConfig.Listen(ctx, "tcp", s.options.BindAddress)
   276  	}
   277  
   278  	cfg := &tls.Config{ //nolint:gosec
   279  		NextProtos: []string{"h2"},
   280  	}
   281  	// fallback TLS config ready, will now mutate if passer wants full control over it
   282  	for _, op := range s.options.TLSOpts {
   283  		op(cfg)
   284  	}
   285  
   286  	if cfg.GetCertificate == nil {
   287  		certPath := filepath.Join(s.options.CertDir, s.options.CertName)
   288  		keyPath := filepath.Join(s.options.CertDir, s.options.KeyName)
   289  
   290  		_, certErr := os.Stat(certPath)
   291  		certExists := !os.IsNotExist(certErr)
   292  		_, keyErr := os.Stat(keyPath)
   293  		keyExists := !os.IsNotExist(keyErr)
   294  		if certExists && keyExists {
   295  			// Create the certificate watcher and
   296  			// set the config's GetCertificate on the TLSConfig
   297  			certWatcher, err := certwatcher.New(certPath, keyPath)
   298  			if err != nil {
   299  				return nil, err
   300  			}
   301  			cfg.GetCertificate = certWatcher.GetCertificate
   302  
   303  			go func() {
   304  				if err := certWatcher.Start(ctx); err != nil {
   305  					log.Error(err, "certificate watcher error")
   306  				}
   307  			}()
   308  		}
   309  	}
   310  
   311  	// If cfg.GetCertificate is still nil, i.e. we didn't configure a cert watcher, fallback to a self-signed certificate.
   312  	if cfg.GetCertificate == nil {
   313  		// Note: Using self-signed certificates here should be good enough. It's just important that we
   314  		// encrypt the communication. For example kube-controller-manager also uses a self-signed certificate
   315  		// for the metrics endpoint per default.
   316  		cert, key, err := certutil.GenerateSelfSignedCertKeyWithFixtures("localhost", []net.IP{{127, 0, 0, 1}}, nil, "")
   317  		if err != nil {
   318  			return nil, fmt.Errorf("failed to generate self-signed certificate for metrics server: %w", err)
   319  		}
   320  
   321  		keyPair, err := tls.X509KeyPair(cert, key)
   322  		if err != nil {
   323  			return nil, fmt.Errorf("failed to create self-signed key pair for metrics server: %w", err)
   324  		}
   325  		cfg.Certificates = []tls.Certificate{keyPair}
   326  	}
   327  
   328  	l, err := s.options.ListenConfig.Listen(ctx, "tcp", s.options.BindAddress)
   329  	if err != nil {
   330  		return nil, err
   331  	}
   332  
   333  	return tls.NewListener(l, cfg), nil
   334  }
   335  
   336  func (s *defaultServer) GetBindAddr() string {
   337  	s.mu.RLock()
   338  	defer s.mu.RUnlock()
   339  	return s.bindAddr
   340  }