Skip to content

Commit 20caad9

Browse files
committed
UPSTREAM: 115328: annotate early and late requests
1 parent 85f0f2c commit 20caad9

File tree

5 files changed

+738
-12
lines changed

5 files changed

+738
-12
lines changed

staging/src/k8s.io/apiserver/pkg/server/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,9 @@ func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
958958
handler = genericapifilters.WithAudit(handler, c.AuditBackend, c.AuditPolicyRuleEvaluator, c.LongRunningFunc)
959959
handler = filterlatency.TrackStarted(handler, c.TracerProvider, "audit")
960960

961+
handler = genericfilters.WithShutdownLateAnnotation(handler, c.lifecycleSignals.ShutdownInitiated, c.ShutdownDelayDuration)
962+
handler = genericfilters.WithStartupEarlyAnnotation(handler, c.lifecycleSignals.HasBeenReady)
963+
961964
failedHandler := genericapifilters.Unauthorized(c.Serializer)
962965
failedHandler = genericapifilters.WithFailedAuthenticationAudit(failedHandler, c.AuditBackend, c.AuditPolicyRuleEvaluator)
963966

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
Copyright 2023 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+
"fmt"
21+
"net"
22+
"net/http"
23+
"strings"
24+
"time"
25+
26+
"k8s.io/apiserver/pkg/audit"
27+
"k8s.io/apiserver/pkg/authentication/user"
28+
"k8s.io/apiserver/pkg/endpoints/request"
29+
clockutils "k8s.io/utils/clock"
30+
netutils "k8s.io/utils/net"
31+
)
32+
33+
type lifecycleEvent interface {
34+
// Name returns the name of the signal, useful for logging.
35+
Name() string
36+
37+
// Signaled returns a channel that is closed when the underlying event
38+
// has been signaled. Successive calls to Signaled return the same value.
39+
Signaled() <-chan struct{}
40+
41+
// SignaledAt returns the time the event was signaled. If SignaledAt is
42+
// invoked before the event is signaled nil will be returned.
43+
SignaledAt() *time.Time
44+
}
45+
46+
type shouldExemptFunc func(*http.Request) bool
47+
48+
var (
49+
// the health probes are not annotated by default
50+
healthProbes = []string{
51+
"/readyz",
52+
"/healthz",
53+
"/livez",
54+
}
55+
)
56+
57+
func exemptIfHealthProbe(r *http.Request) bool {
58+
path := "/" + strings.TrimLeft(r.URL.Path, "/")
59+
for _, probe := range healthProbes {
60+
if path == probe {
61+
return true
62+
}
63+
}
64+
return false
65+
}
66+
67+
// WithShutdownLateAnnotation, if added to the handler chain, tracks the
68+
// incoming request(s) after the apiserver has initiated the graceful
69+
// shutdown, and annoates the audit event for these request(s) with
70+
// diagnostic information.
71+
// This enables us to identify the actor(s)/load balancer(s) that are sending
72+
// requests to the apiserver late during the server termination.
73+
// It should be placed after (in order of execution) the
74+
// 'WithAuthentication' filter.
75+
func WithShutdownLateAnnotation(handler http.Handler, shutdownInitiated lifecycleEvent, delayDuration time.Duration) http.Handler {
76+
return withShutdownLateAnnotation(handler, shutdownInitiated, delayDuration, exemptIfHealthProbe, clockutils.RealClock{})
77+
}
78+
79+
// WithStartupEarlyAnnotation annotates the request with an annotation keyed as
80+
// 'apiserver.k8s.io/startup' if the request arrives early (the server is not
81+
// fully initialized yet). It should be placed after (in order of execution)
82+
// the 'WithAuthentication' filter.
83+
func WithStartupEarlyAnnotation(handler http.Handler, hasBeenReady lifecycleEvent) http.Handler {
84+
return withStartupEarlyAnnotation(handler, hasBeenReady, exemptIfHealthProbe)
85+
}
86+
87+
func withShutdownLateAnnotation(handler http.Handler, shutdownInitiated lifecycleEvent, delayDuration time.Duration, shouldExemptFn shouldExemptFunc, clock clockutils.PassiveClock) http.Handler {
88+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
89+
select {
90+
case <-shutdownInitiated.Signaled():
91+
default:
92+
handler.ServeHTTP(w, req)
93+
return
94+
}
95+
96+
if shouldExemptFn(req) {
97+
handler.ServeHTTP(w, req)
98+
return
99+
}
100+
shutdownInitiatedAt := shutdownInitiated.SignaledAt()
101+
if shutdownInitiatedAt == nil {
102+
handler.ServeHTTP(w, req)
103+
return
104+
}
105+
106+
elapsedSince := clock.Since(*shutdownInitiatedAt)
107+
// TODO: 80% is the threshold, if requests arrive after 80% of
108+
// shutdown-delay-duration elapses we annotate the request as late=true.
109+
late := lateMsg(delayDuration, elapsedSince, 80)
110+
111+
// NOTE: some upstream unit tests have authentication disabled and will
112+
// fail if we require the requestor to be present in the request
113+
// context. Fixing those unit tests will increase the chance of merge
114+
// conflict during rebase.
115+
// This also implies that this filter must be placed after (in order of
116+
// execution) the 'WithAuthentication' filter.
117+
self := "self="
118+
if requestor, exists := request.UserFrom(req.Context()); exists && requestor != nil {
119+
self = fmt.Sprintf("%s%t", self, requestor.GetName() == user.APIServerUser)
120+
}
121+
122+
audit.AddAuditAnnotation(req.Context(), "apiserver.k8s.io/shutdown",
123+
fmt.Sprintf("%s %s loopback=%t", late, self, isLoopback(req.RemoteAddr)))
124+
125+
handler.ServeHTTP(w, req)
126+
})
127+
}
128+
129+
func lateMsg(delayDuration, elapsedSince time.Duration, threshold float64) string {
130+
if delayDuration == time.Duration(0) {
131+
return fmt.Sprintf("elapsed=%s threshold= late=%t", elapsedSince.Round(time.Second).String(), true)
132+
}
133+
134+
percentElapsed := (float64(elapsedSince) / float64(delayDuration)) * 100
135+
return fmt.Sprintf("elapsed=%s threshold=%.2f%% late=%t",
136+
elapsedSince.Round(time.Second).String(), percentElapsed, percentElapsed > threshold)
137+
}
138+
139+
func withStartupEarlyAnnotation(handler http.Handler, hasBeenReady lifecycleEvent, shouldExemptFn shouldExemptFunc) http.Handler {
140+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
141+
select {
142+
case <-hasBeenReady.Signaled():
143+
handler.ServeHTTP(w, req)
144+
return
145+
default:
146+
}
147+
148+
// NOTE: some upstream unit tests have authentication disabled and will
149+
// fail if we require the requestor to be present in the request
150+
// context. Fixing those unit tests will increase the chance of merge
151+
// conflict during rebase.
152+
// This also implies that this filter must be placed after (in order of
153+
// execution) the 'WithAuthentication' filter.
154+
self := "self="
155+
if requestor, exists := request.UserFrom(req.Context()); exists && requestor != nil {
156+
if requestor.GetName() == user.APIServerUser {
157+
handler.ServeHTTP(w, req)
158+
return
159+
}
160+
self = fmt.Sprintf("%s%t", self, false)
161+
}
162+
163+
audit.AddAuditAnnotation(req.Context(), "apiserver.k8s.io/startup",
164+
fmt.Sprintf("early=true %s loopback=%t", self, isLoopback(req.RemoteAddr)))
165+
166+
handler.ServeHTTP(w, req)
167+
})
168+
}
169+
170+
func isLoopback(address string) bool {
171+
host, _, err := net.SplitHostPort(address)
172+
if err != nil {
173+
// if the address is missing a port, SplitHostPort will return an error
174+
// with an empty host, and port value. For such an error, we should
175+
// continue and try to parse the original address.
176+
host = address
177+
}
178+
if ip := netutils.ParseIPSloppy(host); ip != nil {
179+
return ip.IsLoopback()
180+
}
181+
182+
return false
183+
}

0 commit comments

Comments
 (0)