k8s.io/apiserver@v0.31.1/pkg/endpoints/handlers/responsewriters/writers.go (about) 1 /* 2 Copyright 2016 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 responsewriters 18 19 import ( 20 "compress/gzip" 21 "context" 22 "encoding/json" 23 "fmt" 24 "io" 25 "net/http" 26 "strconv" 27 "strings" 28 "sync" 29 "time" 30 31 "go.opentelemetry.io/otel/attribute" 32 33 "k8s.io/apiserver/pkg/features" 34 35 "k8s.io/apimachinery/pkg/runtime" 36 "k8s.io/apimachinery/pkg/runtime/schema" 37 "k8s.io/apimachinery/pkg/util/httpstream/wsstream" 38 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 39 "k8s.io/apiserver/pkg/audit" 40 "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" 41 "k8s.io/apiserver/pkg/endpoints/metrics" 42 "k8s.io/apiserver/pkg/endpoints/request" 43 "k8s.io/apiserver/pkg/registry/rest" 44 utilfeature "k8s.io/apiserver/pkg/util/feature" 45 "k8s.io/apiserver/pkg/util/flushwriter" 46 "k8s.io/component-base/tracing" 47 ) 48 49 // StreamObject performs input stream negotiation from a ResourceStreamer and writes that to the response. 50 // If the client requests a websocket upgrade, negotiate for a websocket reader protocol (because many 51 // browser clients cannot easily handle binary streaming protocols). 52 func StreamObject(statusCode int, gv schema.GroupVersion, s runtime.NegotiatedSerializer, stream rest.ResourceStreamer, w http.ResponseWriter, req *http.Request) { 53 out, flush, contentType, err := stream.InputStream(req.Context(), gv.String(), req.Header.Get("Accept")) 54 if err != nil { 55 ErrorNegotiated(err, s, gv, w, req) 56 return 57 } 58 if out == nil { 59 // No output provided - return StatusNoContent 60 w.WriteHeader(http.StatusNoContent) 61 return 62 } 63 defer out.Close() 64 65 if wsstream.IsWebSocketRequest(req) { 66 r := wsstream.NewReader(out, true, wsstream.NewDefaultReaderProtocols()) 67 if err := r.Copy(w, req); err != nil { 68 utilruntime.HandleError(fmt.Errorf("error encountered while streaming results via websocket: %v", err)) 69 } 70 return 71 } 72 73 if len(contentType) == 0 { 74 contentType = "application/octet-stream" 75 } 76 w.Header().Set("Content-Type", contentType) 77 w.WriteHeader(statusCode) 78 // Flush headers, if possible 79 if flusher, ok := w.(http.Flusher); ok { 80 flusher.Flush() 81 } 82 writer := w.(io.Writer) 83 if flush { 84 writer = flushwriter.Wrap(w) 85 } 86 io.Copy(writer, out) 87 } 88 89 // SerializeObject renders an object in the content type negotiated by the client using the provided encoder. 90 // The context is optional and can be nil. This method will perform optional content compression if requested by 91 // a client and the feature gate for APIResponseCompression is enabled. 92 func SerializeObject(mediaType string, encoder runtime.Encoder, hw http.ResponseWriter, req *http.Request, statusCode int, object runtime.Object) { 93 ctx := req.Context() 94 ctx, span := tracing.Start(ctx, "SerializeObject", 95 attribute.String("audit-id", audit.GetAuditIDTruncated(ctx)), 96 attribute.String("method", req.Method), 97 attribute.String("url", req.URL.Path), 98 attribute.String("protocol", req.Proto), 99 attribute.String("mediaType", mediaType), 100 attribute.String("encoder", string(encoder.Identifier()))) 101 defer span.End(5 * time.Second) 102 103 w := &deferredResponseWriter{ 104 mediaType: mediaType, 105 statusCode: statusCode, 106 contentEncoding: negotiateContentEncoding(req), 107 hw: hw, 108 ctx: ctx, 109 } 110 111 err := encoder.Encode(object, w) 112 if err == nil { 113 err = w.Close() 114 if err != nil { 115 // we cannot write an error to the writer anymore as the Encode call was successful. 116 utilruntime.HandleError(fmt.Errorf("apiserver was unable to close cleanly the response writer: %v", err)) 117 } 118 return 119 } 120 121 // make a best effort to write the object if a failure is detected 122 utilruntime.HandleError(fmt.Errorf("apiserver was unable to write a JSON response: %v", err)) 123 status := ErrorToAPIStatus(err) 124 candidateStatusCode := int(status.Code) 125 // if the current status code is successful, allow the error's status code to overwrite it 126 if statusCode >= http.StatusOK && statusCode < http.StatusBadRequest { 127 w.statusCode = candidateStatusCode 128 } 129 output, err := runtime.Encode(encoder, status) 130 if err != nil { 131 w.mediaType = "text/plain" 132 output = []byte(fmt.Sprintf("%s: %s", status.Reason, status.Message)) 133 } 134 if _, err := w.Write(output); err != nil { 135 utilruntime.HandleError(fmt.Errorf("apiserver was unable to write a fallback JSON response: %v", err)) 136 } 137 w.Close() 138 } 139 140 var gzipPool = &sync.Pool{ 141 New: func() interface{} { 142 gw, err := gzip.NewWriterLevel(nil, defaultGzipContentEncodingLevel) 143 if err != nil { 144 panic(err) 145 } 146 return gw 147 }, 148 } 149 150 const ( 151 // defaultGzipContentEncodingLevel is set to 1 which uses least CPU compared to higher levels, yet offers 152 // similar compression ratios (off by at most 1.5x, but typically within 1.1x-1.3x). For further details see - 153 // https://github.com/kubernetes/kubernetes/issues/112296 154 defaultGzipContentEncodingLevel = 1 155 // defaultGzipThresholdBytes is compared to the size of the first write from the stream 156 // (usually the entire object), and if the size is smaller no gzipping will be performed 157 // if the client requests it. 158 defaultGzipThresholdBytes = 128 * 1024 159 ) 160 161 // negotiateContentEncoding returns a supported client-requested content encoding for the 162 // provided request. It will return the empty string if no supported content encoding was 163 // found or if response compression is disabled. 164 func negotiateContentEncoding(req *http.Request) string { 165 encoding := req.Header.Get("Accept-Encoding") 166 if len(encoding) == 0 { 167 return "" 168 } 169 if !utilfeature.DefaultFeatureGate.Enabled(features.APIResponseCompression) { 170 return "" 171 } 172 for len(encoding) > 0 { 173 var token string 174 if next := strings.Index(encoding, ","); next != -1 { 175 token = encoding[:next] 176 encoding = encoding[next+1:] 177 } else { 178 token = encoding 179 encoding = "" 180 } 181 switch strings.TrimSpace(token) { 182 case "gzip": 183 return "gzip" 184 } 185 } 186 return "" 187 } 188 189 type deferredResponseWriter struct { 190 mediaType string 191 statusCode int 192 contentEncoding string 193 194 hasWritten bool 195 hw http.ResponseWriter 196 w io.Writer 197 198 ctx context.Context 199 } 200 201 func (w *deferredResponseWriter) Write(p []byte) (n int, err error) { 202 ctx := w.ctx 203 span := tracing.SpanFromContext(ctx) 204 // This Step usually wraps in-memory object serialization. 205 span.AddEvent("About to start writing response", attribute.Int("size", len(p))) 206 207 firstWrite := !w.hasWritten 208 defer func() { 209 if err != nil { 210 span.AddEvent("Write call failed", 211 attribute.String("writer", fmt.Sprintf("%T", w.w)), 212 attribute.Int("size", len(p)), 213 attribute.Bool("firstWrite", firstWrite), 214 attribute.String("err", err.Error())) 215 } else { 216 span.AddEvent("Write call succeeded", 217 attribute.String("writer", fmt.Sprintf("%T", w.w)), 218 attribute.Int("size", len(p)), 219 attribute.Bool("firstWrite", firstWrite)) 220 } 221 }() 222 if w.hasWritten { 223 return w.w.Write(p) 224 } 225 w.hasWritten = true 226 227 hw := w.hw 228 header := hw.Header() 229 switch { 230 case w.contentEncoding == "gzip" && len(p) > defaultGzipThresholdBytes: 231 header.Set("Content-Encoding", "gzip") 232 header.Add("Vary", "Accept-Encoding") 233 234 gw := gzipPool.Get().(*gzip.Writer) 235 gw.Reset(hw) 236 237 w.w = gw 238 default: 239 w.w = hw 240 } 241 242 header.Set("Content-Type", w.mediaType) 243 hw.WriteHeader(w.statusCode) 244 return w.w.Write(p) 245 } 246 247 func (w *deferredResponseWriter) Close() error { 248 if !w.hasWritten { 249 return nil 250 } 251 var err error 252 switch t := w.w.(type) { 253 case *gzip.Writer: 254 err = t.Close() 255 t.Reset(nil) 256 gzipPool.Put(t) 257 } 258 return err 259 } 260 261 // WriteObjectNegotiated renders an object in the content type negotiated by the client. 262 func WriteObjectNegotiated(s runtime.NegotiatedSerializer, restrictions negotiation.EndpointRestrictions, gv schema.GroupVersion, w http.ResponseWriter, req *http.Request, statusCode int, object runtime.Object, listGVKInContentType bool) { 263 stream, ok := object.(rest.ResourceStreamer) 264 if ok { 265 requestInfo, _ := request.RequestInfoFrom(req.Context()) 266 metrics.RecordLongRunning(req, requestInfo, metrics.APIServerComponent, func() { 267 StreamObject(statusCode, gv, s, stream, w, req) 268 }) 269 return 270 } 271 272 mediaType, serializer, err := negotiation.NegotiateOutputMediaType(req, s, restrictions) 273 if err != nil { 274 // if original statusCode was not successful we need to return the original error 275 // we cannot hide it behind negotiation problems 276 if statusCode < http.StatusOK || statusCode >= http.StatusBadRequest { 277 WriteRawJSON(int(statusCode), object, w) 278 return 279 } 280 status := ErrorToAPIStatus(err) 281 WriteRawJSON(int(status.Code), status, w) 282 return 283 } 284 285 audit.LogResponseObject(req.Context(), object, gv, s) 286 287 encoder := s.EncoderForVersion(serializer.Serializer, gv) 288 request.TrackSerializeResponseObjectLatency(req.Context(), func() { 289 if listGVKInContentType { 290 SerializeObject(generateMediaTypeWithGVK(serializer.MediaType, mediaType.Convert), encoder, w, req, statusCode, object) 291 } else { 292 SerializeObject(serializer.MediaType, encoder, w, req, statusCode, object) 293 } 294 }) 295 } 296 297 func generateMediaTypeWithGVK(mediaType string, gvk *schema.GroupVersionKind) string { 298 if gvk == nil { 299 return mediaType 300 } 301 if gvk.Group != "" { 302 mediaType += ";g=" + gvk.Group 303 } 304 if gvk.Version != "" { 305 mediaType += ";v=" + gvk.Version 306 } 307 if gvk.Kind != "" { 308 mediaType += ";as=" + gvk.Kind 309 } 310 return mediaType 311 } 312 313 // ErrorNegotiated renders an error to the response. Returns the HTTP status code of the error. 314 // The context is optional and may be nil. 315 func ErrorNegotiated(err error, s runtime.NegotiatedSerializer, gv schema.GroupVersion, w http.ResponseWriter, req *http.Request) int { 316 status := ErrorToAPIStatus(err) 317 code := int(status.Code) 318 // when writing an error, check to see if the status indicates a retry after period 319 if status.Details != nil && status.Details.RetryAfterSeconds > 0 { 320 delay := strconv.Itoa(int(status.Details.RetryAfterSeconds)) 321 w.Header().Set("Retry-After", delay) 322 } 323 324 if code == http.StatusNoContent { 325 w.WriteHeader(code) 326 return code 327 } 328 329 WriteObjectNegotiated(s, negotiation.DefaultEndpointRestrictions, gv, w, req, code, status, false) 330 return code 331 } 332 333 // WriteRawJSON writes a non-API object in JSON. 334 func WriteRawJSON(statusCode int, object interface{}, w http.ResponseWriter) { 335 output, err := json.MarshalIndent(object, "", " ") 336 if err != nil { 337 http.Error(w, err.Error(), http.StatusInternalServerError) 338 return 339 } 340 w.Header().Set("Content-Type", "application/json") 341 w.WriteHeader(statusCode) 342 w.Write(output) 343 }