Unverified Commit a7c65d29 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

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. ```
parents 529bf830 59c305b5
......@@ -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) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
return unstructuredDecoder{delegate: Codecs.DecoderToVersion(serializer, gv)}
func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
return versioning.NewDefaultingCodecForScheme(Scheme, encoder, nil, gv, nil)
}
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) {
......
......@@ -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",
......
......@@ -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 {
......
......@@ -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):
......
......@@ -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",
......
......@@ -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(),
}
}
......@@ -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)
......
......@@ -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)
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment