Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update fake dynamic client to return GVK #96020

Merged
merged 4 commits into from Nov 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 0 additions & 33 deletions staging/src/k8s.io/apimachinery/pkg/runtime/negotiate.go
Expand Up @@ -92,39 +92,6 @@ func NewClientNegotiator(serializer NegotiatedSerializer, gv schema.GroupVersion
}
}

// NewInternalClientNegotiator applies the default client rules for connecting to a Kubernetes apiserver
// where objects are converted to gv prior to sending and decoded to their internal representation prior
// to retrieval.
//
// DEPRECATED: Internal clients are deprecated and will be removed in a future Kubernetes release.
func NewInternalClientNegotiator(serializer NegotiatedSerializer, gv schema.GroupVersion) ClientNegotiator {
decode := schema.GroupVersions{
{
Group: gv.Group,
Version: APIVersionInternal,
},
// always include the legacy group as a decoding target to handle non-error `Status` return types
{
Group: "",
Version: APIVersionInternal,
},
}
return &clientNegotiator{
encode: gv,
decode: decode,
serializer: serializer,
}
}

// NewSimpleClientNegotiator will negotiate for a single serializer. This should only be used
// for testing or when the caller is taking responsibility for setting the GVK on encoded objects.
func NewSimpleClientNegotiator(info SerializerInfo, gv schema.GroupVersion) ClientNegotiator {
return &clientNegotiator{
serializer: &simpleNegotiatedSerializer{info: info},
encode: gv,
}
}

type simpleNegotiatedSerializer struct {
info SerializerInfo
}
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
64 changes: 54 additions & 10 deletions staging/src/k8s.io/client-go/dynamic/fake/simple.go
Expand Up @@ -18,6 +18,7 @@ package fake

import (
"context"
"fmt"
"strings"

"k8s.io/apimachinery/pkg/api/meta"
Expand All @@ -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())
Expand All @@ -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()
Expand All @@ -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 {
Expand Down Expand Up @@ -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"})

}

Expand All @@ -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)
Expand Down
109 changes: 108 additions & 1 deletion staging/src/k8s.io/client-go/dynamic/fake/simple_test.go
Expand Up @@ -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"),
Expand All @@ -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
Expand Down
9 changes: 8 additions & 1 deletion staging/src/k8s.io/client-go/kubernetes_test/BUILD
Expand Up @@ -2,11 +2,18 @@ load("@io_bazel_rules_go//go:def.bzl", "go_test")

go_test(
name = "go_default_test",
srcs = ["timeout_test.go"],
srcs = [
"fake_client_test.go",
"timeout_test.go",
],
deps = [
"//staging/src/k8s.io/api/apps/v1:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library",
"//staging/src/k8s.io/client-go/rest:go_default_library",
"//staging/src/k8s.io/client-go/rest/fake:go_default_library",
Expand Down