diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index b0b429f899..8a300f1683 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -1 +1,105 @@ package controller + +import ( + "testing" + + "github.com/golang/glog" + types "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" + client "github.com/nirmata/kyverno/pkg/dclient" + "github.com/nirmata/kyverno/pkg/sharedinformer" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/sample-controller/pkg/signals" +) + +func TestCreatePolicy(t *testing.T) { + f := newFixture(t) + // new policy is added to policy lister and explictly passed to sync-handler + // to process the existing + policy := newPolicy("test-policy") + f.policyLister = append(f.policyLister, policy) + f.objects = append(f.objects, policy) + // run controller + f.runControler("test-policy") +} + +func (f *fixture) runControler(policyName string) { + policyInformerFactory, err := sharedinformer.NewFakeSharedInformerFactory() + if err != nil { + f.t.Fatal(err) + } + // new controller + policyController := NewPolicyController( + f.Client, + policyInformerFactory, + nil, + nil) + + stopCh := signals.SetupSignalHandler() + // start informer & controller + policyInformerFactory.Run(stopCh) + if err = policyController.Run(stopCh); err != nil { + glog.Fatalf("Error running PolicyController: %v\n", err) + } + // add policy to the informer + for _, p := range f.policyLister { + policyInformerFactory.GetInfomer().GetIndexer().Add(p) + } + + // sync handler + // reads the policy from the policy lister and processes them + err = policyController.syncHandler(policyName) + if err != nil { + f.t.Fatal(err) + } + policyController.Stop() + +} + +type fixture struct { + t *testing.T + Client *client.Client + policyLister []*types.Policy + objects []runtime.Object +} + +func newFixture(t *testing.T) *fixture { + f := &fixture{} + f.t = t + return f +} + +// create mock client with initial resouces +// set registered resources for gvr +func (f *fixture) setupFixture() { + scheme := runtime.NewScheme() + fclient, err := client.NewMockClient(scheme, f.objects...) + if err != nil { + f.t.Fatal(err) + } + regresource := map[string]string{"kyverno.io/v1alpha1": "policys"} + fclient.SetDiscovery(client.NewFakeDiscoveryClient(regresource)) +} + +func newPolicy(name string) *types.Policy { + return &types.Policy{ + TypeMeta: metav1.TypeMeta{APIVersion: types.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } +} + +func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "namespace": namespace, + "name": name, + }, + }, + } +} diff --git a/pkg/dclient/client.go b/pkg/dclient/client.go index ee504644ed..fae5c44793 100644 --- a/pkg/dclient/client.go +++ b/pkg/dclient/client.go @@ -27,15 +27,16 @@ import ( //Client enables interaction with k8 resource type Client struct { - client dynamic.Interface - cachedClient discovery.CachedDiscoveryInterface - clientConfig *rest.Config - kclient *kubernetes.Clientset + client dynamic.Interface + cachedClient discovery.CachedDiscoveryInterface + clientConfig *rest.Config + kclient kubernetes.Interface + discoveryClient IDiscovery } //NewClient creates new instance of client func NewClient(config *rest.Config) (*Client, error) { - client, err := dynamic.NewForConfig(config) + dclient, err := dynamic.NewForConfig(config) if err != nil { return nil, err } @@ -44,13 +45,15 @@ func NewClient(config *rest.Config) (*Client, error) { if err != nil { return nil, err } - - return &Client{ - client: client, + client := Client{ + client: dclient, clientConfig: config, kclient: kclient, - cachedClient: memory.NewMemCacheClient(kclient.Discovery()), - }, nil + } + // Set discovery client + discoveryClient := ServerPreferredResources{memory.NewMemCacheClient(kclient.Discovery())} + client.SetDiscovery(discoveryClient) + return &client, nil } //GetKubePolicyDeployment returns kube policy depoyment value @@ -100,7 +103,7 @@ func (c *Client) getGroupVersionMapper(resource string) schema.GroupVersionResou //TODO: add checks to see if the resource is supported //TODO: build the resource list dynamically( by querying the registered resources) //TODO: the error scenarios - return c.getGVR(resource) + return c.discoveryClient.getGVR(resource) } // GetResource returns the resource in unstructured/json format @@ -156,24 +159,10 @@ func convertToUnstructured(obj interface{}) *unstructured.Unstructured { return &unstructured.Unstructured{Object: unstructuredObj} } -//ConvertToRuntimeObject converts unstructed to runtime.Object runtime instance -func ConvertToRuntimeObject(obj *unstructured.Unstructured) (*runtime.Object, error) { - scheme := runtime.NewScheme() - gvk := obj.GroupVersionKind() - runtimeObj, err := scheme.New(gvk) - if err != nil { - return nil, err - } - if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &runtimeObj); err != nil { - return nil, err - } - return &runtimeObj, nil -} - // GenerateResource creates resource of the specified kind(supports 'clone' & 'data') func (c *Client) GenerateResource(generator types.Generation, namespace string) error { var err error - rGVR := c.getGVRFromKind(generator.Kind) + rGVR := c.discoveryClient.getGVRFromKind(generator.Kind) resource := &unstructured.Unstructured{} var rdata map[string]interface{} @@ -244,7 +233,20 @@ func (c *Client) waitUntilNamespaceIsCreated(name string) error { return lastError } -func (c *Client) getGVR(resource string) schema.GroupVersionResource { +type IDiscovery interface { + getGVR(resource string) schema.GroupVersionResource + getGVRFromKind(kind string) schema.GroupVersionResource +} + +func (c *Client) SetDiscovery(discoveryClient IDiscovery) { + c.discoveryClient = discoveryClient +} + +type ServerPreferredResources struct { + cachedClient discovery.CachedDiscoveryInterface +} + +func (c ServerPreferredResources) getGVR(resource string) schema.GroupVersionResource { emptyGVR := schema.GroupVersionResource{} serverresources, err := c.cachedClient.ServerPreferredResources() if err != nil { @@ -268,7 +270,7 @@ func (c *Client) getGVR(resource string) schema.GroupVersionResource { //To-do: measure performance //To-do: evaluate DefaultRESTMapper to fetch kind->resource mapping -func (c *Client) getGVRFromKind(kind string) schema.GroupVersionResource { +func (c ServerPreferredResources) getGVRFromKind(kind string) schema.GroupVersionResource { emptyGVR := schema.GroupVersionResource{} serverresources, err := c.cachedClient.ServerPreferredResources() if err != nil { diff --git a/pkg/dclient/client_test.go b/pkg/dclient/client_test.go index 75f509ff84..984c316203 100644 --- a/pkg/dclient/client_test.go +++ b/pkg/dclient/client_test.go @@ -1,6 +1,160 @@ package client +import ( + "fmt" + "testing" + + policytypes "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + // GetResource // ListResource // CreateResource // getGroupVersionMapper (valid and invalid resources) + +//NewMockClient creates a mock client +// - dynamic client +// - kubernetes client +// - objects to initialize the client + +func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "namespace": namespace, + "name": name, + }, + }, + } +} + +func newUnstructuredWithSpec(apiVersion, kind, namespace, name string, spec map[string]interface{}) *unstructured.Unstructured { + u := newUnstructured(apiVersion, kind, namespace, name) + u.Object["spec"] = spec + return u +} + +func TestClient(t *testing.T) { + scheme := runtime.NewScheme() + // init groupversion + regresource := map[string]string{"group/version": "thekinds", + "group2/version": "thekinds", + "v1": "namespaces", + "apps/v1": "deployments"} + // init resources + objects := []runtime.Object{newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), + newUnstructured("group2/version", "TheKind", "ns-foo", "name2-foo"), + newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"), + newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"), + newUnstructured("group2/version", "TheKind", "ns-foo", "name2-baz"), + newUnstructured("apps/v1", "Deployment", "kyverno", "kyverno-deployment"), + } + + // Mock Client + client, err := NewMockClient(scheme, objects...) + if err != nil { + t.Fatal(err) + } + + // set discovery Client + client.SetDiscovery(NewFakeDiscoveryClient(regresource)) + // Get Resource + res, err := client.GetResource("thekinds", "ns-foo", "name-foo") + if err != nil { + t.Fatal(err) + } + fmt.Println(res) + // List Resources + list, err := client.ListResource("thekinds", "ns-foo") + if err != nil { + t.Fatal(err) + } + fmt.Println(len(list.Items)) + // DeleteResouce + err = client.DeleteResouce("thekinds", "ns-foo", "name-bar") + if err != nil { + t.Fatal(err) + } + // CreateResource + res, err = client.CreateResource("thekinds", "ns-foo", newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1")) + if err != nil { + t.Fatal(err) + } + // UpdateResource + res, err = client.UpdateResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "bar"})) + if err != nil { + t.Fatal(err) + } + + // UpdateStatusResource + res, err = client.UpdateStatusResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "status"})) + if err != nil { + t.Fatal(err) + } + + iEvent, err := client.GetEventsInterface() + if err != nil { + t.Fatal(err) + } + eventList, err := iEvent.List(meta.ListOptions{}) + if err != nil { + t.Fatal(err) + } + fmt.Println(eventList.Items) + + iCSR, err := client.GetCSRInterface() + if err != nil { + t.Fatal(err) + } + csrList, err := iCSR.List(meta.ListOptions{}) + if err != nil { + t.Fatal(err) + } + fmt.Println(csrList.Items) + + //GenerateResource -> copy From + // 1 create namespace + // 2 generate resource + + // create namespace + ns, err := client.CreateResource("namespaces", "", newUnstructured("v1", "Namespace", "", "ns1")) + if err != nil { + t.Fatal(err) + } + gen := policytypes.Generation{Kind: "TheKind", + Name: "gen-kind", + Clone: &policytypes.CloneFrom{Namespace: "ns-foo", Name: "name-foo"}} + err = client.GenerateResource(gen, ns.GetName()) + if err != nil { + t.Fatal(err) + } + res, err = client.GetResource("thekinds", "ns1", "gen-kind") + if err != nil { + t.Fatal(err) + } + // GenerateResource -> data + gen = policytypes.Generation{Kind: "TheKind", + Name: "name2-baz-new", + Data: newUnstructured("group2/version", "TheKind", "ns1", "name2-baz-new")} + err = client.GenerateResource(gen, ns.GetName()) + if err != nil { + t.Fatal(err) + } + res, err = client.GetResource("thekinds", "ns1", "name2-baz-new") + if err != nil { + t.Fatal(err) + } + + // Get Kube Policy Deployment + deploy, err := client.GetKubePolicyDeployment() + if err != nil { + t.Fatal(err) + } + fmt.Println(deploy.GetName()) +} diff --git a/pkg/dclient/utils.go b/pkg/dclient/utils.go index 5057e797c1..080f2739c6 100644 --- a/pkg/dclient/utils.go +++ b/pkg/dclient/utils.go @@ -1,7 +1,13 @@ package client import ( + "strings" "time" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic/fake" + kubernetesfake "k8s.io/client-go/kubernetes/fake" ) const ( @@ -16,3 +22,46 @@ const ( ) const namespaceCreationMaxWaitTime time.Duration = 30 * time.Second const namespaceCreationWaitInterval time.Duration = 100 * time.Millisecond + +//---testing utilities +func NewMockClient(scheme *runtime.Scheme, objects ...runtime.Object) (*Client, error) { + client := fake.NewSimpleDynamicClient(scheme, objects...) + // the typed and dynamic client are initalized with similar resources + kclient := kubernetesfake.NewSimpleClientset(objects...) + return &Client{ + client: client, + kclient: kclient, + }, nil + +} + +// NewFakeDiscoveryClient returns a fakediscovery client +func NewFakeDiscoveryClient(regResources map[string]string) *fakeDiscoveryClient { + registeredResources := make([]schema.GroupVersionResource, len(regResources)) + for groupVersion, resource := range regResources { + gv, err := schema.ParseGroupVersion(groupVersion) + if err != nil { + continue + } + registeredResources = append(registeredResources, gv.WithResource(resource)) + } + return &fakeDiscoveryClient{registeredResouces: registeredResources} +} + +type fakeDiscoveryClient struct { + registeredResouces []schema.GroupVersionResource +} + +func (c *fakeDiscoveryClient) getGVR(resource string) schema.GroupVersionResource { + for _, gvr := range c.registeredResouces { + if gvr.Resource == resource { + return gvr + } + } + return schema.GroupVersionResource{} +} + +func (c *fakeDiscoveryClient) getGVRFromKind(kind string) schema.GroupVersionResource { + resource := strings.ToLower(kind) + "s" + return c.getGVR(resource) +} diff --git a/pkg/sharedinformer/utils.go b/pkg/sharedinformer/utils.go new file mode 100644 index 0000000000..804be21665 --- /dev/null +++ b/pkg/sharedinformer/utils.go @@ -0,0 +1,15 @@ +package sharedinformer + +import ( + "github.com/nirmata/kyverno/pkg/client/clientset/versioned/fake" + informers "github.com/nirmata/kyverno/pkg/client/informers/externalversions" + "k8s.io/apimachinery/pkg/runtime" +) + +func NewFakeSharedInformerFactory(objects ...runtime.Object) (SharedInfomer, error) { + fakePolicyClient := fake.NewSimpleClientset(objects...) + policyInformerFactory := informers.NewSharedInformerFactory(fakePolicyClient, 0) + return &sharedInfomer{ + policyInformerFactory: policyInformerFactory, + }, nil +}