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  }