320 lines
9.5 KiB
Go
320 lines
9.5 KiB
Go
/*
|
|
Copyright 2017 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 apiserver
|
|
|
|
import (
|
|
"math/rand"
|
|
"reflect"
|
|
"testing"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
|
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
|
"k8s.io/apimachinery/pkg/api/equality"
|
|
metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
|
"k8s.io/apimachinery/pkg/util/diff"
|
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
|
)
|
|
|
|
func TestConvertFieldLabel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
clusterScoped bool
|
|
label string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "cluster scoped - name is ok",
|
|
clusterScoped: true,
|
|
label: "metadata.name",
|
|
},
|
|
{
|
|
name: "cluster scoped - namespace is not ok",
|
|
clusterScoped: true,
|
|
label: "metadata.namespace",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "cluster scoped - other field is not ok",
|
|
clusterScoped: true,
|
|
label: "some.other.field",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "namespace scoped - name is ok",
|
|
label: "metadata.name",
|
|
},
|
|
{
|
|
name: "namespace scoped - namespace is ok",
|
|
label: "metadata.namespace",
|
|
},
|
|
{
|
|
name: "namespace scoped - other field is not ok",
|
|
label: "some.other.field",
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
crd := apiextensions.CustomResourceDefinition{
|
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
|
Conversion: &apiextensions.CustomResourceConversion{
|
|
Strategy: "None",
|
|
},
|
|
},
|
|
}
|
|
|
|
if test.clusterScoped {
|
|
crd.Spec.Scope = apiextensions.ClusterScoped
|
|
} else {
|
|
crd.Spec.Scope = apiextensions.NamespaceScoped
|
|
}
|
|
f, err := conversion.NewCRConverterFactory(nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, c, err := f.NewConverter(&crd)
|
|
|
|
label, value, err := c.ConvertFieldLabel(schema.GroupVersionKind{}, test.label, "value")
|
|
if e, a := test.expectError, err != nil; e != a {
|
|
t.Fatalf("err: expected %t, got %t", e, a)
|
|
}
|
|
if test.expectError {
|
|
if e, a := "field label not supported: "+test.label, err.Error(); e != a {
|
|
t.Errorf("err: expected %s, got %s", e, a)
|
|
}
|
|
return
|
|
}
|
|
|
|
if e, a := test.label, label; e != a {
|
|
t.Errorf("label: expected %s, got %s", e, a)
|
|
}
|
|
if e, a := "value", value; e != a {
|
|
t.Errorf("value: expected %s, got %s", e, a)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRoundtripObjectMeta(t *testing.T) {
|
|
scheme := runtime.NewScheme()
|
|
codecs := serializer.NewCodecFactory(scheme)
|
|
codec := json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false)
|
|
seed := rand.Int63()
|
|
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(seed), codecs)
|
|
|
|
N := 1000
|
|
for i := 0; i < N; i++ {
|
|
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
|
original := &metav1.ObjectMeta{}
|
|
fuzzer.Fuzz(original)
|
|
if err := setObjectMeta(u, original); err != nil {
|
|
t.Fatalf("unexpected error setting ObjectMeta: %v", err)
|
|
}
|
|
o, _, err := getObjectMeta(u, false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error getting the Objectmeta: %v", err)
|
|
}
|
|
|
|
if !equality.Semantic.DeepEqual(original, o) {
|
|
t.Errorf("diff: %v\nCodec: %#v", diff.ObjectReflectDiff(original, o), codec)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestMalformedObjectMetaFields sets a number of different random values and types for all
|
|
// metadata fields. If json.Unmarshal accepts them, compare that getObjectMeta
|
|
// gives the same result. Otherwise, drop malformed fields.
|
|
func TestMalformedObjectMetaFields(t *testing.T) {
|
|
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(rand.Int63()), serializer.NewCodecFactory(runtime.NewScheme()))
|
|
spuriousValues := func() []interface{} {
|
|
return []interface{}{
|
|
// primitives
|
|
nil,
|
|
int64(1),
|
|
float64(1.5),
|
|
true,
|
|
"a",
|
|
// well-formed complex values
|
|
[]interface{}{"a", "b"},
|
|
map[string]interface{}{"a": "1", "b": "2"},
|
|
[]interface{}{int64(1), int64(2)},
|
|
[]interface{}{float64(1.5), float64(2.5)},
|
|
// known things json decoding tolerates
|
|
map[string]interface{}{"a": "1", "b": nil},
|
|
// malformed things
|
|
map[string]interface{}{"a": "1", "b": []interface{}{"nested"}},
|
|
[]interface{}{"a", int64(1), float64(1.5), true, []interface{}{"nested"}},
|
|
}
|
|
}
|
|
N := 100
|
|
for i := 0; i < N; i++ {
|
|
fuzzedObjectMeta := &metav1.ObjectMeta{}
|
|
fuzzer.Fuzz(fuzzedObjectMeta)
|
|
goodMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, pth := range jsonPaths(nil, goodMetaMap) {
|
|
for _, v := range spuriousValues() {
|
|
// skip values of same type, because they can only cause decoding errors further insides
|
|
orig, err := JsonPathValue(goodMetaMap, pth, 0)
|
|
if err != nil {
|
|
t.Fatalf("unexpected to not find something at %v: %v", pth, err)
|
|
}
|
|
if reflect.TypeOf(v) == reflect.TypeOf(orig) {
|
|
continue
|
|
}
|
|
|
|
// make a spurious map
|
|
spuriousMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := SetJsonPath(spuriousMetaMap, pth, 0, v); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// See if it can unmarshal to object meta
|
|
spuriousJSON, err := encodingjson.Marshal(spuriousMetaMap)
|
|
if err != nil {
|
|
t.Fatalf("error on %v=%#v: %v", pth, v, err)
|
|
}
|
|
expectedObjectMeta := &metav1.ObjectMeta{}
|
|
if err := encodingjson.Unmarshal(spuriousJSON, expectedObjectMeta); err != nil {
|
|
// if standard json unmarshal would fail decoding this field, drop the field entirely
|
|
truncatedMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// we expect this logic for the different fields:
|
|
switch {
|
|
default:
|
|
// delete complete top-level field by default
|
|
DeleteJsonPath(truncatedMetaMap, pth[:1], 0)
|
|
}
|
|
|
|
truncatedJSON, err := encodingjson.Marshal(truncatedMetaMap)
|
|
if err != nil {
|
|
t.Fatalf("error on %v=%#v: %v", pth, v, err)
|
|
}
|
|
expectedObjectMeta = &metav1.ObjectMeta{}
|
|
if err := encodingjson.Unmarshal(truncatedJSON, expectedObjectMeta); err != nil {
|
|
t.Fatalf("error on %v=%#v: %v", pth, v, err)
|
|
}
|
|
}
|
|
|
|
// make sure dropInvalidTypedFields+getObjectMeta matches what we expect
|
|
u := &unstructured.Unstructured{Object: map[string]interface{}{"metadata": spuriousMetaMap}}
|
|
actualObjectMeta, _, err := getObjectMeta(u, true)
|
|
if err != nil {
|
|
t.Errorf("got unexpected error after dropping invalid typed fields on %v=%#v: %v", pth, v, err)
|
|
continue
|
|
}
|
|
|
|
if !equality.Semantic.DeepEqual(expectedObjectMeta, actualObjectMeta) {
|
|
t.Errorf("%v=%#v, diff: %v\n", pth, v, diff.ObjectReflectDiff(expectedObjectMeta, actualObjectMeta))
|
|
t.Errorf("expectedObjectMeta %#v", expectedObjectMeta)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetObjectMetaNils(t *testing.T) {
|
|
u := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"kind": "Pod",
|
|
"apiVersion": "v1",
|
|
"metadata": map[string]interface{}{
|
|
"generateName": nil,
|
|
"labels": map[string]interface{}{
|
|
"foo": nil,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
o, _, err := getObjectMeta(u, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o.GenerateName != "" {
|
|
t.Errorf("expected null json value to be read as \"\" string, but got: %q", o.GenerateName)
|
|
}
|
|
if got, expected := o.Labels, map[string]string{"foo": ""}; !reflect.DeepEqual(got, expected) {
|
|
t.Errorf("unexpected labels, expected=%#v, got=%#v", expected, got)
|
|
}
|
|
|
|
// double check this what the kube JSON decode is doing
|
|
bs, _ := encodingjson.Marshal(u.UnstructuredContent())
|
|
kubeObj, _, err := clientgoscheme.Codecs.UniversalDecoder(corev1.SchemeGroupVersion).Decode(bs, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pod, ok := kubeObj.(*corev1.Pod)
|
|
if !ok {
|
|
t.Fatalf("expected v1 Pod, got: %T", pod)
|
|
}
|
|
if got, expected := o.GenerateName, pod.ObjectMeta.GenerateName; got != expected {
|
|
t.Errorf("expected generatedName to be %q, got %q", expected, got)
|
|
}
|
|
if got, expected := o.Labels, pod.ObjectMeta.Labels; !reflect.DeepEqual(got, expected) {
|
|
t.Errorf("expected labels to be %v, got %v", expected, got)
|
|
}
|
|
}
|
|
|
|
func TestGetObjectMeta(t *testing.T) {
|
|
for i := 0; i < 100; i++ {
|
|
u := &unstructured.Unstructured{Object: map[string]interface{}{
|
|
"metadata": map[string]interface{}{
|
|
"name": "good",
|
|
"Name": "bad1",
|
|
"nAme": "bad2",
|
|
"naMe": "bad3",
|
|
"namE": "bad4",
|
|
|
|
"namespace": "good",
|
|
"Namespace": "bad1",
|
|
"nAmespace": "bad2",
|
|
"naMespace": "bad3",
|
|
"namEspace": "bad4",
|
|
|
|
"creationTimestamp": "a",
|
|
},
|
|
}}
|
|
|
|
meta, _, err := getObjectMeta(u, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if meta.Name != "good" || meta.Namespace != "good" {
|
|
t.Fatalf("got %#v", meta)
|
|
}
|
|
}
|
|
}
|