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()
kctl := kubectl.NewKubectl(tf)
tc := fake.Client{
MockKubectl: func() kubectl.Interface { return kctl },
}
tests := []struct {
theInfra *initinfra.Infra
@ -71,24 +68,22 @@ func TestDeploy(t *testing.T) {
expectedError error
}{
{
client: fake.Client{
MockKubectl: func() kubectl.Interface {
return kubectl.NewKubectl(k8sutils.
client: fake.NewClient(fake.WithKubectl(
kubectl.NewKubectl(k8sutils.
NewMockKubectlFactory().
WithDynamicClientByError(nil, DynamicClientError))
},
},
WithDynamicClientByError(nil, DynamicClientError)))),
expectedError: DynamicClientError,
},
{
expectedError: nil,
prune: false,
client: tc,
client: fake.NewClient(fake.WithKubectl(kctl)),
},
{
expectedError: nil,
prune: true,
client: tc,
client: fake.NewClient(fake.WithKubectl(kctl)),
},
}

View File

@ -22,9 +22,6 @@ import (
"github.com/stretchr/testify/require"
"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/document"
@ -107,7 +104,7 @@ func TestGetStatusForResource(t *testing.T) {
tests := []struct {
name string
selector document.Selector
testClient fake.Client
testClient *fake.Client
expectedStatus cluster.Status
err error
}{
@ -116,7 +113,9 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector().
ByGvk("example.com", "v1", "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"),
},
{
@ -124,7 +123,9 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector().
ByGvk("example.com", "v1", "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"),
},
{
@ -132,7 +133,9 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector().
ByGvk("example.com", "v1", "Resource").
ByName("unknown"),
testClient: makeTestClient(makeResource("Resource", "unknown", "unknown")),
testClient: fake.NewClient(
fake.WithDynamicObjects(makeResource("Resource", "unknown", "unknown")),
),
expectedStatus: cluster.UnknownStatus,
},
{
@ -140,7 +143,9 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector().
ByGvk("example.com", "v1", "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"),
},
{
@ -148,7 +153,7 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector().
ByGvk("example.com", "v1", "Missing").
ByName("missing-resource"),
testClient: makeTestClient(),
testClient: fake.NewClient(),
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 {
return &unstructured.Unstructured{
Object: map[string]interface{}{

View File

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

View File

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

View File

@ -16,79 +16,131 @@ package fake
import (
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"
dynamicFake "k8s.io/client-go/dynamic/fake"
"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/kubectl"
"opendev.org/airship/airshipctl/testutil/k8sutils"
)
// 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 {
MockClientSet func() kubernetes.Interface
MockDynamicClient func() dynamic.Interface
MockApiextensionsClientSet func() apix.Interface
MockKubectl func() kubectl.Interface
mockClientSet func() kubernetes.Interface
mockDynamicClient func() dynamic.Interface
mockApiextensionsClientSet func() apix.Interface
mockKubectl func() kubectl.Interface
}
var _ client.Interface = &Client{}
// ClientSet is used to get a mocked implementation of a kubernetes clientset.
// To initialize the mocked clientset to be returned, the MockClientSet method
// must be implemented, ideally returning a k8s.io/client-go/kubernetes/fake.Clientset.
//
// Example:
//
// testClient := fake.Client {
// MockClientSet: func() kubernetes.Interface {
// return kubernetes_fake.NewSimpleClientset()
// },
// }
func (c Client) ClientSet() kubernetes.Interface {
return c.MockClientSet()
// To initialize the mocked clientset to be returned, use the WithTypedObjects
// ResourceAccumulator
func (c *Client) ClientSet() kubernetes.Interface {
return c.mockClientSet()
}
// DynamicClient is used to get a mocked implementation of a dynamic client.
// To initialize the mocked client to be returned, the MockDynamicClient method
// must be implemented, ideally returning a k8s.io/client-go/dynamic/fake.FakeDynamicClient.
//
// Example:
// 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()
// To initialize the mocked client to be returned, use the WithDynamicObjects
// ResourceAccumulator.
func (c *Client) DynamicClient() dynamic.Interface {
return c.mockDynamicClient()
}
// ApiextensionsClientSet is used to get a mocked implementation of an
// Apiextensions clientset. To initialize the mocked client to be returned,
// the MockApiextensionsClientSet method must be implemented, ideally returning a
// k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake.ClientSet.
//
// Example:
//
// testClient := fake.Client {
// MockApiextensionsClientSet: func() apix.Interface {
// return apix_fake.NewSimpleClientset()
// },
// }
func (c Client) ApiextensionsClientSet() apix.Interface {
return c.MockApiextensionsClientSet()
// use the WithCRDs ResourceAccumulator
func (c *Client) ApiextensionsClientSet() apix.Interface {
return c.mockApiextensionsClientSet()
}
// Kubectl is used to get a mocked implementation of a Kubectl client.
// To initialize the mocked client to be returned, the MockKubectl method
// must be implemented.
//
// Example: TODO(howell)
func (c Client) Kubectl() kubectl.Interface {
return c.MockKubectl()
// To initialize the mocked client to be returned, use the WithKubectl ResourceAccumulator
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
}
}
}