Merge pull request #96020 from deads2k/dynamiclist

update fake dynamic client to return GVK

Kubernetes-commit: dbd2be08fb0515599aadb897e0c1476abc8b2552
This commit is contained in:
Kubernetes Publisher
2020-11-04 18:11:06 -08:00
7 changed files with 271 additions and 17 deletions

2
Godeps/Godeps.json generated
View File

@@ -452,7 +452,7 @@
},
{
"ImportPath": "k8s.io/apimachinery",
"Rev": "79ef3cbd919a"
"Rev": "d67967d70958"
},
{
"ImportPath": "k8s.io/gengo",

View File

@@ -95,7 +95,12 @@ func TestFilteredDynamicSharedInformerFactory(t *testing.T) {
if ts.existingObj != nil {
objs = append(objs, ts.existingObj)
}
fakeClient := fake.NewSimpleDynamicClient(scheme, objs...)
// don't adjust the scheme to include deploymentlist. This is testing whether an informer can be created against using
// a client that doesn't have a type registered in the scheme.
gvrToListKind := map[schema.GroupVersionResource]string{
{Group: "apps", Version: "v1", Resource: "deployments"}: "DeploymentList",
}
fakeClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, objs...)
target := dynamicinformer.NewFilteredDynamicSharedInformerFactory(fakeClient, 0, ts.informNS, nil)
// act
@@ -214,7 +219,12 @@ func TestDynamicSharedInformerFactory(t *testing.T) {
if ts.existingObj != nil {
objs = append(objs, ts.existingObj)
}
fakeClient := fake.NewSimpleDynamicClient(scheme, objs...)
// don't adjust the scheme to include deploymentlist. This is testing whether an informer can be created against using
// a client that doesn't have a type registered in the scheme.
gvrToListKind := map[schema.GroupVersionResource]string{
{Group: "extensions", Version: "v1beta1", Resource: "deployments"}: "DeploymentList",
}
fakeClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, objs...)
target := dynamicinformer.NewDynamicSharedInformerFactory(fakeClient, 0)
// act

View File

@@ -18,6 +18,7 @@ package fake
import (
"context"
"fmt"
"strings"
"k8s.io/apimachinery/pkg/api/meta"
@@ -34,11 +35,45 @@ import (
)
func NewSimpleDynamicClient(scheme *runtime.Scheme, objects ...runtime.Object) *FakeDynamicClient {
// In order to use List with this client, you have to have the v1.List registered in your scheme. Neat thing though
// it does NOT have to be the *same* list. UnstructuredList returned from this fake client will NOT have apiVersion and kind set,
// but each Unstructured object in Items will preserve their respective apiVersion and kind. As a result, schema conversion for
// *List kinds will not work and conversion of each Unstructured object in Items will be required instead.
scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "fake-dynamic-client-group", Version: "v1", Kind: "List"}, &unstructured.UnstructuredList{})
return NewSimpleDynamicClientWithCustomListKinds(scheme, nil, objects...)
}
// NewSimpleDynamicClientWithCustomListKinds try not to use this. In general you want to have the scheme have the List types registered
// and allow the default guessing for resources match. Sometimes that doesn't work, so you can specify a custom mapping here.
func NewSimpleDynamicClientWithCustomListKinds(scheme *runtime.Scheme, gvrToListKind map[schema.GroupVersionResource]string, objects ...runtime.Object) *FakeDynamicClient {
// In order to use List with this client, you have to have your lists registered so that the object tracker will find them
// in the scheme to support the t.scheme.New(listGVK) call when it's building the return value.
// Since the base fake client needs the listGVK passed through the action (in cases where there are no instances, it
// cannot look up the actual hits), we need to know a mapping of GVR to listGVK here. For GETs and other types of calls,
// there is no return value that contains a GVK, so it doesn't have to know the mapping in advance.
// first we attempt to invert known List types from the scheme to auto guess the resource with unsafe guesses
// this covers common usage of registering types in scheme and passing them
completeGVRToListKind := map[schema.GroupVersionResource]string{}
for listGVK := range scheme.AllKnownTypes() {
if !strings.HasSuffix(listGVK.Kind, "List") {
continue
}
nonListGVK := listGVK.GroupVersion().WithKind(listGVK.Kind[:len(listGVK.Kind)-4])
plural, _ := meta.UnsafeGuessKindToResource(nonListGVK)
completeGVRToListKind[plural] = listGVK.Kind
}
for gvr, listKind := range gvrToListKind {
if !strings.HasSuffix(listKind, "List") {
panic("coding error, listGVK must end in List or this fake client doesn't work right")
}
listGVK := gvr.GroupVersion().WithKind(listKind)
// if we already have this type registered, just skip it
if _, err := scheme.New(listGVK); err == nil {
completeGVRToListKind[gvr] = listKind
continue
}
scheme.AddKnownTypeWithName(listGVK, &unstructured.UnstructuredList{})
completeGVRToListKind[gvr] = listKind
}
codecs := serializer.NewCodecFactory(scheme)
o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
@@ -48,7 +83,7 @@ func NewSimpleDynamicClient(scheme *runtime.Scheme, objects ...runtime.Object) *
}
}
cs := &FakeDynamicClient{scheme: scheme}
cs := &FakeDynamicClient{scheme: scheme, gvrToListKind: completeGVRToListKind}
cs.AddReactor("*", "*", testing.ObjectReaction(o))
cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
gvr := action.GetResource()
@@ -68,19 +103,21 @@ func NewSimpleDynamicClient(scheme *runtime.Scheme, objects ...runtime.Object) *
// you want to test easier.
type FakeDynamicClient struct {
testing.Fake
scheme *runtime.Scheme
scheme *runtime.Scheme
gvrToListKind map[schema.GroupVersionResource]string
}
type dynamicResourceClient struct {
client *FakeDynamicClient
namespace string
resource schema.GroupVersionResource
listKind string
}
var _ dynamic.Interface = &FakeDynamicClient{}
func (c *FakeDynamicClient) Resource(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface {
return &dynamicResourceClient{client: c, resource: resource}
return &dynamicResourceClient{client: c, resource: resource, listKind: c.gvrToListKind[resource]}
}
func (c *dynamicResourceClient) Namespace(ns string) dynamic.ResourceInterface {
@@ -276,16 +313,22 @@ func (c *dynamicResourceClient) Get(ctx context.Context, name string, opts metav
}
func (c *dynamicResourceClient) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
if len(c.listKind) == 0 {
panic(fmt.Sprintf("coding error: you must register resource to list kind for every resource you're going to LIST when creating the client. See NewSimpleDynamicClientWithCustomListKinds or register the list into the scheme: %v out of %v", c.resource, c.client.gvrToListKind))
}
listGVK := c.resource.GroupVersion().WithKind(c.listKind)
listForFakeClientGVK := c.resource.GroupVersion().WithKind(c.listKind[:len(c.listKind)-4]) /*base library appends List*/
var obj runtime.Object
var err error
switch {
case len(c.namespace) == 0:
obj, err = c.client.Fake.
Invokes(testing.NewRootListAction(c.resource, schema.GroupVersionKind{Group: "fake-dynamic-client-group", Version: "v1", Kind: "" /*List is appended by the tracker automatically*/}, opts), &metav1.Status{Status: "dynamic list fail"})
Invokes(testing.NewRootListAction(c.resource, listForFakeClientGVK, opts), &metav1.Status{Status: "dynamic list fail"})
case len(c.namespace) > 0:
obj, err = c.client.Fake.
Invokes(testing.NewListAction(c.resource, schema.GroupVersionKind{Group: "fake-dynamic-client-group", Version: "v1", Kind: "" /*List is appended by the tracker automatically*/}, c.namespace, opts), &metav1.Status{Status: "dynamic list fail"})
Invokes(testing.NewListAction(c.resource, listForFakeClientGVK, c.namespace, opts), &metav1.Status{Status: "dynamic list fail"})
}
@@ -309,6 +352,7 @@ func (c *dynamicResourceClient) List(ctx context.Context, opts metav1.ListOption
list := &unstructured.UnstructuredList{}
list.SetResourceVersion(entireList.GetResourceVersion())
list.GetObjectKind().SetGroupVersionKind(listGVK)
for i := range entireList.Items {
item := &entireList.Items[i]
metadata, err := meta.Accessor(item)

View File

@@ -59,10 +59,74 @@ func newUnstructuredWithSpec(spec map[string]interface{}) *unstructured.Unstruct
return u
}
func TestGet(t *testing.T) {
scheme := runtime.NewScheme()
client := NewSimpleDynamicClient(scheme, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"))
get, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).Namespace("ns-foo").Get(context.TODO(), "name-foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
expected := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "group/version",
"kind": "TheKind",
"metadata": map[string]interface{}{
"name": "name-foo",
"namespace": "ns-foo",
},
},
}
if !equality.Semantic.DeepEqual(get, expected) {
t.Fatal(diff.ObjectGoPrintDiff(expected, get))
}
}
func TestListDecoding(t *testing.T) {
// this the duplication of logic from the real List API. This will prove that our dynamic client actually returns the gvk
uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKindList", "items":[]}`))
if err != nil {
t.Fatal(err)
}
list := uncastObj.(*unstructured.UnstructuredList)
expectedList := &unstructured.UnstructuredList{
Object: map[string]interface{}{
"apiVersion": "group/version",
"kind": "TheKindList",
},
Items: []unstructured.Unstructured{},
}
if !equality.Semantic.DeepEqual(list, expectedList) {
t.Fatal(diff.ObjectGoPrintDiff(expectedList, list))
}
}
func TestGetDecoding(t *testing.T) {
// this the duplication of logic from the real Get API. This will prove that our dynamic client actually returns the gvk
uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKind"}`))
if err != nil {
t.Fatal(err)
}
get := uncastObj.(*unstructured.Unstructured)
expectedObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "group/version",
"kind": "TheKind",
},
}
if !equality.Semantic.DeepEqual(get, expectedObj) {
t.Fatal(diff.ObjectGoPrintDiff(expectedObj, get))
}
}
func TestList(t *testing.T) {
scheme := runtime.NewScheme()
client := NewSimpleDynamicClient(scheme,
client := NewSimpleDynamicClientWithCustomListKinds(scheme,
map[schema.GroupVersionResource]string{
{Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList",
},
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
newUnstructured("group2/version", "TheKind", "ns-foo", "name2-foo"),
newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
@@ -84,6 +148,49 @@ func TestList(t *testing.T) {
}
}
func Test_ListKind(t *testing.T) {
scheme := runtime.NewScheme()
client := NewSimpleDynamicClientWithCustomListKinds(scheme,
map[schema.GroupVersionResource]string{
{Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList",
},
&unstructured.UnstructuredList{
Object: map[string]interface{}{
"apiVersion": "group/version",
"kind": "TheKindList",
},
Items: []unstructured.Unstructured{
*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
},
},
)
listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(context.TODO(), metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
expectedList := &unstructured.UnstructuredList{
Object: map[string]interface{}{
"apiVersion": "group/version",
"kind": "TheKindList",
"metadata": map[string]interface{}{
"resourceVersion": "",
},
},
Items: []unstructured.Unstructured{
*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
},
}
if !equality.Semantic.DeepEqual(listFirst, expectedList) {
t.Fatal(diff.ObjectGoPrintDiff(expectedList, listFirst))
}
}
type patchTestCase struct {
name string
object runtime.Object

4
go.mod
View File

@@ -27,7 +27,7 @@ require (
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
k8s.io/api v0.0.0-20201104162213-01c5338f427f
k8s.io/apimachinery v0.0.0-20201104162036-79ef3cbd919a
k8s.io/apimachinery v0.0.0-20201105042036-d67967d70958
k8s.io/klog/v2 v2.4.0
k8s.io/utils v0.0.0-20200729134348-d5654de09c73
sigs.k8s.io/yaml v1.2.0
@@ -35,5 +35,5 @@ require (
replace (
k8s.io/api => k8s.io/api v0.0.0-20201104162213-01c5338f427f
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20201104162036-79ef3cbd919a
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20201105042036-d67967d70958
)

2
go.sum
View File

@@ -336,7 +336,7 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
k8s.io/api v0.0.0-20201104162213-01c5338f427f/go.mod h1:5iJ2LFNvkNfwL+E+lMewJWlnnT3Fpj1BYHa2Qb4fjrI=
k8s.io/apimachinery v0.0.0-20201104162036-79ef3cbd919a/go.mod h1:WC5jfah6jg0OD6SE11oSOCQHkfkLh7pwIYbwS/dZLe4=
k8s.io/apimachinery v0.0.0-20201105042036-d67967d70958/go.mod h1:WC5jfah6jg0OD6SE11oSOCQHkfkLh7pwIYbwS/dZLe4=
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=

View File

@@ -0,0 +1,93 @@
/*
Copyright 2020 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 kubernetes
import (
"context"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/scheme"
)
// This test proves that the kube fake client does not return GVKs. This is consistent with actual client (see tests below)
// and should not be changed unless the decoding behavior and somehow literal creation (`&corev1.ConfigMap{}`) behavior change.
func Test_ConfigMapFakeClient(t *testing.T) {
fakeKubeClient := fake.NewSimpleClientset(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "foo-ns", Name: "foo-name"}})
cm, err := fakeKubeClient.CoreV1().ConfigMaps("foo-ns").Get(context.TODO(), "foo-name", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
if cm.GetObjectKind().GroupVersionKind() != (schema.GroupVersionKind{}) {
t.Fatal(cm.GetObjectKind().GroupVersionKind())
}
cmList, err := fakeKubeClient.CoreV1().ConfigMaps("foo-ns").List(context.TODO(), metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if cmList.GetObjectKind().GroupVersionKind() != (schema.GroupVersionKind{}) {
t.Fatal(cmList.GetObjectKind().GroupVersionKind())
}
}
// This test checks decoding behavior for the actual client to ensure the fake client (tested above) is consistent.
func TestGetDecoding(t *testing.T) {
// this the duplication of logic from the real Get API for configmaps. This will prove that the generated client will not return a GVK
mediaTypes := scheme.Codecs.WithoutConversion().SupportedMediaTypes()
info, ok := runtime.SerializerInfoForMediaType(mediaTypes, "application/json")
if !ok {
t.Fatal("missing serializer")
}
decoder := scheme.Codecs.WithoutConversion().DecoderToVersion(info.Serializer, corev1.SchemeGroupVersion)
body := []byte(`{"apiVersion": "v1", "kind": "ConfigMap", "metadata":{"Namespace":"foo","Name":"bar"}}`)
obj := &corev1.ConfigMap{}
out, _, err := decoder.Decode(body, nil, obj)
if err != nil || out != obj {
t.Fatal(err)
}
if obj.GetObjectKind().GroupVersionKind() != (schema.GroupVersionKind{}) {
t.Fatal(obj.GetObjectKind().GroupVersionKind())
}
}
// This test checks decoding behavior for the actual client to ensure the fake client (tested above) is consistent.
func TestListDecoding(t *testing.T) {
// this the duplication of logic from the real Get API for configmaps. This will prove that the generated client will not return a GVK
mediaTypes := scheme.Codecs.WithoutConversion().SupportedMediaTypes()
info, ok := runtime.SerializerInfoForMediaType(mediaTypes, "application/json")
if !ok {
t.Fatal("missing serializer")
}
decoder := scheme.Codecs.WithoutConversion().DecoderToVersion(info.Serializer, corev1.SchemeGroupVersion)
body := []byte(`{"apiVersion": "v1", "kind": "ConfigMapList", "items":[]}`)
obj := &corev1.ConfigMapList{}
out, _, err := decoder.Decode(body, nil, obj)
if err != nil || out != obj {
t.Fatal(err)
}
if obj.GetObjectKind().GroupVersionKind() != (schema.GroupVersionKind{}) {
t.Fatal(obj.GetObjectKind().GroupVersionKind())
}
}