Ensure all response object modification happens in one place

Make setLink and setListLink the same, and make them happen in transformResponseObject.
Make those methods also responsible for ensuring an empty list. Then move outputMediaType
negotiation before all other calls in the specific methods, to ensure we fail fast.

Refactoring in preparation to support type conversion on watch.
This commit is contained in:
Clayton Coleman 2018-11-28 21:32:51 -05:00
parent 9c50dd0562
commit 56a25d8c5f
No known key found for this signature in database
GPG Key ID: 3D16906B4F1C5CB3
8 changed files with 90 additions and 99 deletions

View File

@ -158,6 +158,19 @@ func ExtractList(obj runtime.Object) ([]runtime.Object, error) {
// objectSliceType is the type of a slice of Objects // objectSliceType is the type of a slice of Objects
var objectSliceType = reflect.TypeOf([]runtime.Object{}) var objectSliceType = reflect.TypeOf([]runtime.Object{})
// LenList returns the length of this list or 0 if it is not a list.
func LenList(list runtime.Object) int {
itemsPtr, err := GetItemsPtr(list)
if err != nil {
return 0
}
items, err := conversion.EnforcePtr(itemsPtr)
if err != nil {
return 0
}
return items.Len()
}
// SetList sets the given list object's Items member have the elements given in // SetList sets the given list object's Items member have the elements given in
// objects. // objects.
// Returns an error if list is not a List type (does not have an Items member), // Returns an error if list is not a List type (does not have an Items member),

View File

@ -70,6 +70,11 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte
ctx := req.Context() ctx := req.Context()
ctx = request.WithNamespace(ctx, namespace) ctx = request.WithNamespace(ctx, namespace)
outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, &scope)
if err != nil {
scope.err(err, w, req)
return
}
gv := scope.Kind.GroupVersion() gv := scope.Kind.GroupVersion()
s, err := negotiation.NegotiateInputSerializer(req, false, scope.Serializer) s, err := negotiation.NegotiateInputSerializer(req, false, scope.Serializer)
@ -77,6 +82,7 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte
scope.err(err, w, req) scope.err(err, w, req)
return return
} }
decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion) decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion)
body, err := readBody(req) body, err := readBody(req)
@ -144,17 +150,6 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte
} }
trace.Step("Object stored in database") trace.Step("Object stored in database")
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
scope.err(fmt.Errorf("missing requestInfo"), w, req)
return
}
if err := setSelfLink(result, requestInfo, scope.Namer); err != nil {
scope.err(err, w, req)
return
}
trace.Step("Self-link added")
// If the object is partially initialized, always indicate it via StatusAccepted // If the object is partially initialized, always indicate it via StatusAccepted
code := http.StatusCreated code := http.StatusCreated
if accessor, err := meta.Accessor(result); err == nil { if accessor, err := meta.Accessor(result); err == nil {
@ -168,7 +163,7 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte
} }
scope.Trace = trace scope.Trace = trace
transformResponseObject(ctx, scope, req, w, code, result) transformResponseObject(ctx, scope, req, w, code, outputMediaType, result)
} }
} }

View File

@ -64,6 +64,12 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco
ae := request.AuditEventFrom(ctx) ae := request.AuditEventFrom(ctx)
admit = admission.WithAudit(admit, ae) admit = admission.WithAudit(admit, ae)
outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, &scope)
if err != nil {
scope.err(err, w, req)
return
}
options := &metav1.DeleteOptions{} options := &metav1.DeleteOptions{}
if allowsOptions { if allowsOptions {
body, err := readBody(req) body, err := readBody(req)
@ -160,23 +166,10 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco
Kind: scope.Kind.Kind, Kind: scope.Kind.Kind,
}, },
} }
} else {
// when a non-status response is returned, set the self link
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
scope.err(fmt.Errorf("missing requestInfo"), w, req)
return
}
if _, ok := result.(*metav1.Status); !ok {
if err := setSelfLink(result, requestInfo, scope.Namer); err != nil {
scope.err(err, w, req)
return
}
}
} }
scope.Trace = trace scope.Trace = trace
transformResponseObject(ctx, scope, req, w, status, result) transformResponseObject(ctx, scope, req, w, status, outputMediaType, result)
} }
} }
@ -204,6 +197,12 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope RequestSco
ctx = request.WithNamespace(ctx, namespace) ctx = request.WithNamespace(ctx, namespace)
ae := request.AuditEventFrom(ctx) ae := request.AuditEventFrom(ctx)
outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, &scope)
if err != nil {
scope.err(err, w, req)
return
}
listOptions := metainternalversion.ListOptions{} listOptions := metainternalversion.ListOptions{}
if err := metainternalversion.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, &listOptions); err != nil { if err := metainternalversion.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, &listOptions); err != nil {
err = errors.NewBadRequest(err.Error()) err = errors.NewBadRequest(err.Error())
@ -304,17 +303,9 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope RequestSco
Kind: scope.Kind.Kind, Kind: scope.Kind.Kind,
}, },
} }
} else {
// when a non-status response is returned, set the self link
if _, ok := result.(*metav1.Status); !ok {
if _, err := setListSelfLink(result, ctx, req, scope.Namer); err != nil {
scope.err(err, w, req)
return
}
}
} }
scope.Trace = trace scope.Trace = trace
transformResponseObject(ctx, scope, req, w, http.StatusOK, result) transformResponseObject(ctx, scope, req, w, http.StatusOK, outputMediaType, result)
} }
} }

