Enhance the fake Client

This change accomplishes the following:
* Add a constructor for fake.Clients
* Add the ResourceAccumulator type and several instances of
  ResourceAccumulators, each of which is intended to supply a fake.Client
  with arbitrary kubernetes resources.
* Add the client.Factory type, which provides an easier method of
  providing a fake.Client in place of a real one.

Change-Id: I97f5a613df3ca14bc4fdcf726d3e20c5413cbb5b
This commit is contained in:
Ian Howell 2020-04-08 13:50:25 -05:00
parent e9f8ac3ac3
commit e15f1218f6
5 changed files with 148 additions and 97 deletions

View File

@ -60,9 +60,6 @@ func TestDeploy(t *testing.T) {
infra.FileSystem = document.NewDocumentFs() infra.FileSystem = document.NewDocumentFs()
kctl := kubectl.NewKubectl(tf) kctl := kubectl.NewKubectl(tf)
tc := fake.Client{
MockKubectl: func() kubectl.Interface { return kctl },
}
tests := []struct { tests := []struct {
theInfra *initinfra.Infra theInfra *initinfra.Infra
@ -71,24 +68,22 @@ func TestDeploy(t *testing.T) {
expectedError error expectedError error
}{ }{
{ {
client: fake.Client{
MockKubectl: func() kubectl.Interface { client: fake.NewClient(fake.WithKubectl(
return kubectl.NewKubectl(k8sutils. kubectl.NewKubectl(k8sutils.
NewMockKubectlFactory(). NewMockKubectlFactory().
WithDynamicClientByError(nil, DynamicClientError)) WithDynamicClientByError(nil, DynamicClientError)))),
},
},
expectedError: DynamicClientError, expectedError: DynamicClientError,
}, },
{ {
expectedError: nil, expectedError: nil,
prune: false, prune: false,
client: tc, client: fake.NewClient(fake.WithKubectl(kctl)),
}, },
{ {
expectedError: nil, expectedError: nil,
prune: true, prune: true,
client: tc, client: fake.NewClient(fake.WithKubectl(kctl)),
}, },
} }

View File

@ -22,9 +22,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic"
dynamicFake "k8s.io/client-go/dynamic/fake"
"opendev.org/airship/airshipctl/pkg/cluster" "opendev.org/airship/airshipctl/pkg/cluster"
"opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/document"
@ -107,7 +104,7 @@ func TestGetStatusForResource(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
selector document.Selector selector document.Selector
testClient fake.Client testClient *fake.Client
expectedStatus cluster.Status expectedStatus cluster.Status
err error err error
}{ }{
@ -116,7 +113,9 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector(). selector: document.NewSelector().
ByGvk("example.com", "v1", "Resource"). ByGvk("example.com", "v1", "Resource").
ByName("stable-resource"), ByName("stable-resource"),
testClient: makeTestClient(makeResource("Resource", "stable-resource", "stable")), testClient: fake.NewClient(
fake.WithDynamicObjects(makeResource("Resource", "stable-resource", "stable")),
),
expectedStatus: cluster.Status("Stable"), expectedStatus: cluster.Status("Stable"),
}, },
{ {
@ -124,7 +123,9 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector(). selector: document.NewSelector().
ByGvk("example.com", "v1", "Resource"). ByGvk("example.com", "v1", "Resource").
ByName("pending-resource"), ByName("pending-resource"),
testClient: makeTestClient(makeResource("Resource", "pending-resource", "pending")), testClient: fake.NewClient(
fake.WithDynamicObjects(makeResource("Resource", "pending-resource", "pending")),
),
expectedStatus: cluster.Status("Pending"), expectedStatus: cluster.Status("Pending"),
}, },
{ {
@ -132,7 +133,9 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector(). selector: document.NewSelector().
ByGvk("example.com", "v1", "Resource"). ByGvk("example.com", "v1", "Resource").
ByName("unknown"), ByName("unknown"),
testClient: makeTestClient(makeResource("Resource", "unknown", "unknown")), testClient: fake.NewClient(
fake.WithDynamicObjects(makeResource("Resource", "unknown", "unknown")),
),
expectedStatus: cluster.UnknownStatus, expectedStatus: cluster.UnknownStatus,
}, },
{ {
@ -140,7 +143,9 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector(). selector: document.NewSelector().
ByGvk("example.com", "v1", "Legacy"). ByGvk("example.com", "v1", "Legacy").
ByName("stable-legacy"), ByName("stable-legacy"),
testClient: makeTestClient(makeResource("Legacy", "stable-legacy", "stable")), testClient: fake.NewClient(
fake.WithDynamicObjects(makeResource("Legacy", "stable-legacy", "stable")),
),
expectedStatus: cluster.Status("Stable"), expectedStatus: cluster.Status("Stable"),
}, },
{ {
@ -148,7 +153,7 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector(). selector: document.NewSelector().
ByGvk("example.com", "v1", "Missing"). ByGvk("example.com", "v1", "Missing").
ByName("missing-resource"), ByName("missing-resource"),
testClient: makeTestClient(), testClient: fake.NewClient(),
err: cluster.ErrResourceNotFound{Resource: "missing-resource"}, err: cluster.ErrResourceNotFound{Resource: "missing-resource"},
}, },
} }
@ -177,15 +182,6 @@ func TestGetStatusForResource(t *testing.T) {
} }
} }
func makeTestClient(obj ...runtime.Object) fake.Client {
testClient := fake.Client{
MockDynamicClient: func() dynamic.Interface {
return dynamicFake.NewSimpleDynamicClient(runtime.NewScheme(), obj...)
},
}
return testClient
}
func makeResource(kind, name, state string) *unstructured.Unstructured { func makeResource(kind, name, state string) *unstructured.Unstructured {
return &unstructured.Unstructured{ return &unstructured.Unstructured{
Object: map[string]interface{}{ Object: map[string]interface{}{

View File

@ -28,11 +28,12 @@ import (
) )
// Interface provides an abstraction layer to interactions with kubernetes // Interface provides an abstraction layer to interactions with kubernetes
// clusters by providing a ClientSet which includes all kubernetes core objects // clusters by providing the following:
// with standard operations, a DynamicClient which provides interactions with // * A ClientSet which includes all kubernetes core objects with standard operations
// loosely typed kubernetes resources, and a Kubectl interface that is built on // * A DynamicClient which provides interactions with loosely typed kubernetes resources
// top of kubectl libraries and implements such kubectl subcommands as kubectl // * An ApiextensionsClientSet which provides interactions with CustomResourceDefinitions
// apply (more will be added) // * A Kubectl interface that is built on top of kubectl libraries and
// implements such kubectl subcommands as kubectl apply (more will be added)
type Interface interface { type Interface interface {
ClientSet() kubernetes.Interface ClientSet() kubernetes.Interface
DynamicClient() dynamic.Interface DynamicClient() dynamic.Interface
@ -53,8 +54,13 @@ type Client struct {
// Client implements Interface // Client implements Interface
var _ Interface = &Client{} var _ Interface = &Client{}
// NewClient returns Cluster interface with Kubectl // Factory is a function which creates Interfaces
// and ClientSet interfaces initialized type Factory func(*environment.AirshipCTLSettings) (Interface, error)
// DefaultClient is a factory which generates a default client
var DefaultClient Factory = NewClient
// NewClient creates a Client initialized from the passed in settings
func NewClient(settings *environment.AirshipCTLSettings) (Interface, error) { func NewClient(settings *environment.AirshipCTLSettings) (Interface, error) {
client := new(Client) client := new(Client)
var err error var err error
@ -88,42 +94,42 @@ func NewClient(settings *environment.AirshipCTLSettings) (Interface, error) {
return client, nil return client, nil
} }
// ClientSet getter for ClientSet interface // ClientSet returns the ClientSet interface
func (c *Client) ClientSet() kubernetes.Interface { func (c *Client) ClientSet() kubernetes.Interface {
return c.clientSet return c.clientSet
} }
// SetClientSet setter for ClientSet interface // SetClientSet sets the ClientSet interface
func (c *Client) SetClientSet(clientSet kubernetes.Interface) { func (c *Client) SetClientSet(clientSet kubernetes.Interface) {
c.clientSet = clientSet c.clientSet = clientSet
} }
// DynamicClient getter for DynamicClient interface // DynamicClient returns the DynamicClient interface
func (c *Client) DynamicClient() dynamic.Interface { func (c *Client) DynamicClient() dynamic.Interface {
return c.dynamicClient return c.dynamicClient
} }
// SetDynamicClient setter for DynamicClient interface // SetDynamicClient sets the DynamicClient interface
func (c *Client) SetDynamicClient(dynamicClient dynamic.Interface) { func (c *Client) SetDynamicClient(dynamicClient dynamic.Interface) {
c.dynamicClient = dynamicClient c.dynamicClient = dynamicClient
} }
// ApiextensionsV1 getter for ApiextensionsV1 interface // ApiextensionsClientSet returns the Apiextensions interface
func (c *Client) ApiextensionsClientSet() apix.Interface { func (c *Client) ApiextensionsClientSet() apix.Interface {
return c.apixClient return c.apixClient
} }
// SetApiextensionsV1 setter for ApiextensionsV1 interface // SetApiextensionsClientSet sets the ApiextensionsClientSet interface
func (c *Client) SetApiextensionsClientSet(apixClient apix.Interface) { func (c *Client) SetApiextensionsClientSet(apixClient apix.Interface) {
c.apixClient = apixClient c.apixClient = apixClient
} }
// Kubectl getter for Kubectl interface // Kubectl returns the Kubectl interface
func (c *Client) Kubectl() kubectl.Interface { func (c *Client) Kubectl() kubectl.Interface {
return c.kubectl return c.kubectl
} }
// SetKubectl setter for Kubectl interface // SetKubectl sets the Kubectl interface
func (c *Client) SetKubectl(kctl kubectl.Interface) { func (c *Client) SetKubectl(kctl kubectl.Interface) {
c.kubectl = kctl c.kubectl = kctl
} }

View File

@ -51,5 +51,7 @@ func TestNewClient(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, client) assert.NotNil(t, client)
assert.NotNil(t, client.ClientSet()) assert.NotNil(t, client.ClientSet())
assert.NotNil(t, client.DynamicClient())
assert.NotNil(t, client.ApiextensionsClientSet())
assert.NotNil(t, client.Kubectl()) assert.NotNil(t, client.Kubectl())
} }

View File

@ -16,79 +16,131 @@ package fake
import ( import (
apix "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" apix "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apixFake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
dynamicFake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
kubernetesFake "k8s.io/client-go/kubernetes/fake"
"opendev.org/airship/airshipctl/pkg/k8s/client" "opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/k8s/kubectl" "opendev.org/airship/airshipctl/pkg/k8s/kubectl"
"opendev.org/airship/airshipctl/testutil/k8sutils"
) )
// Client is an implementation of client.Interface meant for testing purposes. // Client is an implementation of client.Interface meant for testing purposes.
// Its member methods are intended to be implemented on a case-by-case basis
// per test. Examples of implementations can be found with each interface
// method.
type Client struct { type Client struct {
MockClientSet func() kubernetes.Interface mockClientSet func() kubernetes.Interface
MockDynamicClient func() dynamic.Interface mockDynamicClient func() dynamic.Interface
MockApiextensionsClientSet func() apix.Interface mockApiextensionsClientSet func() apix.Interface
MockKubectl func() kubectl.Interface mockKubectl func() kubectl.Interface
} }
var _ client.Interface = &Client{} var _ client.Interface = &Client{}
// ClientSet is used to get a mocked implementation of a kubernetes clientset. // ClientSet is used to get a mocked implementation of a kubernetes clientset.
// To initialize the mocked clientset to be returned, the MockClientSet method // To initialize the mocked clientset to be returned, use the WithTypedObjects
// must be implemented, ideally returning a k8s.io/client-go/kubernetes/fake.Clientset. // ResourceAccumulator
// func (c *Client) ClientSet() kubernetes.Interface {
// Example: return c.mockClientSet()
//
// testClient := fake.Client {
// MockClientSet: func() kubernetes.Interface {
// return kubernetes_fake.NewSimpleClientset()
// },
// }
func (c Client) ClientSet() kubernetes.Interface {
return c.MockClientSet()
} }
// DynamicClient is used to get a mocked implementation of a dynamic client. // DynamicClient is used to get a mocked implementation of a dynamic client.
// To initialize the mocked client to be returned, the MockDynamicClient method // To initialize the mocked client to be returned, use the WithDynamicObjects
// must be implemented, ideally returning a k8s.io/client-go/dynamic/fake.FakeDynamicClient. // ResourceAccumulator.
// func (c *Client) DynamicClient() dynamic.Interface {
// Example: return c.mockDynamicClient()
// Here, scheme is a k8s.io/apimachinery/pkg/runtime.Scheme, possibly created
// via runtime.NewScheme()
//
// testClient := fake.Client {
// MockDynamicClient: func() dynamic.Interface {
// return dynamic_fake.NewSimpleDynamicClient(scheme)
// },
// }
func (c Client) DynamicClient() dynamic.Interface {
return c.MockDynamicClient()
} }
// ApiextensionsClientSet is used to get a mocked implementation of an // ApiextensionsClientSet is used to get a mocked implementation of an
// Apiextensions clientset. To initialize the mocked client to be returned, // Apiextensions clientset. To initialize the mocked client to be returned,
// the MockApiextensionsClientSet method must be implemented, ideally returning a // use the WithCRDs ResourceAccumulator
// k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake.ClientSet. func (c *Client) ApiextensionsClientSet() apix.Interface {
// return c.mockApiextensionsClientSet()
// Example:
//
// testClient := fake.Client {
// MockApiextensionsClientSet: func() apix.Interface {
// return apix_fake.NewSimpleClientset()
// },
// }
func (c Client) ApiextensionsClientSet() apix.Interface {
return c.MockApiextensionsClientSet()
} }
// Kubectl is used to get a mocked implementation of a Kubectl client. // Kubectl is used to get a mocked implementation of a Kubectl client.
// To initialize the mocked client to be returned, the MockKubectl method // To initialize the mocked client to be returned, use the WithKubectl ResourceAccumulator
// must be implemented. func (c *Client) Kubectl() kubectl.Interface {
// return c.mockKubectl()
// Example: TODO(howell) }
func (c Client) Kubectl() kubectl.Interface {
return c.MockKubectl() // A ResourceAccumulator is an option meant to be passed to NewClient.
// ResourceAccumulators can be mixed and matched to create a collection of
// mocked clients, each having their own fake objects.
type ResourceAccumulator func(*Client)
// NewClient creates an instance of a Client. If no arguments are passed, the
// returned Client will have fresh mocked kubernetes clients which will have no
// prior knowledge of any resources.
//
// If prior knowledge of resources is desirable, NewClient should receive an
// appropriate ResourceAccumulator initialized with the desired resources.
func NewClient(resourceAccumulators ...ResourceAccumulator) *Client {
fakeClient := new(Client)
for _, accumulator := range resourceAccumulators {
accumulator(fakeClient)
}
if fakeClient.mockClientSet == nil {
fakeClient.mockClientSet = func() kubernetes.Interface {
return kubernetesFake.NewSimpleClientset()
}
}
if fakeClient.mockDynamicClient == nil {
fakeClient.mockDynamicClient = func() dynamic.Interface {
return dynamicFake.NewSimpleDynamicClient(runtime.NewScheme())
}
}
if fakeClient.mockApiextensionsClientSet == nil {
fakeClient.mockApiextensionsClientSet = func() apix.Interface {
return apixFake.NewSimpleClientset()
}
}
if fakeClient.mockKubectl == nil {
fakeClient.mockKubectl = func() kubectl.Interface {
return kubectl.NewKubectl(k8sutils.NewMockKubectlFactory())
}
}
return fakeClient
}
// WithTypedObjects returns a ResourceAccumulator with resources which would
// normally be accessible through a kubernetes ClientSet (e.g. Pods,
// Deployments, etc...).
func WithTypedObjects(objs ...runtime.Object) ResourceAccumulator {
return func(c *Client) {
c.mockClientSet = func() kubernetes.Interface {
return kubernetesFake.NewSimpleClientset(objs...)
}
}
}
// WithCRDs returns a ResourceAccumulator with resources which would
// normally be accessible through a kubernetes ApiextensionsClientSet (e.g. CRDs).
func WithCRDs(objs ...runtime.Object) ResourceAccumulator {
return func(c *Client) {
c.mockApiextensionsClientSet = func() apix.Interface {
return apixFake.NewSimpleClientset(objs...)
}
}
}
// WithDynamicObjects returns a ResourceAccumulator with resources which would
// normally be accessible through a kubernetes DynamicClient (e.g. unstructured.Unstructured).
func WithDynamicObjects(objs ...runtime.Object) ResourceAccumulator {
return func(c *Client) {
c.mockDynamicClient = func() dynamic.Interface {
return dynamicFake.NewSimpleDynamicClient(runtime.NewScheme(), objs...)
}
}
}
// WithKubectl returns a ResourceAccumulator with an instance of a kubectl.Interface.
func WithKubectl(kubectlInstance *kubectl.Kubectl) ResourceAccumulator {
return func(c *Client) {
c.mockKubectl = func() kubectl.Interface {
return kubectlInstance
}
}
} }