Merge pull request #58260 from liggitt/crd-yaml

Automatic merge from submit-queue (batch tested with PRs 58260, 58326). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Add support for submitting/receiving CRD objects as yaml

Fixes #37455

```release-note
Custom resources can now be submitted to and received from the API server in application/yaml format.
```
This commit is contained in:
Kubernetes Submit Queue 2018-01-16 06:42:30 -08:00 committed by GitHub
commit a7c65d29e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 436 additions and 55 deletions

View File

@ -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) {

View File

@ -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",

View File

@ -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
}

View File

@ -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 {

View File

@ -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):

View File

@ -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",

View File

@ -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(),
}
}

View File

@ -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)

View File

@ -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)
}
}