k8s.io/apiserver@v0.31.1/pkg/server/filters/with_retry_after.go (about)

     1  /*
     2  Copyright 2021 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 filters
    18  
    19  import (
    20  	"net/http"
    21  	"strings"
    22  )
    23  
    24  var (
    25  	// health probes and metrics scraping are never rejected, we will continue
    26  	// serving these requests after shutdown delay duration elapses.
    27  	pathPrefixesExemptFromRetryAfter = []string{
    28  		"/readyz",
    29  		"/livez",
    30  		"/healthz",
    31  		"/metrics",
    32  	}
    33  )
    34  
    35  // isRequestExemptFunc returns true if the request should not be rejected,
    36  // with a Retry-After response, otherwise it returns false.
    37  type isRequestExemptFunc func(*http.Request) bool
    38  
    39  // retryAfterParams dictates how the Retry-After response is constructed
    40  type retryAfterParams struct {
    41  	// TearDownConnection is true when we should send a 'Connection: close'
    42  	// header in the response so net/http can tear down the TCP connection.
    43  	TearDownConnection bool
    44  
    45  	// Message describes why Retry-After response has been sent by the server
    46  	Message string
    47  }
    48  
    49  // shouldRespondWithRetryAfterFunc returns true if the requests should
    50  // be rejected with a Retry-After response once certain conditions are met.
    51  // The retryAfterParams returned contains instructions on how to
    52  // construct the Retry-After response.
    53  type shouldRespondWithRetryAfterFunc func() (*retryAfterParams, bool)
    54  
    55  // WithRetryAfter rejects any incoming new request(s) with a 429
    56  // if the specified shutdownDelayDurationElapsedFn channel is closed
    57  //
    58  // It includes new request(s) on a new or an existing TCP connection
    59  // Any new request(s) arriving after shutdownDelayDurationElapsedFn is closed
    60  // are replied with a 429 and the following response headers:
    61  //   - 'Retry-After: N` (so client can retry after N seconds, hopefully on a new apiserver instance)
    62  //   - 'Connection: close': tear down the TCP connection
    63  //
    64  // TODO: is there a way to merge WithWaitGroup and this filter?
    65  func WithRetryAfter(handler http.Handler, shutdownDelayDurationElapsedCh <-chan struct{}) http.Handler {
    66  	shutdownRetryAfterParams := &retryAfterParams{
    67  		TearDownConnection: true,
    68  		Message:            "The apiserver is shutting down, please try again later.",
    69  	}
    70  
    71  	// NOTE: both WithRetryAfter and WithWaitGroup must use the same exact isRequestExemptFunc 'isRequestExemptFromRetryAfter,
    72  	// otherwise SafeWaitGroup might wait indefinitely and will prevent the server from shutting down gracefully.
    73  	return withRetryAfter(handler, isRequestExemptFromRetryAfter, func() (*retryAfterParams, bool) {
    74  		select {
    75  		case <-shutdownDelayDurationElapsedCh:
    76  			return shutdownRetryAfterParams, true
    77  		default:
    78  			return nil, false
    79  		}
    80  	})
    81  }
    82  
    83  func withRetryAfter(handler http.Handler, isRequestExemptFn isRequestExemptFunc, shouldRespondWithRetryAfterFn shouldRespondWithRetryAfterFunc) http.Handler {
    84  	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
    85  		params, send := shouldRespondWithRetryAfterFn()
    86  		if !send || isRequestExemptFn(req) {
    87  			handler.ServeHTTP(w, req)
    88  			return
    89  		}
    90  
    91  		// If we are here this means it's time to send Retry-After response
    92  		//
    93  		// Copied from net/http2 library
    94  		// "Connection" headers aren't allowed in HTTP/2 (RFC 7540, 8.1.2.2),
    95  		// but respect "Connection" == "close" to mean sending a GOAWAY and tearing
    96  		// down the TCP connection when idle, like we do for HTTP/1.
    97  		if params.TearDownConnection {
    98  			w.Header().Set("Connection", "close")
    99  		}
   100  
   101  		// Return a 429 status asking the client to try again after 5 seconds
   102  		w.Header().Set("Retry-After", "5")
   103  		http.Error(w, params.Message, http.StatusTooManyRequests)
   104  	})
   105  }
   106  
   107  // isRequestExemptFromRetryAfter returns true if the given request should be exempt
   108  // from being rejected with a 'Retry-After' response.
   109  // NOTE: both 'WithRetryAfter' and 'WithWaitGroup' filters should use this function
   110  // to exempt the set of requests from being rejected or tracked.
   111  func isRequestExemptFromRetryAfter(r *http.Request) bool {
   112  	return isKubeApiserverUserAgent(r) || hasExemptPathPrefix(r)
   113  }
   114  
   115  // isKubeApiserverUserAgent returns true if the user-agent matches
   116  // the one set by the local loopback.
   117  // NOTE: we can't look up the authenticated user informaion from the
   118  // request context since the authentication filter has not executed yet.
   119  func isKubeApiserverUserAgent(req *http.Request) bool {
   120  	return strings.HasPrefix(req.UserAgent(), "kube-apiserver/")
   121  }
   122  
   123  func hasExemptPathPrefix(r *http.Request) bool {
   124  	for _, whiteListedPrefix := range pathPrefixesExemptFromRetryAfter {
   125  		if strings.HasPrefix(r.URL.Path, whiteListedPrefix) {
   126  			return true
   127  		}
   128  	}
   129  	return false
   130  }