k8s.io/apiserver@v0.31.1/pkg/endpoints/handlers/response.go (about) 1 /* 2 Copyright 2017 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 handlers 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 "reflect" 26 27 "k8s.io/apimachinery/pkg/api/errors" 28 "k8s.io/apimachinery/pkg/api/meta" 29 metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" 32 "k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation" 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/runtime/schema" 35 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 36 "k8s.io/apimachinery/pkg/watch" 37 "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" 38 "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" 39 "k8s.io/apiserver/pkg/endpoints/metrics" 40 endpointsrequest "k8s.io/apiserver/pkg/endpoints/request" 41 42 klog "k8s.io/klog/v2" 43 ) 44 45 // watchEmbeddedEncoder performs encoding of the embedded object. 46 // 47 // NOTE: watchEmbeddedEncoder is NOT thread-safe. 48 type watchEmbeddedEncoder struct { 49 encoder runtime.Encoder 50 51 ctx context.Context 52 53 // target, if non-nil, configures transformation type. 54 // The other options are ignored if target is nil. 55 target *schema.GroupVersionKind 56 tableOptions *metav1.TableOptions 57 scope *RequestScope 58 59 // identifier of the encoder, computed lazily 60 identifier runtime.Identifier 61 } 62 63 func newWatchEmbeddedEncoder(ctx context.Context, encoder runtime.Encoder, target *schema.GroupVersionKind, tableOptions *metav1.TableOptions, scope *RequestScope) *watchEmbeddedEncoder { 64 return &watchEmbeddedEncoder{ 65 encoder: encoder, 66 ctx: ctx, 67 target: target, 68 tableOptions: tableOptions, 69 scope: scope, 70 } 71 } 72 73 // Encode implements runtime.Encoder interface. 74 func (e *watchEmbeddedEncoder) Encode(obj runtime.Object, w io.Writer) error { 75 if co, ok := obj.(runtime.CacheableObject); ok { 76 return co.CacheEncode(e.Identifier(), e.doEncode, w) 77 } 78 return e.doEncode(obj, w) 79 } 80 81 func (e *watchEmbeddedEncoder) doEncode(obj runtime.Object, w io.Writer) error { 82 result, err := doTransformObject(e.ctx, obj, e.tableOptions, e.target, e.scope) 83 if err != nil { 84 utilruntime.HandleError(fmt.Errorf("failed to transform object %v: %v", reflect.TypeOf(obj), err)) 85 result = obj 86 } 87 88 // When we are tranforming to a table, use the original table options when 89 // we should print headers only on the first object - headers should be 90 // omitted on subsequent events. 91 if e.tableOptions != nil && !e.tableOptions.NoHeaders { 92 e.tableOptions.NoHeaders = true 93 // With options change, we should recompute the identifier. 94 // Clearing this will trigger lazy recompute when needed. 95 e.identifier = "" 96 } 97 98 return e.encoder.Encode(result, w) 99 } 100 101 // Identifier implements runtime.Encoder interface. 102 func (e *watchEmbeddedEncoder) Identifier() runtime.Identifier { 103 if e.identifier == "" { 104 e.identifier = e.embeddedIdentifier() 105 } 106 return e.identifier 107 } 108 109 type watchEmbeddedEncoderIdentifier struct { 110 Name string `json:"name,omitempty"` 111 Encoder string `json:"encoder,omitempty"` 112 Target string `json:"target,omitempty"` 113 Options metav1.TableOptions `json:"options,omitempty"` 114 NoHeaders bool `json:"noHeaders,omitempty"` 115 } 116 117 func (e *watchEmbeddedEncoder) embeddedIdentifier() runtime.Identifier { 118 if e.target == nil { 119 // If no conversion is performed, we effective only use 120 // the embedded identifier. 121 return e.encoder.Identifier() 122 } 123 identifier := watchEmbeddedEncoderIdentifier{ 124 Name: "watch-embedded", 125 Encoder: string(e.encoder.Identifier()), 126 Target: e.target.String(), 127 } 128 if e.target.Kind == "Table" && e.tableOptions != nil { 129 identifier.Options = *e.tableOptions 130 identifier.NoHeaders = e.tableOptions.NoHeaders 131 } 132 133 result, err := json.Marshal(identifier) 134 if err != nil { 135 klog.Fatalf("Failed marshaling identifier for watchEmbeddedEncoder: %v", err) 136 } 137 return runtime.Identifier(result) 138 } 139 140 // watchEncoder performs encoding of the watch events. 141 // 142 // NOTE: watchEncoder is NOT thread-safe. 143 type watchEncoder struct { 144 ctx context.Context 145 kind schema.GroupVersionKind 146 embeddedEncoder runtime.Encoder 147 encoder runtime.Encoder 148 framer io.Writer 149 150 buffer runtime.Splice 151 eventBuffer runtime.Splice 152 153 currentEmbeddedIdentifier runtime.Identifier 154 identifiers map[watch.EventType]runtime.Identifier 155 } 156 157 func newWatchEncoder(ctx context.Context, kind schema.GroupVersionKind, embeddedEncoder runtime.Encoder, encoder runtime.Encoder, framer io.Writer) *watchEncoder { 158 return &watchEncoder{ 159 ctx: ctx, 160 kind: kind, 161 embeddedEncoder: embeddedEncoder, 162 encoder: encoder, 163 framer: framer, 164 buffer: runtime.NewSpliceBuffer(), 165 eventBuffer: runtime.NewSpliceBuffer(), 166 } 167 } 168 169 // Encode encodes a given watch event. 170 // NOTE: if events object is implementing the CacheableObject interface, 171 // 172 // the serialized version is cached in that object [not the event itself]. 173 func (e *watchEncoder) Encode(event watch.Event) error { 174 encodeFunc := func(obj runtime.Object, w io.Writer) error { 175 return e.doEncode(obj, event, w) 176 } 177 if co, ok := event.Object.(runtime.CacheableObject); ok { 178 return co.CacheEncode(e.identifier(event.Type), encodeFunc, e.framer) 179 } 180 return encodeFunc(event.Object, e.framer) 181 } 182 183 func (e *watchEncoder) doEncode(obj runtime.Object, event watch.Event, w io.Writer) error { 184 defer e.buffer.Reset() 185 186 if err := e.embeddedEncoder.Encode(obj, e.buffer); err != nil { 187 return fmt.Errorf("unable to encode watch object %T: %v", obj, err) 188 } 189 190 // ContentType is not required here because we are defaulting to the serializer type. 191 outEvent := &metav1.WatchEvent{ 192 Type: string(event.Type), 193 Object: runtime.RawExtension{Raw: e.buffer.Bytes()}, 194 } 195 metrics.WatchEventsSizes.WithContext(e.ctx).WithLabelValues(e.kind.Group, e.kind.Version, e.kind.Kind).Observe(float64(len(outEvent.Object.Raw))) 196 197 defer e.eventBuffer.Reset() 198 if err := e.encoder.Encode(outEvent, e.eventBuffer); err != nil { 199 return fmt.Errorf("unable to encode watch object %T: %v (%#v)", outEvent, err, e) 200 } 201 202 _, err := w.Write(e.eventBuffer.Bytes()) 203 return err 204 } 205 206 type watchEncoderIdentifier struct { 207 Name string `json:"name,omitempty"` 208 EmbeddedEncoder string `json:"embeddedEncoder,omitempty"` 209 Encoder string `json:"encoder,omitempty"` 210 EventType string `json:"eventType,omitempty"` 211 } 212 213 func (e *watchEncoder) identifier(eventType watch.EventType) runtime.Identifier { 214 // We need to take into account that in embeddedEncoder includes table 215 // transformer, then its identifier is dynamic. As a result, whenever 216 // the identifier of embeddedEncoder changes, we need to invalidate the 217 // whole identifiers cache. 218 // TODO(wojtek-t): Can we optimize it somehow? 219 if e.currentEmbeddedIdentifier != e.embeddedEncoder.Identifier() { 220 e.currentEmbeddedIdentifier = e.embeddedEncoder.Identifier() 221 e.identifiers = map[watch.EventType]runtime.Identifier{} 222 } 223 if _, ok := e.identifiers[eventType]; !ok { 224 e.identifiers[eventType] = e.typeIdentifier(eventType) 225 } 226 return e.identifiers[eventType] 227 } 228 229 func (e *watchEncoder) typeIdentifier(eventType watch.EventType) runtime.Identifier { 230 // The eventType is a non-standard pattern. This is coming from the fact 231 // that we're effectively serializing the whole watch event, but storing 232 // it in serializations of the Object within the watch event. 233 identifier := watchEncoderIdentifier{ 234 Name: "watch", 235 EmbeddedEncoder: string(e.embeddedEncoder.Identifier()), 236 Encoder: string(e.encoder.Identifier()), 237 EventType: string(eventType), 238 } 239 240 result, err := json.Marshal(identifier) 241 if err != nil { 242 klog.Fatalf("Failed marshaling identifier for watchEncoder: %v", err) 243 } 244 return runtime.Identifier(result) 245 } 246 247 // doTransformResponseObject is used for handling all requests, including watch. 248 func doTransformObject(ctx context.Context, obj runtime.Object, opts interface{}, target *schema.GroupVersionKind, scope *RequestScope) (runtime.Object, error) { 249 if _, ok := obj.(*metav1.Status); ok { 250 return obj, nil 251 } 252 253 switch { 254 case target == nil: 255 // If we ever change that from a no-op, the identifier of 256 // the watchEmbeddedEncoder has to be adjusted accordingly. 257 return obj, nil 258 259 case target.Kind == "PartialObjectMetadata": 260 return asPartialObjectMetadata(obj, target.GroupVersion()) 261 262 case target.Kind == "PartialObjectMetadataList": 263 return asPartialObjectMetadataList(obj, target.GroupVersion()) 264 265 case target.Kind == "Table": 266 options, ok := opts.(*metav1.TableOptions) 267 if !ok { 268 return nil, fmt.Errorf("unexpected TableOptions, got %T", opts) 269 } 270 return asTable(ctx, obj, options, scope, target.GroupVersion()) 271 272 default: 273 accepted, _ := negotiation.MediaTypesForSerializer(metainternalversionscheme.Codecs) 274 err := negotiation.NewNotAcceptableError(accepted) 275 return nil, err 276 } 277 } 278 279 // optionsForTransform will load and validate any additional query parameter options for 280 // a conversion or return an error. 281 func optionsForTransform(mediaType negotiation.MediaTypeOptions, req *http.Request) (interface{}, error) { 282 switch target := mediaType.Convert; { 283 case target == nil: 284 case target.Kind == "Table" && (target.GroupVersion() == metav1beta1.SchemeGroupVersion || target.GroupVersion() == metav1.SchemeGroupVersion): 285 opts := &metav1.TableOptions{} 286 if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), metav1.SchemeGroupVersion, opts); err != nil { 287 return nil, err 288 } 289 switch errs := validation.ValidateTableOptions(opts); len(errs) { 290 case 0: 291 return opts, nil 292 case 1: 293 return nil, errors.NewBadRequest(fmt.Sprintf("Unable to convert to Table as requested: %v", errs[0].Error())) 294 default: 295 return nil, errors.NewBadRequest(fmt.Sprintf("Unable to convert to Table as requested: %v", errs)) 296 } 297 } 298 return nil, nil 299 } 300 301 // targetEncodingForTransform returns the appropriate serializer for the input media type 302 func targetEncodingForTransform(scope *RequestScope, mediaType negotiation.MediaTypeOptions, req *http.Request) (schema.GroupVersionKind, runtime.NegotiatedSerializer, bool) { 303 switch target := mediaType.Convert; { 304 case target == nil: 305 case (target.Kind == "PartialObjectMetadata" || target.Kind == "PartialObjectMetadataList" || target.Kind == "Table") && 306 (target.GroupVersion() == metav1beta1.SchemeGroupVersion || target.GroupVersion() == metav1.SchemeGroupVersion): 307 return *target, metainternalversionscheme.Codecs, true 308 } 309 return scope.Kind, scope.Serializer, false 310 } 311 312 // transformResponseObject takes an object loaded from storage and performs any necessary transformations. 313 // Will write the complete response object. 314 // transformResponseObject is used only for handling non-streaming requests. 315 func transformResponseObject(ctx context.Context, scope *RequestScope, req *http.Request, w http.ResponseWriter, statusCode int, mediaType negotiation.MediaTypeOptions, result runtime.Object) { 316 options, err := optionsForTransform(mediaType, req) 317 if err != nil { 318 scope.err(err, w, req) 319 return 320 } 321 322 // ensure that for empty lists we don't return <nil> items. 323 // This is safe to modify without deep-copying the object, as 324 // List objects themselves are never cached. 325 if meta.IsListType(result) && meta.LenList(result) == 0 { 326 if err := meta.SetList(result, []runtime.Object{}); err != nil { 327 scope.err(err, w, req) 328 return 329 } 330 } 331 332 var obj runtime.Object 333 do := func() { 334 obj, err = doTransformObject(ctx, result, options, mediaType.Convert, scope) 335 } 336 endpointsrequest.TrackTransformResponseObjectLatency(ctx, do) 337 338 if err != nil { 339 scope.err(err, w, req) 340 return 341 } 342 kind, serializer, _ := targetEncodingForTransform(scope, mediaType, req) 343 responsewriters.WriteObjectNegotiated(serializer, scope, kind.GroupVersion(), w, req, statusCode, obj, false) 344 } 345 346 // errNotAcceptable indicates Accept negotiation has failed 347 type errNotAcceptable struct { 348 message string 349 } 350 351 func newNotAcceptableError(message string) error { 352 return errNotAcceptable{message} 353 } 354 355 func (e errNotAcceptable) Error() string { 356 return e.message 357 } 358 359 func (e errNotAcceptable) Status() metav1.Status { 360 return metav1.Status{ 361 Status: metav1.StatusFailure, 362 Code: http.StatusNotAcceptable, 363 Reason: metav1.StatusReason("NotAcceptable"), 364 Message: e.Error(), 365 } 366 } 367 368 func asTable(ctx context.Context, result runtime.Object, opts *metav1.TableOptions, scope *RequestScope, groupVersion schema.GroupVersion) (runtime.Object, error) { 369 switch groupVersion { 370 case metav1beta1.SchemeGroupVersion, metav1.SchemeGroupVersion: 371 default: 372 return nil, newNotAcceptableError(fmt.Sprintf("no Table exists in group version %s", groupVersion)) 373 } 374 375 obj, err := scope.TableConvertor.ConvertToTable(ctx, result, opts) 376 if err != nil { 377 return nil, err 378 } 379 380 table := (*metav1.Table)(obj) 381 382 for i := range table.Rows { 383 item := &table.Rows[i] 384 switch opts.IncludeObject { 385 case metav1.IncludeObject: 386 item.Object.Object, err = scope.Convertor.ConvertToVersion(item.Object.Object, scope.Kind.GroupVersion()) 387 if err != nil { 388 return nil, err 389 } 390 // TODO: rely on defaulting for the value here? 391 case metav1.IncludeMetadata, "": 392 m, err := meta.Accessor(item.Object.Object) 393 if err != nil { 394 return nil, err 395 } 396 // TODO: turn this into an internal type and do conversion in order to get object kind automatically set? 397 partial := meta.AsPartialObjectMetadata(m) 398 partial.GetObjectKind().SetGroupVersionKind(groupVersion.WithKind("PartialObjectMetadata")) 399 item.Object.Object = partial 400 case metav1.IncludeNone: 401 item.Object.Object = nil 402 default: 403 err = errors.NewBadRequest(fmt.Sprintf("unrecognized includeObject value: %q", opts.IncludeObject)) 404 return nil, err 405 } 406 } 407 408 return table, nil 409 } 410 411 func asPartialObjectMetadata(result runtime.Object, groupVersion schema.GroupVersion) (runtime.Object, error) { 412 if meta.IsListType(result) { 413 err := newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadata, but the requested object is a list (%T)", result)) 414 return nil, err 415 } 416 switch groupVersion { 417 case metav1beta1.SchemeGroupVersion, metav1.SchemeGroupVersion: 418 default: 419 return nil, newNotAcceptableError(fmt.Sprintf("no PartialObjectMetadataList exists in group version %s", groupVersion)) 420 } 421 m, err := meta.Accessor(result) 422 if err != nil { 423 return nil, err 424 } 425 partial := meta.AsPartialObjectMetadata(m) 426 partial.GetObjectKind().SetGroupVersionKind(groupVersion.WithKind("PartialObjectMetadata")) 427 return partial, nil 428 } 429 430 func asPartialObjectMetadataList(result runtime.Object, groupVersion schema.GroupVersion) (runtime.Object, error) { 431 li, ok := result.(metav1.ListInterface) 432 if !ok { 433 return nil, newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadataList, but the requested object is not a list (%T)", result)) 434 } 435 436 gvk := groupVersion.WithKind("PartialObjectMetadata") 437 switch { 438 case groupVersion == metav1beta1.SchemeGroupVersion: 439 list := &metav1beta1.PartialObjectMetadataList{} 440 err := meta.EachListItem(result, func(obj runtime.Object) error { 441 m, err := meta.Accessor(obj) 442 if err != nil { 443 return err 444 } 445 partial := meta.AsPartialObjectMetadata(m) 446 partial.GetObjectKind().SetGroupVersionKind(gvk) 447 list.Items = append(list.Items, *partial) 448 return nil 449 }) 450 if err != nil { 451 return nil, err 452 } 453 list.ResourceVersion = li.GetResourceVersion() 454 list.Continue = li.GetContinue() 455 list.RemainingItemCount = li.GetRemainingItemCount() 456 return list, nil 457 458 case groupVersion == metav1.SchemeGroupVersion: 459 list := &metav1.PartialObjectMetadataList{} 460 err := meta.EachListItem(result, func(obj runtime.Object) error { 461 m, err := meta.Accessor(obj) 462 if err != nil { 463 return err 464 } 465 partial := meta.AsPartialObjectMetadata(m) 466 partial.GetObjectKind().SetGroupVersionKind(gvk) 467 list.Items = append(list.Items, *partial) 468 return nil 469 }) 470 if err != nil { 471 return nil, err 472 } 473 list.ResourceVersion = li.GetResourceVersion() 474 list.Continue = li.GetContinue() 475 list.RemainingItemCount = li.GetRemainingItemCount() 476 return list, nil 477 478 default: 479 return nil, newNotAcceptableError(fmt.Sprintf("no PartialObjectMetadataList exists in group version %s", groupVersion)) 480 } 481 }