diff --git a/pkg/cluster/initinfra/infra_test.go b/pkg/cluster/initinfra/infra_test.go index 2d89813bd..73e889da0 100644 --- a/pkg/cluster/initinfra/infra_test.go +++ b/pkg/cluster/initinfra/infra_test.go @@ -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. - NewMockKubectlFactory(). - WithDynamicClientByError(nil, DynamicClientError)) - }, - }, + + client: fake.NewClient(fake.WithKubectl( + kubectl.NewKubectl(k8sutils. + NewMockKubectlFactory(). + 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)), }, } diff --git a/pkg/cluster/status_test.go b/pkg/cluster/status_test.go index 83b8d4181..07bb4e65d 100644 --- a/pkg/cluster/status_test.go +++ b/pkg/cluster/status_test.go @@ -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{}{ diff --git a/pkg/k8s/client/client.go b/pkg/k8s/client/client.go index dd397524d..54e362374 100644 --- a/pkg/k8s/client/client.go +++ b/pkg/k8s/client/client.go @@ -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 } diff --git a/pkg/k8s/client/client_test.go b/pkg/k8s/client/client_test.go index 600177ff3..401eda7c0 100644 --- a/pkg/k8s/client/client_test.go +++ b/pkg/k8s/client/client_test.go @@ -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()) } diff --git a/pkg/k8s/client/fake/fake.go b/pkg/k8s/client/fake/fake.go index 92d477b13..25df6bbd3 100644 --- a/pkg/k8s/client/fake/fake.go +++ b/pkg/k8s/client/fake/fake.go @@ -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() +// Apiextensions clientset. To initialize the mocked client to be returned, +// 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 + } + } }