Add support for submitting/receiving CRD objects as yaml

This commit is contained in:
Jordan Liggitt 2018-01-14 13:22:41 -05:00
parent aa504ccd57
commit 59c305b590
No known key found for this signature in database
GPG Key ID: 39928704103C7229
3 changed files with 372 additions and 40 deletions

View File

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

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
}