View File

@ -33,6 +33,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
"k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/endpoints/metrics"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
@ -58,24 +59,21 @@ func getResourceHandler(scope RequestScope, getter getterFunc) http.HandlerFunc
ctx := req.Context() ctx := req.Context()
ctx = request.WithNamespace(ctx, namespace) ctx = request.WithNamespace(ctx, namespace)
result, err := getter(ctx, name, req, trace) outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, &scope)
if err != nil { if err != nil {
scope.err(err, w, req) scope.err(err, w, req)
return return
} }
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok { result, err := getter(ctx, name, req, trace)
scope.err(fmt.Errorf("missing requestInfo"), w, req) if err != nil {
return
}
if err := setSelfLink(result, requestInfo, scope.Namer); err != nil {
scope.err(err, w, req) scope.err(err, w, req)
return return
} }
trace.Step("About to write a response") trace.Step("About to write a response")
scope.Trace = trace scope.Trace = trace
transformResponseObject(ctx, scope, req, w, http.StatusOK, result) transformResponseObject(ctx, scope, req, w, http.StatusOK, outputMediaType, result)
trace.Step("Transformed response object") trace.Step("Transformed response object")
} }
} }
@ -187,6 +185,12 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope RequestScope, forceWatch
ctx := req.Context() ctx := req.Context()
ctx = request.WithNamespace(ctx, namespace) ctx = request.WithNamespace(ctx, namespace)
outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, &scope)
if err != nil {
scope.err(err, w, req)
return
}
opts := metainternalversion.ListOptions{} opts := metainternalversion.ListOptions{}
if err := metainternalversion.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, &opts); err != nil { if err := metainternalversion.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, &opts); err != nil {
err = errors.NewBadRequest(err.Error()) err = errors.NewBadRequest(err.Error())
@ -267,22 +271,9 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope RequestScope, forceWatch
return return
} }
trace.Step("Listing from storage done") trace.Step("Listing from storage done")
numberOfItems, err := setListSelfLink(result, ctx, req, scope.Namer)
if err != nil {
scope.err(err, w, req)
return
}
trace.Step("Self-linking done")
// Ensure empty lists return a non-nil items slice
if numberOfItems == 0 && meta.IsListType(result) {
if err := meta.SetList(result, []runtime.Object{}); err != nil {
scope.err(err, w, req)
return
}
}
scope.Trace = trace scope.Trace = trace
transformResponseObject(ctx, scope, req, w, http.StatusOK, result) transformResponseObject(ctx, scope, req, w, http.StatusOK, outputMediaType, result)
trace.Step(fmt.Sprintf("Writing http response done (%d items)", numberOfItems)) trace.Step(fmt.Sprintf("Writing http response done (%d items)", meta.LenList(result)))
} }
} }

View File

@ -88,6 +88,12 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
ctx := req.Context() ctx := req.Context()
ctx = request.WithNamespace(ctx, namespace) ctx = request.WithNamespace(ctx, namespace)
outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, &scope)
if err != nil {
scope.err(err, w, req)
return
}
patchJS, err := readBody(req) patchJS, err := readBody(req)
if err != nil { if err != nil {
scope.err(err, w, req) scope.err(err, w, req)
@ -190,19 +196,8 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
} }
trace.Step("Object stored in database") trace.Step("Object stored in database")
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
scope.err(fmt.Errorf("missing requestInfo"), w, req)
return
}
if err := setSelfLink(result, requestInfo, scope.Namer); err != nil {
scope.err(err, w, req)
return
}
trace.Step("Self-link added")
scope.Trace = trace scope.Trace = trace
transformResponseObject(ctx, scope, req, w, http.StatusOK, result) transformResponseObject(ctx, scope, req, w, http.StatusOK, outputMediaType, result)
} }
} }

View File

@ -33,17 +33,14 @@ import (
// transformResponseObject takes an object loaded from storage and performs any necessary transformations. // transformResponseObject takes an object loaded from storage and performs any necessary transformations.
// Will write the complete response object. // Will write the complete response object.
func transformResponseObject(ctx context.Context, scope RequestScope, req *http.Request, w http.ResponseWriter, statusCode int, result runtime.Object) { func transformResponseObject(ctx context.Context, scope RequestScope, req *http.Request, w http.ResponseWriter, statusCode int, mediaType negotiation.MediaTypeOptions, result runtime.Object) {
// TODO: fetch the media type much earlier in request processing and pass it into this method. if err := setObjectSelfLink(ctx, result, req, scope.Namer); err != nil {
trace := scope.Trace scope.err(err, w, req)
mediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, &scope)
if err != nil {
status := responsewriters.ErrorToAPIStatus(err)
trace.Step("Writing raw JSON response")
responsewriters.WriteRawJSON(int(status.Code), status, w)
return return
} }
trace := scope.Trace
// If conversion was allowed by the scope, perform it before writing the response // If conversion was allowed by the scope, perform it before writing the response
if target := mediaType.Convert; target != nil { if target := mediaType.Convert; target != nil {
switch { switch {
@ -51,7 +48,7 @@ func transformResponseObject(ctx context.Context, scope RequestScope, req *http.
case target.Kind == "PartialObjectMetadata" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: case target.Kind == "PartialObjectMetadata" && target.GroupVersion() == metav1beta1.SchemeGroupVersion:
if meta.IsListType(result) { if meta.IsListType(result) {
// TODO: this should be calculated earlier // TODO: this should be calculated earlier
err = newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadata, but the requested object is a list (%T)", result)) err := newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadata, but the requested object is a list (%T)", result))
scope.err(err, w, req) scope.err(err, w, req)
return return
} }
@ -77,7 +74,7 @@ func transformResponseObject(ctx context.Context, scope RequestScope, req *http.
case target.Kind == "PartialObjectMetadataList" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: case target.Kind == "PartialObjectMetadataList" && target.GroupVersion() == metav1beta1.SchemeGroupVersion:
if !meta.IsListType(result) { if !meta.IsListType(result) {
// TODO: this should be calculated earlier // TODO: this should be calculated earlier
err = newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadataList, but the requested object is not a list (%T)", result)) err := newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadataList, but the requested object is not a list (%T)", result))
scope.err(err, w, req) scope.err(err, w, req)
return return
} }

View File

@ -280,23 +280,30 @@ func checkName(obj runtime.Object, name, namespace string, namer ScopeNamer) err
return nil return nil
} }
// setListSelfLink sets the self link of a list to the base URL, then sets the self links // setObjectSelfLink sets the self link of an object as needed.
// on all child objects returned. Returns the number of items in the list. func setObjectSelfLink(ctx context.Context, obj runtime.Object, req *http.Request, namer ScopeNamer) error {
func setListSelfLink(obj runtime.Object, ctx context.Context, req *http.Request, namer ScopeNamer) (int, error) {
if !meta.IsListType(obj) { if !meta.IsListType(obj) {
return 0, nil // status needs no self link
if _, ok := obj.(*metav1.Status); ok {
return nil
}
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
return fmt.Errorf("missing requestInfo")
}
return setSelfLink(obj, requestInfo, namer)
} }
uri, err := namer.GenerateListLink(req) uri, err := namer.GenerateListLink(req)
if err != nil { if err != nil {
return 0, err return err
} }
if err := namer.SetSelfLink(obj, uri); err != nil { if err := namer.SetSelfLink(obj, uri); err != nil {
klog.V(4).Infof("Unable to set self link on object: %v", err) klog.V(4).Infof("Unable to set self link on object: %v", err)
} }
requestInfo, ok := request.RequestInfoFrom(ctx) requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok { if !ok {
return 0, fmt.Errorf("missing requestInfo") return fmt.Errorf("missing requestInfo")
} }
count := 0 count := 0
@ -304,7 +311,14 @@ func setListSelfLink(obj runtime.Object, ctx context.Context, req *http.Request,
count++ count++
return setSelfLink(obj, requestInfo, namer) return setSelfLink(obj, requestInfo, namer)
}) })
return count, err
if count == 0 {
if err := meta.SetList(obj, []runtime.Object{}); err != nil {
return err
}
}
return err
} }
func summarizeData(data []byte, maxLength int) string { func summarizeData(data []byte, maxLength int) string {

View File

@ -64,6 +64,12 @@ func UpdateResource(r rest.Updater, scope RequestScope, admit admission.Interfac
ctx := req.Context() ctx := req.Context()
ctx = request.WithNamespace(ctx, namespace) ctx = request.WithNamespace(ctx, namespace)
outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, &scope)
if err != nil {
scope.err(err, w, req)
return
}
body, err := readBody(req) body, err := readBody(req)
if err != nil { if err != nil {
scope.err(err, w, req) scope.err(err, w, req)
@ -174,24 +180,13 @@ func UpdateResource(r rest.Updater, scope RequestScope, admit admission.Interfac
} }
trace.Step("Object stored in database") trace.Step("Object stored in database")
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
scope.err(fmt.Errorf("missing requestInfo"), w, req)
return
}
if err := setSelfLink(result, requestInfo, scope.Namer); err != nil {
scope.err(err, w, req)
return
}
trace.Step("Self-link added")
status := http.StatusOK status := http.StatusOK
if wasCreated { if wasCreated {
status = http.StatusCreated status = http.StatusCreated
} }
scope.Trace = trace scope.Trace = trace
transformResponseObject(ctx, scope, req, w, status, result) transformResponseObject(ctx, scope, req, w, status, outputMediaType, result)
} }
} }