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 }