k8s.io/apiserver@v0.31.1/pkg/endpoints/handlers/watch.go (about)

     1  /*
     2  Copyright 2014 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 handlers
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"time"
    25  
    26  	"golang.org/x/net/websocket"
    27  
    28  	"k8s.io/apimachinery/pkg/api/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
    32  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    33  	"k8s.io/apimachinery/pkg/watch"
    34  	"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
    35  	"k8s.io/apiserver/pkg/endpoints/metrics"
    36  	apirequest "k8s.io/apiserver/pkg/endpoints/request"
    37  	"k8s.io/apiserver/pkg/features"
    38  	"k8s.io/apiserver/pkg/storage"
    39  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    40  )
    41  
    42  // nothing will ever be sent down this channel
    43  var neverExitWatch <-chan time.Time = make(chan time.Time)
    44  
    45  // timeoutFactory abstracts watch timeout logic for testing
    46  type TimeoutFactory interface {
    47  	TimeoutCh() (<-chan time.Time, func() bool)
    48  }
    49  
    50  // realTimeoutFactory implements timeoutFactory
    51  type realTimeoutFactory struct {
    52  	timeout time.Duration
    53  }
    54  
    55  // TimeoutCh returns a channel which will receive something when the watch times out,
    56  // and a cleanup function to call when this happens.
    57  func (w *realTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) {
    58  	if w.timeout == 0 {
    59  		return neverExitWatch, func() bool { return false }
    60  	}
    61  	t := time.NewTimer(w.timeout)
    62  	return t.C, t.Stop
    63  }
    64  
    65  // serveWatchHandler returns a handle to serve a watch response.
    66  // TODO: the functionality in this method and in WatchServer.Serve is not cleanly decoupled.
    67  func serveWatchHandler(watcher watch.Interface, scope *RequestScope, mediaTypeOptions negotiation.MediaTypeOptions, req *http.Request, w http.ResponseWriter, timeout time.Duration, metricsScope string) (http.Handler, error) {
    68  	options, err := optionsForTransform(mediaTypeOptions, req)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	// negotiate for the stream serializer from the scope's serializer
    74  	serializer, err := negotiation.NegotiateOutputMediaTypeStream(req, scope.Serializer, scope)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	framer := serializer.StreamSerializer.Framer
    79  	streamSerializer := serializer.StreamSerializer.Serializer
    80  	encoder := scope.Serializer.EncoderForVersion(streamSerializer, scope.Kind.GroupVersion())
    81  	useTextFraming := serializer.EncodesAsText
    82  	if framer == nil {
    83  		return nil, fmt.Errorf("no framer defined for %q available for embedded encoding", serializer.MediaType)
    84  	}
    85  	// TODO: next step, get back mediaTypeOptions from negotiate and return the exact value here
    86  	mediaType := serializer.MediaType
    87  	if mediaType != runtime.ContentTypeJSON {
    88  		mediaType += ";stream=watch"
    89  	}
    90  
    91  	ctx := req.Context()
    92  
    93  	// locate the appropriate embedded encoder based on the transform
    94  	var embeddedEncoder runtime.Encoder
    95  	contentKind, contentSerializer, transform := targetEncodingForTransform(scope, mediaTypeOptions, req)
    96  	if transform {
    97  		info, ok := runtime.SerializerInfoForMediaType(contentSerializer.SupportedMediaTypes(), serializer.MediaType)
    98  		if !ok {
    99  			return nil, fmt.Errorf("no encoder for %q exists in the requested target %#v", serializer.MediaType, contentSerializer)
   100  		}
   101  		embeddedEncoder = contentSerializer.EncoderForVersion(info.Serializer, contentKind.GroupVersion())
   102  	} else {
   103  		embeddedEncoder = scope.Serializer.EncoderForVersion(serializer.Serializer, contentKind.GroupVersion())
   104  	}
   105  
   106  	var memoryAllocator runtime.MemoryAllocator
   107  
   108  	if encoderWithAllocator, supportsAllocator := embeddedEncoder.(runtime.EncoderWithAllocator); supportsAllocator {
   109  		// don't put the allocator inside the embeddedEncodeFn as that would allocate memory on every call.
   110  		// instead, we allocate the buffer for the entire watch session and release it when we close the connection.
   111  		memoryAllocator = runtime.AllocatorPool.Get().(*runtime.Allocator)
   112  		embeddedEncoder = runtime.NewEncoderWithAllocator(encoderWithAllocator, memoryAllocator)
   113  	}
   114  	var tableOptions *metav1.TableOptions
   115  	if options != nil {
   116  		if passedOptions, ok := options.(*metav1.TableOptions); ok {
   117  			tableOptions = passedOptions
   118  		} else {
   119  			return nil, fmt.Errorf("unexpected options type: %T", options)
   120  		}
   121  	}
   122  	embeddedEncoder = newWatchEmbeddedEncoder(ctx, embeddedEncoder, mediaTypeOptions.Convert, tableOptions, scope)
   123  
   124  	if encoderWithAllocator, supportsAllocator := encoder.(runtime.EncoderWithAllocator); supportsAllocator {
   125  		if memoryAllocator == nil {
   126  			// don't put the allocator inside the embeddedEncodeFn as that would allocate memory on every call.
   127  			// instead, we allocate the buffer for the entire watch session and release it when we close the connection.
   128  			memoryAllocator = runtime.AllocatorPool.Get().(*runtime.Allocator)
   129  		}
   130  		encoder = runtime.NewEncoderWithAllocator(encoderWithAllocator, memoryAllocator)
   131  	}
   132  
   133  	var serverShuttingDownCh <-chan struct{}
   134  	if signals := apirequest.ServerShutdownSignalFrom(req.Context()); signals != nil {
   135  		serverShuttingDownCh = signals.ShuttingDown()
   136  	}
   137  
   138  	server := &WatchServer{
   139  		Watching: watcher,
   140  		Scope:    scope,
   141  
   142  		UseTextFraming:  useTextFraming,
   143  		MediaType:       mediaType,
   144  		Framer:          framer,
   145  		Encoder:         encoder,
   146  		EmbeddedEncoder: embeddedEncoder,
   147  
   148  		MemoryAllocator:      memoryAllocator,
   149  		TimeoutFactory:       &realTimeoutFactory{timeout},
   150  		ServerShuttingDownCh: serverShuttingDownCh,
   151  
   152  		metricsScope: metricsScope,
   153  	}
   154  
   155  	if wsstream.IsWebSocketRequest(req) {
   156  		w.Header().Set("Content-Type", server.MediaType)
   157  		return websocket.Handler(server.HandleWS), nil
   158  	}
   159  	return http.HandlerFunc(server.HandleHTTP), nil
   160  }
   161  
   162  // WatchServer serves a watch.Interface over a websocket or vanilla HTTP.
   163  type WatchServer struct {
   164  	Watching watch.Interface
   165  	Scope    *RequestScope
   166  
   167  	// true if websocket messages should use text framing (as opposed to binary framing)
   168  	UseTextFraming bool
   169  	// the media type this watch is being served with
   170  	MediaType string
   171  	// used to frame the watch stream
   172  	Framer runtime.Framer
   173  	// used to encode the watch stream event itself
   174  	Encoder runtime.Encoder
   175  	// used to encode the nested object in the watch stream
   176  	EmbeddedEncoder runtime.Encoder
   177  
   178  	MemoryAllocator      runtime.MemoryAllocator
   179  	TimeoutFactory       TimeoutFactory
   180  	ServerShuttingDownCh <-chan struct{}
   181  
   182  	metricsScope string
   183  }
   184  
   185  // HandleHTTP serves a series of encoded events via HTTP with Transfer-Encoding: chunked.
   186  // or over a websocket connection.
   187  func (s *WatchServer) HandleHTTP(w http.ResponseWriter, req *http.Request) {
   188  	defer func() {
   189  		if s.MemoryAllocator != nil {
   190  			runtime.AllocatorPool.Put(s.MemoryAllocator)
   191  		}
   192  	}()
   193  
   194  	flusher, ok := w.(http.Flusher)
   195  	if !ok {
   196  		err := fmt.Errorf("unable to start watch - can't get http.Flusher: %#v", w)
   197  		utilruntime.HandleError(err)
   198  		s.Scope.err(errors.NewInternalError(err), w, req)
   199  		return
   200  	}
   201  
   202  	framer := s.Framer.NewFrameWriter(w)
   203  	if framer == nil {
   204  		// programmer error
   205  		err := fmt.Errorf("no stream framing support is available for media type %q", s.MediaType)
   206  		utilruntime.HandleError(err)
   207  		s.Scope.err(errors.NewBadRequest(err.Error()), w, req)
   208  		return
   209  	}
   210  
   211  	// ensure the connection times out
   212  	timeoutCh, cleanup := s.TimeoutFactory.TimeoutCh()
   213  	defer cleanup()
   214  
   215  	// begin the stream
   216  	w.Header().Set("Content-Type", s.MediaType)
   217  	w.Header().Set("Transfer-Encoding", "chunked")
   218  	w.WriteHeader(http.StatusOK)
   219  	flusher.Flush()
   220  
   221  	kind := s.Scope.Kind
   222  	watchEncoder := newWatchEncoder(req.Context(), kind, s.EmbeddedEncoder, s.Encoder, framer)
   223  	ch := s.Watching.ResultChan()
   224  	done := req.Context().Done()
   225  
   226  	for {
   227  		select {
   228  		case <-s.ServerShuttingDownCh:
   229  			// the server has signaled that it is shutting down (not accepting
   230  			// any new request), all active watch request(s) should return
   231  			// immediately here. The WithWatchTerminationDuringShutdown server
   232  			// filter will ensure that the response to the client is rate
   233  			// limited in order to avoid any thundering herd issue when the
   234  			// client(s) try to reestablish the WATCH on the other
   235  			// available apiserver instance(s).
   236  			return
   237  		case <-done:
   238  			return
   239  		case <-timeoutCh:
   240  			return
   241  		case event, ok := <-ch:
   242  			if !ok {
   243  				// End of results.
   244  				return
   245  			}
   246  			metrics.WatchEvents.WithContext(req.Context()).WithLabelValues(kind.Group, kind.Version, kind.Kind).Inc()
   247  			isWatchListLatencyRecordingRequired := shouldRecordWatchListLatency(event)
   248  
   249  			if err := watchEncoder.Encode(event); err != nil {
   250  				utilruntime.HandleError(err)
   251  				// client disconnect.
   252  				return
   253  			}
   254  
   255  			if len(ch) == 0 {
   256  				flusher.Flush()
   257  			}
   258  			if isWatchListLatencyRecordingRequired {
   259  				metrics.RecordWatchListLatency(req.Context(), s.Scope.Resource, s.metricsScope)
   260  			}
   261  		}
   262  	}
   263  }
   264  
   265  // HandleWS serves a series of encoded events over a websocket connection.
   266  func (s *WatchServer) HandleWS(ws *websocket.Conn) {
   267  	defer func() {
   268  		if s.MemoryAllocator != nil {
   269  			runtime.AllocatorPool.Put(s.MemoryAllocator)
   270  		}
   271  	}()
   272  
   273  	defer ws.Close()
   274  	done := make(chan struct{})
   275  	// ensure the connection times out
   276  	timeoutCh, cleanup := s.TimeoutFactory.TimeoutCh()
   277  	defer cleanup()
   278  
   279  	go func() {
   280  		defer utilruntime.HandleCrash()
   281  		// This blocks until the connection is closed.
   282  		// Client should not send anything.
   283  		wsstream.IgnoreReceives(ws, 0)
   284  		// Once the client closes, we should also close
   285  		close(done)
   286  	}()
   287  
   288  	framer := newWebsocketFramer(ws, s.UseTextFraming)
   289  
   290  	kind := s.Scope.Kind
   291  	watchEncoder := newWatchEncoder(context.TODO(), kind, s.EmbeddedEncoder, s.Encoder, framer)
   292  	ch := s.Watching.ResultChan()
   293  
   294  	for {
   295  		select {
   296  		case <-done:
   297  			return
   298  		case <-timeoutCh:
   299  			return
   300  		case event, ok := <-ch:
   301  			if !ok {
   302  				// End of results.
   303  				return
   304  			}
   305  
   306  			if err := watchEncoder.Encode(event); err != nil {
   307  				utilruntime.HandleError(err)
   308  				// client disconnect.
   309  				return
   310  			}
   311  		}
   312  	}
   313  }
   314  
   315  type websocketFramer struct {
   316  	ws             *websocket.Conn
   317  	useTextFraming bool
   318  }
   319  
   320  func newWebsocketFramer(ws *websocket.Conn, useTextFraming bool) io.Writer {
   321  	return &websocketFramer{
   322  		ws:             ws,
   323  		useTextFraming: useTextFraming,
   324  	}
   325  }
   326  
   327  func (w *websocketFramer) Write(p []byte) (int, error) {
   328  	if w.useTextFraming {
   329  		// bytes.Buffer::String() has a special handling of nil value, but given
   330  		// we're writing serialized watch events, this will never happen here.
   331  		if err := websocket.Message.Send(w.ws, string(p)); err != nil {
   332  			return 0, err
   333  		}
   334  		return len(p), nil
   335  	}
   336  	if err := websocket.Message.Send(w.ws, p); err != nil {
   337  		return 0, err
   338  	}
   339  	return len(p), nil
   340  }
   341  
   342  var _ io.Writer = &websocketFramer{}
   343  
   344  func shouldRecordWatchListLatency(event watch.Event) bool {
   345  	if event.Type != watch.Bookmark || !utilfeature.DefaultFeatureGate.Enabled(features.WatchList) {
   346  		return false
   347  	}
   348  	// as of today the initial-events-end annotation is added only to a single event
   349  	// by the watch cache and only when certain conditions are met
   350  	//
   351  	// for more please read https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/3157-watch-list
   352  	hasAnnotation, err := storage.HasInitialEventsEndBookmarkAnnotation(event.Object)
   353  	if err != nil {
   354  		utilruntime.HandleError(fmt.Errorf("unable to determine if the obj has the required annotation for measuring watchlist latency, obj %T: %v", event.Object, err))
   355  		return false
   356  	}
   357  	return hasAnnotation
   358  }