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 17d4329e33a..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" @@ -475,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 { @@ -515,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 +}