diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index 3112b9353fd..f87718d7621 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -18,7 +18,6 @@ package apiserver import ( "fmt" - "io" "net/http" "path" "sync" @@ -227,7 +226,11 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed) return } - handler := handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}) + supportedTypes := []string{ + string(types.JSONPatchType), + string(types.MergePatchType), + } + handler := handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes) handler(w, req) return case "delete": @@ -471,27 +474,20 @@ func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.Serial Framer: json.Framer, }, }, + { + MediaType: "application/yaml", + EncodesAsText: true, + Serializer: json.NewYAMLSerializer(json.DefaultMetaFactory, s.creator, s.typer), + }, } } -func (s unstructuredNegotiatedSerializer) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { - return versioning.NewDefaultingCodecForScheme(Scheme, crEncoderInstance, nil, gv, nil) +func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { + return versioning.NewDefaultingCodecForScheme(Scheme, encoder, nil, gv, nil) } -func (s unstructuredNegotiatedSerializer) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { - return unstructuredDecoder{delegate: Codecs.DecoderToVersion(serializer, gv)} -} - -type unstructuredDecoder struct { - delegate runtime.Decoder -} - -func (d unstructuredDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { - // Delegate for things other than Unstructured. - if _, ok := into.(runtime.Unstructured); !ok && into != nil { - return d.delegate.Decode(data, defaults, into) - } - return unstructured.UnstructuredJSONScheme.Decode(data, defaults, into) +func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { + return versioning.NewDefaultingCodecForScheme(Scheme, nil, decoder, nil, gv) } type unstructuredObjectTyper struct { @@ -511,29 +507,6 @@ func (t unstructuredObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool { return t.delegate.Recognizes(gvk) || t.unstructuredTyper.Recognizes(gvk) } -var crEncoderInstance = crEncoder{} - -// crEncoder *usually* encodes using the unstructured.UnstructuredJSONScheme, but if the type is Status or WatchEvent -// it will serialize them out using the converting codec. -type crEncoder struct{} - -func (crEncoder) Encode(obj runtime.Object, w io.Writer) error { - switch t := obj.(type) { - case *metav1.Status, *metav1.WatchEvent: - for _, info := range Codecs.SupportedMediaTypes() { - // we are always json - if info.MediaType == "application/json" { - return info.Serializer.Encode(obj, w) - } - } - - return fmt.Errorf("unable to find json serializer for %T", t) - - default: - return unstructured.UnstructuredJSONScheme.Encode(obj, w) - } -} - type unstructuredCreator struct{} func (c unstructuredCreator) New(kind schema.GroupVersionKind) (runtime.Object, error) { diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD b/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD index 1aca59abf8e..5d26af605a7 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD @@ -12,11 +12,13 @@ go_test( "finalization_test.go", "registration_test.go", "validation_test.go", + "yaml_test.go", ], importpath = "k8s.io/apiextensions-apiserver/test/integration", tags = ["integration"], deps = [ "//vendor/github.com/coreos/etcd/clientv3:go_default_library", + "//vendor/github.com/ghodss/yaml:go_default_library", "//vendor/github.com/stretchr/testify/require:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver:go_default_library", diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/yaml_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/yaml_test.go new file mode 100644 index 00000000000..b6410123896 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/yaml_test.go @@ -0,0 +1,361 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/ghodss/yaml" + + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apiextensions-apiserver/test/integration/testserver" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" +) + +func TestYAML(t *testing.T) { + config, err := testserver.DefaultServerConfig() + if err != nil { + t.Fatal(err) + } + + stopCh, apiExtensionClient, clientPool, err := testserver.StartServer(config) + if err != nil { + t.Fatal(err) + } + defer close(stopCh) + + noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped) + _, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) + if err != nil { + t.Fatal(err) + } + + kind := noxuDefinition.Spec.Names.Kind + listKind := noxuDefinition.Spec.Names.ListKind + apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version + + rest := apiExtensionClient.Discovery().RESTClient() + + // Discovery + { + result, err := rest.Get(). + SetHeader("Accept", "application/yaml"). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version). + DoRaw() + if err != nil { + t.Fatal(err, string(result)) + } + obj, err := decodeYAML(result) + if err != nil { + t.Fatal(err) + } + if obj.GetAPIVersion() != "v1" || obj.GetKind() != "APIResourceList" { + t.Fatalf("unexpected discovery kind: %s", string(result)) + } + if v, ok, err := unstructured.NestedString(obj.Object, "groupVersion"); v != apiVersion || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + } + + // Error + { + result, err := rest.Get(). + SetHeader("Accept", "application/yaml"). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "missingname"). + DoRaw() + if !errors.IsNotFound(err) { + t.Fatalf("expected not found, got %v", err) + } + obj, err := decodeYAML(result) + if err != nil { + t.Fatal(err) + } + if obj.GetAPIVersion() != "v1" || obj.GetKind() != "Status" { + t.Fatalf("unexpected discovery kind: %s", string(result)) + } + if v, ok, err := unstructured.NestedString(obj.Object, "reason"); v != "NotFound" || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + } + + uid := types.UID("") + resourceVersion := "" + + // Create + { + yamlBody := []byte(fmt.Sprintf(` +apiVersion: %s +kind: %s +metadata: + name: mytest +values: + numVal: 1 + boolVal: true + stringVal: "1"`, apiVersion, kind)) + + result, err := rest.Post(). + SetHeader("Accept", "application/yaml"). + SetHeader("Content-Type", "application/yaml"). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural). + Body(yamlBody). + DoRaw() + if err != nil { + t.Fatal(err, string(result)) + } + obj, err := decodeYAML(result) + if err != nil { + t.Fatal(err) + } + if obj.GetName() != "mytest" { + t.Fatalf("expected mytest, got %s", obj.GetName()) + } + if obj.GetAPIVersion() != apiVersion { + t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion()) + } + if obj.GetKind() != kind { + t.Fatalf("expected %s, got %s", kind, obj.GetKind()) + } + if v, ok, err := unstructured.NestedFloat64(obj.Object, "values", "numVal"); v != 1 || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + if v, ok, err := unstructured.NestedBool(obj.Object, "values", "boolVal"); v != true || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + if v, ok, err := unstructured.NestedString(obj.Object, "values", "stringVal"); v != "1" || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + uid = obj.GetUID() + resourceVersion = obj.GetResourceVersion() + } + + // Get + { + result, err := rest.Get(). + SetHeader("Accept", "application/yaml"). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest"). + DoRaw() + if err != nil { + t.Fatal(err) + } + obj, err := decodeYAML(result) + if err != nil { + t.Fatal(err, string(result)) + } + if obj.GetName() != "mytest" { + t.Fatalf("expected mytest, got %s", obj.GetName()) + } + if obj.GetAPIVersion() != apiVersion { + t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion()) + } + if obj.GetKind() != kind { + t.Fatalf("expected %s, got %s", kind, obj.GetKind()) + } + if v, ok, err := unstructured.NestedFloat64(obj.Object, "values", "numVal"); v != 1 || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + if v, ok, err := unstructured.NestedBool(obj.Object, "values", "boolVal"); v != true || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + if v, ok, err := unstructured.NestedString(obj.Object, "values", "stringVal"); v != "1" || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + } + + // List + { + result, err := rest.Get(). + SetHeader("Accept", "application/yaml"). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural). + DoRaw() + if err != nil { + t.Fatal(err, string(result)) + } + listObj, err := decodeYAML(result) + if err != nil { + t.Fatal(err) + } + if listObj.GetAPIVersion() != apiVersion { + t.Fatalf("expected %s, got %s", apiVersion, listObj.GetAPIVersion()) + } + if listObj.GetKind() != listKind { + t.Fatalf("expected %s, got %s", kind, listObj.GetKind()) + } + items, ok, err := unstructured.NestedSlice(listObj.Object, "items") + if !ok || err != nil || len(items) != 1 { + t.Fatalf("expected one item, got %v %v %v", items, ok, err) + } + obj := unstructured.Unstructured{Object: items[0].(map[string]interface{})} + if obj.GetName() != "mytest" { + t.Fatalf("expected mytest, got %s", obj.GetName()) + } + if obj.GetAPIVersion() != apiVersion { + t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion()) + } + if obj.GetKind() != kind { + t.Fatalf("expected %s, got %s", kind, obj.GetKind()) + } + if v, ok, err := unstructured.NestedFloat64(obj.Object, "values", "numVal"); v != 1 || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + if v, ok, err := unstructured.NestedBool(obj.Object, "values", "boolVal"); v != true || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + if v, ok, err := unstructured.NestedString(obj.Object, "values", "stringVal"); v != "1" || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + } + + // Watch rejects yaml (no streaming support) + { + result, err := rest.Get(). + SetHeader("Accept", "application/yaml"). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural). + Param("watch", "true"). + DoRaw() + if !errors.IsNotAcceptable(err) { + t.Fatal("expected not acceptable error, got %v (%s)", err, string(result)) + } + obj, err := decodeYAML(result) + if err != nil { + t.Fatal(err) + } + if obj.GetAPIVersion() != "v1" || obj.GetKind() != "Status" { + t.Fatalf("unexpected result: %s", string(result)) + } + if v, ok, err := unstructured.NestedString(obj.Object, "reason"); v != "NotAcceptable" || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + if v, ok, err := unstructured.NestedFloat64(obj.Object, "code"); v != http.StatusNotAcceptable || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + } + + // Update + { + yamlBody := []byte(fmt.Sprintf(` +apiVersion: %s +kind: %s +metadata: + name: mytest + uid: %s + resourceVersion: "%s" +values: + numVal: 2 + boolVal: false + stringVal: "2"`, apiVersion, kind, uid, resourceVersion)) + result, err := rest.Put(). + SetHeader("Accept", "application/yaml"). + SetHeader("Content-Type", "application/yaml"). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest"). + Body(yamlBody). + DoRaw() + if err != nil { + t.Fatal(err, string(result)) + } + obj, err := decodeYAML(result) + if err != nil { + t.Fatal(err) + } + if obj.GetName() != "mytest" { + t.Fatalf("expected mytest, got %s", obj.GetName()) + } + if obj.GetAPIVersion() != apiVersion { + t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion()) + } + if obj.GetKind() != kind { + t.Fatalf("expected %s, got %s", kind, obj.GetKind()) + } + if v, ok, err := unstructured.NestedFloat64(obj.Object, "values", "numVal"); v != 2 || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + if v, ok, err := unstructured.NestedBool(obj.Object, "values", "boolVal"); v != false || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + if v, ok, err := unstructured.NestedString(obj.Object, "values", "stringVal"); v != "2" || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + if obj.GetUID() != uid { + t.Fatal("uid changed: %v vs %v", uid, obj.GetUID()) + } + } + + // Patch rejects yaml requests (only JSON mime types are allowed) + { + yamlBody := []byte(fmt.Sprintf(` +values: + numVal: 3`, apiVersion, kind, uid, resourceVersion)) + result, err := rest.Patch(types.MergePatchType). + SetHeader("Accept", "application/yaml"). + SetHeader("Content-Type", "application/yaml"). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest"). + Body(yamlBody). + DoRaw() + if !errors.IsUnsupportedMediaType(err) { + t.Fatalf("Expected bad request, got %v\n%s", err, string(result)) + } + obj, err := decodeYAML(result) + if err != nil { + t.Fatal(err) + } + if obj.GetAPIVersion() != "v1" || obj.GetKind() != "Status" { + t.Fatalf("expected %s %s, got %s %s", "v1", "Status", obj.GetAPIVersion(), obj.GetKind()) + } + if v, ok, err := unstructured.NestedString(obj.Object, "reason"); v != "UnsupportedMediaType" || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + } + + // Delete + { + result, err := rest.Delete(). + SetHeader("Accept", "application/yaml"). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest"). + DoRaw() + if err != nil { + t.Fatal(err, string(result)) + } + obj, err := decodeYAML(result) + if err != nil { + t.Fatal(err) + } + if obj.GetAPIVersion() != "v1" || obj.GetKind() != "Status" { + t.Fatalf("unexpected response: %s", string(result)) + } + if v, ok, err := unstructured.NestedString(obj.Object, "status"); v != "Success" || !ok || err != nil { + t.Fatal(v, ok, err, string(result)) + } + } +} + +func decodeYAML(data []byte) (*unstructured.Unstructured, error) { + retval := &unstructured.Unstructured{Object: map[string]interface{}{}} + // ensure this isn't JSON + if json.Unmarshal(data, &retval.Object) == nil { + return nil, fmt.Errorf("data is JSON, not YAML: %s", string(data)) + } + // ensure it is YAML + retval.Object = map[string]interface{}{} + if err := yaml.Unmarshal(data, &retval.Object); err != nil { + return nil, fmt.Errorf("error decoding YAML: %v\noriginal YAML: %s", err, string(data)) + } + return retval, nil +} diff --git a/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go b/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go index 9960600be33..3a2c9549ba2 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go @@ -352,6 +352,14 @@ func NewGenericServerResponse(code int, verb string, qualifiedResource schema.Gr reason = metav1.StatusReasonForbidden // the server message has details about who is trying to perform what action. Keep its message. message = serverMessage + case http.StatusNotAcceptable: + reason = metav1.StatusReasonNotAcceptable + // the server message has details about what types are acceptable + message = serverMessage + case http.StatusUnsupportedMediaType: + reason = metav1.StatusReasonUnsupportedMediaType + // the server message has details about what types are acceptable + message = serverMessage case http.StatusMethodNotAllowed: reason = metav1.StatusReasonMethodNotAllowed message = "the server does not allow this method on the requested resource" @@ -434,6 +442,16 @@ func IsResourceExpired(err error) bool { return ReasonForError(err) == metav1.StatusReasonExpired } +// IsNotAcceptable determines if err is an error which indicates that the request failed due to an invalid Accept header +func IsNotAcceptable(err error) bool { + return ReasonForError(err) == metav1.StatusReasonNotAcceptable +} + +// IsUnsupportedMediaType determines if err is an error which indicates that the request failed due to an invalid Content-Type header +func IsUnsupportedMediaType(err error) bool { + return ReasonForError(err) == metav1.StatusReasonUnsupportedMediaType +} + // IsMethodNotSupported determines if the err is an error which indicates the provided action could not // be performed because it is not supported by the server. func IsMethodNotSupported(err error) bool { diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go index c8ee4e5d65b..750080770c4 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go @@ -651,6 +651,18 @@ const ( // can only be created. API calls that return MethodNotAllowed can never succeed. StatusReasonMethodNotAllowed StatusReason = "MethodNotAllowed" + // StatusReasonNotAcceptable means that the accept types indicated by the client were not acceptable + // to the server - for instance, attempting to receive protobuf for a resource that supports only json and yaml. + // API calls that return NotAcceptable can never succeed. + // Status code 406 + StatusReasonNotAcceptable StatusReason = "NotAcceptable" + + // StatusReasonUnsupportedMediaType means that the content type sent by the client is not acceptable + // to the server - for instance, attempting to send protobuf for a resource that supports only json and yaml. + // API calls that return UnsupportedMediaType can never succeed. + // Status code 415 + StatusReasonUnsupportedMediaType StatusReason = "UnsupportedMediaType" + // StatusReasonInternalError indicates that an internal error occurred, it is unexpected // and the outcome of the call is unknown. // Details (optional): diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD index 16901d6a6b0..2bf780bd6b5 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD @@ -71,6 +71,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/proxy:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library", "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go index 07bc8e280f4..93b17cfb097 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go @@ -41,7 +41,7 @@ func (e errNotAcceptable) Status() metav1.Status { return metav1.Status{ Status: metav1.StatusFailure, Code: http.StatusNotAcceptable, - Reason: metav1.StatusReason("NotAcceptable"), + Reason: metav1.StatusReasonNotAcceptable, Message: e.Error(), } } @@ -63,7 +63,7 @@ func (e errUnsupportedMediaType) Status() metav1.Status { return metav1.Status{ Status: metav1.StatusFailure, Code: http.StatusUnsupportedMediaType, - Reason: metav1.StatusReason("UnsupportedMediaType"), + Reason: metav1.StatusReasonUnsupportedMediaType, Message: e.Error(), } } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 1ac736d09dd..a54054127bf 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -32,17 +32,34 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/mergepatch" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" ) // PatchResource returns a function that will handle a resource patch // TODO: Eventually PatchResource should just use GuaranteedUpdate and this routine should be a bit cleaner -func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface, converter runtime.ObjectConvertor) http.HandlerFunc { +func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface, converter runtime.ObjectConvertor, patchTypes []string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { + // Do this first, otherwise name extraction can fail for unrecognized content types + // TODO: handle this in negotiation + contentType := req.Header.Get("Content-Type") + // Remove "; charset=" if included in header. + if idx := strings.Index(contentType, ";"); idx > 0 { + contentType = contentType[:idx] + } + patchType := types.PatchType(contentType) + + // Ensure the patchType is one we support + if !sets.NewString(patchTypes...).Has(contentType) { + scope.err(negotiation.NewUnsupportedMediaTypeError(patchTypes), w, req) + return + } + // TODO: we either want to remove timeout or document it (if we // document, move timeout out of this function and declare it in // api_installer) @@ -63,14 +80,6 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface return } - // TODO: handle this in negotiation - contentType := req.Header.Get("Content-Type") - // Remove "; charset=" if included in header. - if idx := strings.Index(contentType, ";"); idx > 0 { - contentType = contentType[:idx] - } - patchType := types.PatchType(contentType) - patchJS, err := readBody(req) if err != nil { scope.err(err, w, req) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index 87bf1700d2b..11f658ee82a 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -690,7 +690,12 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag if hasSubresource { doc = "partially update " + subresource + " of the specified " + kind } - handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulPatchResource(patcher, reqScope, admit, mapping.ObjectConvertor)) + supportedTypes := []string{ + string(types.JSONPatchType), + string(types.MergePatchType), + string(types.StrategicMergePatchType), + } + handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulPatchResource(patcher, reqScope, admit, mapping.ObjectConvertor, supportedTypes)) route := ws.PATCH(action.Path).To(handler). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). @@ -1099,9 +1104,9 @@ func restfulUpdateResource(r rest.Updater, scope handlers.RequestScope, typer ru } } -func restfulPatchResource(r rest.Patcher, scope handlers.RequestScope, admit admission.Interface, converter runtime.ObjectConvertor) restful.RouteFunction { +func restfulPatchResource(r rest.Patcher, scope handlers.RequestScope, admit admission.Interface, converter runtime.ObjectConvertor, supportedTypes []string) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { - handlers.PatchResource(r, scope, admit, converter)(res.ResponseWriter, req.Request) + handlers.PatchResource(r, scope, admit, converter, supportedTypes)(res.ResponseWriter, req.Request) } }