Merge pull request #45431 from DirectXMan12/feature/fake-dynamic-client

Automatic merge from submit-queue (batch tested with PRs 48224, 45431, 45946, 48775, 49396)

[client-go] Fake Dynamic Client

This commit converts `"k8s.io/client-go/dynamic".Client` to an interface, and implements fake versions of both `ClientPool` and `Client`.  This allows components which make uses of these clients to be tested in the same way that clientset-based components can be tested, using the standard `testing.Fake` machinery.

**Release note**:
```release-note
NONE
```
This commit is contained in:
Kubernetes Submit Queue 2017-07-25 11:06:49 -07:00 committed by GitHub
commit 393b1228c9
14 changed files with 298 additions and 23 deletions

View File

@ -690,6 +690,7 @@ staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook
staging/src/k8s.io/client-go/discovery/cached
staging/src/k8s.io/client-go/discovery/fake
staging/src/k8s.io/client-go/dynamic
staging/src/k8s.io/client-go/dynamic/fake
staging/src/k8s.io/client-go/examples/workqueue
staging/src/k8s.io/client-go/informers/internalinterfaces
staging/src/k8s.io/client-go/kubernetes

View File

@ -97,7 +97,7 @@ type GraphBuilder struct {
ignoredResources map[schema.GroupResource]struct{}
}
func listWatcher(client *dynamic.Client, resource schema.GroupVersionResource) *cache.ListWatch {
func listWatcher(client dynamic.Interface, resource schema.GroupVersionResource) *cache.ListWatch {
return &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
// APIResource.Kind is not used by the dynamic client, so

View File

@ -46,7 +46,7 @@ func NewRegisteredRateLimiter(resources map[schema.GroupVersionResource]struct{}
return &RegisteredRateLimiter{rateLimiters: rateLimiters}
}
func (r *RegisteredRateLimiter) registerIfNotPresent(gv schema.GroupVersion, client *dynamic.Client, prefix string) {
func (r *RegisteredRateLimiter) registerIfNotPresent(gv schema.GroupVersion, client dynamic.Interface, prefix string) {
once, found := r.rateLimiters[gv]
if !found {
return

View File

@ -325,7 +325,7 @@ func (d *namespacedResourcesDeleter) finalizeNamespace(namespace *v1.Namespace)
// it returns true if the operation was supported on the server.
// it returns an error if the operation was supported on the server but was unable to complete.
func (d *namespacedResourcesDeleter) deleteCollection(
dynamicClient *dynamic.Client, gvr schema.GroupVersionResource,
dynamicClient dynamic.Interface, gvr schema.GroupVersionResource,
namespace string) (bool, error) {
glog.V(5).Infof("namespace controller - deleteCollection - namespace: %s, gvr: %v", namespace, gvr)
@ -370,7 +370,7 @@ func (d *namespacedResourcesDeleter) deleteCollection(
// a boolean if the operation is supported
// an error if the operation is supported but could not be completed.
func (d *namespacedResourcesDeleter) listCollection(
dynamicClient *dynamic.Client, gvr schema.GroupVersionResource, namespace string) (*unstructured.UnstructuredList, bool, error) {
dynamicClient dynamic.Interface, gvr schema.GroupVersionResource, namespace string) (*unstructured.UnstructuredList, bool, error) {
glog.V(5).Infof("namespace controller - listCollection - namespace: %s, gvr: %v", namespace, gvr)
key := operationKey{operation: operationList, gvr: gvr}
@ -406,7 +406,7 @@ func (d *namespacedResourcesDeleter) listCollection(
// deleteEachItem is a helper function that will list the collection of resources and delete each item 1 by 1.
func (d *namespacedResourcesDeleter) deleteEachItem(
dynamicClient *dynamic.Client, gvr schema.GroupVersionResource, namespace string) error {
dynamicClient dynamic.Interface, gvr schema.GroupVersionResource, namespace string) error {
glog.V(5).Infof("namespace controller - deleteEachItem - namespace: %s, gvr: %v", namespace, gvr)
unstructuredList, listSupported, err := d.listCollection(dynamicClient, gvr, namespace)

View File

@ -180,13 +180,13 @@ func DescriberFor(kind schema.GroupKind, c clientset.Interface) (printers.Descri
// GenericDescriberFor returns a generic describer for the specified mapping
// that uses only information available from runtime.Unstructured
func GenericDescriberFor(mapping *meta.RESTMapping, dynamic *dynamic.Client, events coreclient.EventsGetter) printers.Describer {
func GenericDescriberFor(mapping *meta.RESTMapping, dynamic dynamic.Interface, events coreclient.EventsGetter) printers.Describer {
return &genericDescriber{mapping, dynamic, events}
}
type genericDescriber struct {
mapping *meta.RESTMapping
dynamic *dynamic.Client
dynamic dynamic.Interface
events coreclient.EventsGetter
}

View File

@ -75,7 +75,7 @@ func TestClusterScopedCRUD(t *testing.T) {
testSimpleCRUD(t, ns, noxuDefinition, noxuVersionClient)
}
func testSimpleCRUD(t *testing.T, ns string, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition, noxuVersionClient *dynamic.Client) {
func testSimpleCRUD(t *testing.T, ns string, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition, noxuVersionClient dynamic.Interface) {
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
initialList, err := noxuResourceClient.List(metav1.ListOptions{})
if err != nil {
@ -501,7 +501,7 @@ func TestCrossNamespaceListWatch(t *testing.T) {
checkNamespacesWatchHelper(t, ns2, noxuNamespacesWatch2)
}
func createInstanceWithNamespaceHelper(t *testing.T, ns string, name string, noxuNamespacedResourceClient *dynamic.ResourceClient, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition) *unstructured.Unstructured {
func createInstanceWithNamespaceHelper(t *testing.T, ns string, name string, noxuNamespacedResourceClient dynamic.ResourceInterface, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition) *unstructured.Unstructured {
createdInstance, err := instantiateCustomResource(t, testserver.NewNoxuInstance(ns, name), noxuNamespacedResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu Instance:%v", err)

View File

@ -39,7 +39,7 @@ import (
"k8s.io/client-go/dynamic"
)
func instantiateCustomResource(t *testing.T, instanceToCreate *unstructured.Unstructured, client *dynamic.ResourceClient, definition *apiextensionsv1beta1.CustomResourceDefinition) (*unstructured.Unstructured, error) {
func instantiateCustomResource(t *testing.T, instanceToCreate *unstructured.Unstructured, client dynamic.ResourceInterface, definition *apiextensionsv1beta1.CustomResourceDefinition) (*unstructured.Unstructured, error) {
createdInstance, err := client.Create(instanceToCreate)
if err != nil {
t.Logf("%#v", createdInstance)
@ -66,7 +66,7 @@ func instantiateCustomResource(t *testing.T, instanceToCreate *unstructured.Unst
return createdInstance, nil
}
func NewNamespacedCustomResourceClient(ns string, client *dynamic.Client, definition *apiextensionsv1beta1.CustomResourceDefinition) *dynamic.ResourceClient {
func NewNamespacedCustomResourceClient(ns string, client dynamic.Interface, definition *apiextensionsv1beta1.CustomResourceDefinition) dynamic.ResourceInterface {
return client.Resource(&metav1.APIResource{
Name: definition.Spec.Names.Plural,
Namespaced: definition.Spec.Scope == apiextensionsv1beta1.NamespaceScoped,

View File

@ -141,7 +141,7 @@ func NewCurletInstance(namespace, name string) *unstructured.Unstructured {
}
}
func CreateNewCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefinition, apiExtensionsClient clientset.Interface, clientPool dynamic.ClientPool) (*dynamic.Client, error) {
func CreateNewCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefinition, apiExtensionsClient clientset.Interface, clientPool dynamic.ClientPool) (dynamic.Interface, error) {
_, err := apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Create(crd)
if err != nil {
return nil, err
@ -194,7 +194,7 @@ func CreateNewCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceD
return dynamicClient, nil
}
func checkForWatchCachePrimed(crd *apiextensionsv1beta1.CustomResourceDefinition, dynamicClient *dynamic.Client) error {
func checkForWatchCachePrimed(crd *apiextensionsv1beta1.CustomResourceDefinition, dynamicClient dynamic.Interface) error {
ns := ""
if crd.Spec.Scope != apiextensionsv1beta1.ClusterScoped {
ns = "aval"

View File

@ -40,8 +40,42 @@ import (
"k8s.io/client-go/util/flowcontrol"
)
// Client is a Kubernetes client that allows you to access metadata
// Interface is a Kubernetes client that allows you to access metadata
// and manipulate metadata of a Kubernetes API group.
type Interface interface {
// GetRateLimiter returns the rate limiter for this client.
GetRateLimiter() flowcontrol.RateLimiter
// Resource returns an API interface to the specified resource for this client's
// group and version. If resource is not a namespaced resource, then namespace
// is ignored. The ResourceInterface inherits the paramater codec of this client.
Resource(resource *metav1.APIResource, namespace string) ResourceInterface
// ParameterCodec returns a client with the provided parameter codec.
ParameterCodec(parameterCodec runtime.ParameterCodec) Interface
}
// ResourceInterface is an API interface to a specific resource under a
// dynamic client.
type ResourceInterface interface {
// List returns a list of objects for this resource.
List(opts metav1.ListOptions) (runtime.Object, error)
// Get gets the resource with the specified name.
Get(name string, opts metav1.GetOptions) (*unstructured.Unstructured, error)
// Delete deletes the resource with the specified name.
Delete(name string, opts *metav1.DeleteOptions) error
// DeleteCollection deletes a collection of objects.
DeleteCollection(deleteOptions *metav1.DeleteOptions, listOptions metav1.ListOptions) error
// Create creates the provided resource.
Create(obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
// Update updates the provided resource.
Update(obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
// Watch returns a watch.Interface that watches the resource.
Watch(opts metav1.ListOptions) (watch.Interface, error)
// Patch patches the provided resource.
Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error)
}
// Client is a Kubernetes client that allows you to access metadata
// and manipulate metadata of a Kubernetes API group, and implements Interface.
type Client struct {
cl *restclient.RESTClient
parameterCodec runtime.ParameterCodec
@ -84,8 +118,8 @@ func (c *Client) GetRateLimiter() flowcontrol.RateLimiter {
// Resource returns an API interface to the specified resource for this client's
// group and version. If resource is not a namespaced resource, then namespace
// is ignored. The ResourceClient inherits the parameter codec of c.
func (c *Client) Resource(resource *metav1.APIResource, namespace string) *ResourceClient {
// is ignored. The ResourceInterface inherits the parameter codec of c.
func (c *Client) Resource(resource *metav1.APIResource, namespace string) ResourceInterface {
return &ResourceClient{
cl: c.cl,
resource: resource,
@ -95,7 +129,7 @@ func (c *Client) Resource(resource *metav1.APIResource, namespace string) *Resou
}
// ParameterCodec returns a client with the provided parameter codec.
func (c *Client) ParameterCodec(parameterCodec runtime.ParameterCodec) *Client {
func (c *Client) ParameterCodec(parameterCodec runtime.ParameterCodec) Interface {
return &Client{
cl: c.cl,
parameterCodec: parameterCodec,
@ -103,7 +137,7 @@ func (c *Client) ParameterCodec(parameterCodec runtime.ParameterCodec) *Client {
}
// ResourceClient is an API interface to a specific resource under a
// dynamic client.
// dynamic client, and implements ResourceInterface.
type ResourceClient struct {
cl *restclient.RESTClient
resource *metav1.APIResource

View File

@ -28,10 +28,10 @@ import (
type ClientPool interface {
// ClientForGroupVersionKind returns a client configured for the specified groupVersionResource.
// Resource may be empty.
ClientForGroupVersionResource(resource schema.GroupVersionResource) (*Client, error)
ClientForGroupVersionResource(resource schema.GroupVersionResource) (Interface, error)
// ClientForGroupVersionKind returns a client configured for the specified groupVersionKind.
// Kind may be empty.
ClientForGroupVersionKind(kind schema.GroupVersionKind) (*Client, error)
ClientForGroupVersionKind(kind schema.GroupVersionKind) (Interface, error)
}
// APIPathResolverFunc knows how to convert a groupVersion to its API path. The Kind field is
@ -79,7 +79,7 @@ func NewDynamicClientPool(cfg *restclient.Config) ClientPool {
// ClientForGroupVersionResource uses the provided RESTMapper to identify the appropriate resource. Resource may
// be empty. If no matching kind is found the underlying client for that group is still returned.
func (c *clientPoolImpl) ClientForGroupVersionResource(resource schema.GroupVersionResource) (*Client, error) {
func (c *clientPoolImpl) ClientForGroupVersionResource(resource schema.GroupVersionResource) (Interface, error) {
kinds, err := c.mapper.KindsFor(resource)
if err != nil {
if meta.IsNoMatchError(err) {
@ -92,7 +92,7 @@ func (c *clientPoolImpl) ClientForGroupVersionResource(resource schema.GroupVers
// ClientForGroupVersion returns a client for the specified groupVersion, creates one if none exists. Kind
// in the GroupVersionKind may be empty.
func (c *clientPoolImpl) ClientForGroupVersionKind(kind schema.GroupVersionKind) (*Client, error) {
func (c *clientPoolImpl) ClientForGroupVersionKind(kind schema.GroupVersionKind) (Interface, error) {
c.lock.Lock()
defer c.lock.Unlock()

View File

@ -58,7 +58,7 @@ func getObject(version, kind, name string) *unstructured.Unstructured {
}
}
func getClientServer(gv *schema.GroupVersion, h func(http.ResponseWriter, *http.Request)) (*Client, *httptest.Server, error) {
func getClientServer(gv *schema.GroupVersion, h func(http.ResponseWriter, *http.Request)) (Interface, *httptest.Server, error) {
srv := httptest.NewServer(http.HandlerFunc(h))
cl, err := NewClient(&restclient.Config{
Host: srv.URL,

View File

@ -0,0 +1,29 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"client.go",
"client_pool.go",
],
tags = ["automanaged"],
deps = [
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library",
"//vendor/k8s.io/client-go/util/flowcontrol:go_default_library",
],
)

View File

@ -0,0 +1,163 @@
/*
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 fake provides a fake client interface to arbitrary Kubernetes
// APIs that exposes common high level operations and exposes common
// metadata.
package fake
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/testing"
"k8s.io/client-go/util/flowcontrol"
)
// FakeClient is a fake implementation of dynamic.Interface.
type FakeClient struct {
GroupVersion schema.GroupVersion
*testing.Fake
}
// GetRateLimiter returns the rate limiter for this client.
func (c *FakeClient) GetRateLimiter() flowcontrol.RateLimiter {
return nil
}
// Resource returns an API interface to the specified resource for this client's
// group and version. If resource is not a namespaced resource, then namespace
// is ignored. The ResourceClient inherits the paramater codec of this client
func (c *FakeClient) Resource(resource *metav1.APIResource, namespace string) dynamic.ResourceInterface {
return &FakeResourceClient{
Resource: c.GroupVersion.WithResource(resource.Name),
Kind: c.GroupVersion.WithKind(resource.Kind),
Namespace: namespace,
Fake: c.Fake,
}
}
// ParameterCodec returns a client with the provided parameter codec.
func (c *FakeClient) ParameterCodec(parameterCodec runtime.ParameterCodec) dynamic.Interface {
return &FakeClient{
Fake: c.Fake,
}
}
// FakeResourceClient is a fake implementation of dynamic.ResourceInterface
type FakeResourceClient struct {
Resource schema.GroupVersionResource
Kind schema.GroupVersionKind
Namespace string
*testing.Fake
}
// List returns a list of objects for this resource.
func (c *FakeResourceClient) List(opts metav1.ListOptions) (runtime.Object, error) {
obj, err := c.Fake.
Invokes(testing.NewListAction(c.Resource, c.Kind, c.Namespace, opts), &unstructured.UnstructuredList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &unstructured.UnstructuredList{}
for _, item := range obj.(*unstructured.UnstructuredList).Items {
if label.Matches(labels.Set(item.GetLabels())) {
list.Items = append(list.Items, item)
}
}
return list, err
}
// Get gets the resource with the specified name.
func (c *FakeResourceClient) Get(name string, opts metav1.GetOptions) (*unstructured.Unstructured, error) {
obj, err := c.Fake.
Invokes(testing.NewGetAction(c.Resource, c.Namespace, name), &unstructured.Unstructured{})
if obj == nil {
return nil, err
}
return obj.(*unstructured.Unstructured), err
}
// Delete deletes the resource with the specified name.
func (c *FakeResourceClient) Delete(name string, opts *metav1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewDeleteAction(c.Resource, c.Namespace, name), &unstructured.Unstructured{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakeResourceClient) DeleteCollection(deleteOptions *metav1.DeleteOptions, listOptions metav1.ListOptions) error {
_, err := c.Fake.
Invokes(testing.NewDeleteCollectionAction(c.Resource, c.Namespace, listOptions), &unstructured.Unstructured{})
return err
}
// Create creates the provided resource.
func (c *FakeResourceClient) Create(inObj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
obj, err := c.Fake.
Invokes(testing.NewCreateAction(c.Resource, c.Namespace, inObj), &unstructured.Unstructured{})
if obj == nil {
return nil, err
}
return obj.(*unstructured.Unstructured), err
}
// Update updates the provided resource.
func (c *FakeResourceClient) Update(inObj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
obj, err := c.Fake.
Invokes(testing.NewUpdateAction(c.Resource, c.Namespace, inObj), &unstructured.Unstructured{})
if obj == nil {
return nil, err
}
return obj.(*unstructured.Unstructured), err
}
// Watch returns a watch.Interface that watches the resource.
func (c *FakeResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewWatchAction(c.Resource, c.Namespace, opts))
}
// Patch patches the provided resource.
func (c *FakeResourceClient) Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error) {
obj, err := c.Fake.
Invokes(testing.NewPatchAction(c.Resource, c.Namespace, name, data), &unstructured.Unstructured{})
if obj == nil {
return nil, err
}
return obj.(*unstructured.Unstructured), err
}

View File

@ -0,0 +1,48 @@
/*
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 fake provides a fake client interface to arbitrary Kubernetes
// APIs that exposes common high level operations and exposes common
// metadata.
package fake
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/testing"
)
// FakeClientPool provides a fake implementation of dynamic.ClientPool.
// It assumes resource GroupVersions are the same as their corresponding kind GroupVersions.
type FakeClientPool struct {
testing.Fake
}
// ClientForGroupVersionKind returns a client configured for the specified groupVersionResource.
// Resource may be empty.
func (p *FakeClientPool) ClientForGroupVersionResource(resource schema.GroupVersionResource) (dynamic.Interface, error) {
return p.ClientForGroupVersionKind(resource.GroupVersion().WithKind(""))
}
// ClientForGroupVersionKind returns a client configured for the specified groupVersionKind.
// Kind may be empty.
func (p *FakeClientPool) ClientForGroupVersionKind(kind schema.GroupVersionKind) (dynamic.Interface, error) {
// we can just create a new client every time for testing purposes
return &FakeClient{
GroupVersion: kind.GroupVersion(),
Fake: &p.Fake,
}, nil
}