From 05274b1b149139d91445ca10a73defe41f14824a Mon Sep 17 00:00:00 2001 From: Victor Morales Date: Tue, 18 Sep 2018 17:19:00 -0700 Subject: Add UTs to plugins Deployment, service and namespace are plugins which offers CRUD operations to manage their resources. They haven't implemented Unit Tests which makes fragile to change/refactor the source code. This change adds their corresponding Unit Tests and defines a standard interface. Change-Id: I1e1eb40f1a18ba33c74069a117462c8df17767ac Signed-off-by: Victor Morales Issue-ID: MULTICLOUD-301 --- src/k8splugin/Gopkg.lock | 36 +++- src/k8splugin/Makefile | 4 - src/k8splugin/csar/parser.go | 30 ++-- src/k8splugin/krd/plugins.go | 49 +++-- .../mock_files/mock_plugins/mockplugin.go | 20 +-- .../mock_files/mock_yamls/deployment.yaml | 2 +- src/k8splugin/mock_files/mock_yamls/service.yaml | 2 +- src/k8splugin/plugins/deployment/plugin.go | 75 +++----- src/k8splugin/plugins/deployment/plugin_test.go | 199 +++++++++++++++++++++ src/k8splugin/plugins/namespace/plugin.go | 74 +++++--- src/k8splugin/plugins/namespace/plugin_test.go | 170 ++++++++++++++++++ src/k8splugin/plugins/service/plugin.go | 83 ++++----- src/k8splugin/plugins/service/plugin_test.go | 199 +++++++++++++++++++++ 13 files changed, 777 insertions(+), 166 deletions(-) create mode 100644 src/k8splugin/plugins/deployment/plugin_test.go create mode 100644 src/k8splugin/plugins/namespace/plugin_test.go create mode 100644 src/k8splugin/plugins/service/plugin_test.go (limited to 'src') diff --git a/src/k8splugin/Gopkg.lock b/src/k8splugin/Gopkg.lock index 0fc98853..4ccf69ea 100644 --- a/src/k8splugin/Gopkg.lock +++ b/src/k8splugin/Gopkg.lock @@ -352,46 +352,77 @@ version = "kubernetes-1.10.3" [[projects]] - digest = "1:0a4e3d4f41939942aa81b5900cd8332def6f529f2f286e521cc54d6d7874dbb8" + digest = "1:490b16761d2ded2729bf4e262eedc2d0df8b57d6f7d4f60a2893bb24be108331" name = "k8s.io/client-go" packages = [ "discovery", + "discovery/fake", "kubernetes", + "kubernetes/fake", "kubernetes/scheme", "kubernetes/typed/admissionregistration/v1alpha1", + "kubernetes/typed/admissionregistration/v1alpha1/fake", "kubernetes/typed/admissionregistration/v1beta1", + "kubernetes/typed/admissionregistration/v1beta1/fake", "kubernetes/typed/apps/v1", + "kubernetes/typed/apps/v1/fake", "kubernetes/typed/apps/v1beta1", + "kubernetes/typed/apps/v1beta1/fake", "kubernetes/typed/apps/v1beta2", + "kubernetes/typed/apps/v1beta2/fake", "kubernetes/typed/authentication/v1", + "kubernetes/typed/authentication/v1/fake", "kubernetes/typed/authentication/v1beta1", + "kubernetes/typed/authentication/v1beta1/fake", "kubernetes/typed/authorization/v1", + "kubernetes/typed/authorization/v1/fake", "kubernetes/typed/authorization/v1beta1", + "kubernetes/typed/authorization/v1beta1/fake", "kubernetes/typed/autoscaling/v1", + "kubernetes/typed/autoscaling/v1/fake", "kubernetes/typed/autoscaling/v2beta1", + "kubernetes/typed/autoscaling/v2beta1/fake", "kubernetes/typed/batch/v1", + "kubernetes/typed/batch/v1/fake", "kubernetes/typed/batch/v1beta1", + "kubernetes/typed/batch/v1beta1/fake", "kubernetes/typed/batch/v2alpha1", + "kubernetes/typed/batch/v2alpha1/fake", "kubernetes/typed/certificates/v1beta1", + "kubernetes/typed/certificates/v1beta1/fake", "kubernetes/typed/core/v1", + "kubernetes/typed/core/v1/fake", "kubernetes/typed/events/v1beta1", + "kubernetes/typed/events/v1beta1/fake", "kubernetes/typed/extensions/v1beta1", + "kubernetes/typed/extensions/v1beta1/fake", "kubernetes/typed/networking/v1", + "kubernetes/typed/networking/v1/fake", "kubernetes/typed/policy/v1beta1", + "kubernetes/typed/policy/v1beta1/fake", "kubernetes/typed/rbac/v1", + "kubernetes/typed/rbac/v1/fake", "kubernetes/typed/rbac/v1alpha1", + "kubernetes/typed/rbac/v1alpha1/fake", "kubernetes/typed/rbac/v1beta1", + "kubernetes/typed/rbac/v1beta1/fake", "kubernetes/typed/scheduling/v1alpha1", + "kubernetes/typed/scheduling/v1alpha1/fake", "kubernetes/typed/settings/v1alpha1", + "kubernetes/typed/settings/v1alpha1/fake", "kubernetes/typed/storage/v1", + "kubernetes/typed/storage/v1/fake", "kubernetes/typed/storage/v1alpha1", + "kubernetes/typed/storage/v1alpha1/fake", "kubernetes/typed/storage/v1beta1", + "kubernetes/typed/storage/v1beta1/fake", "pkg/apis/clientauthentication", "pkg/apis/clientauthentication/v1alpha1", "pkg/version", "plugin/pkg/client/auth/exec", "rest", "rest/watch", + "testing", "tools/auth", "tools/clientcmd", "tools/clientcmd/api", @@ -422,7 +453,10 @@ "k8s.io/api/core/v1", "k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/client-go/kubernetes", + "k8s.io/client-go/kubernetes/fake", "k8s.io/client-go/kubernetes/scheme", + "k8s.io/client-go/kubernetes/typed/apps/v1", + "k8s.io/client-go/kubernetes/typed/core/v1", "k8s.io/client-go/tools/clientcmd", "k8s.io/client-go/util/homedir", ] diff --git a/src/k8splugin/Makefile b/src/k8splugin/Makefile index 34b51982..510ac163 100644 --- a/src/k8splugin/Makefile +++ b/src/k8splugin/Makefile @@ -39,20 +39,16 @@ integration: clean @go build -buildmode=plugin -o ./mock_files/mock_plugins/mockplugin.so ./mock_files/mock_plugins/mockplugin.go @go test -v -tags 'integration' ./... -.PHONY: format format: @go fmt ./... -.PHONY: plugins plugins: @find plugins -type d -not -path plugins -exec sh -c "ls {}/plugin.go | xargs go build -buildmode=plugin -o $(basename {}).so" \; -.PHONY: dep dep: @go get -u $(DEPENDENCIES) $(GOPATH)/bin/dep ensure -.PHONY: clean clean: find . -name "*so" -delete @rm -f k8plugin diff --git a/src/k8splugin/csar/parser.go b/src/k8splugin/csar/parser.go index af4546c6..6cb07fc2 100644 --- a/src/k8splugin/csar/parser.go +++ b/src/k8splugin/csar/parser.go @@ -34,31 +34,31 @@ func generateExternalVNFID() string { return hex.EncodeToString(b) } -func ensuresNamespace(namespace string, kubeclient *kubernetes.Clientset) error { +func ensuresNamespace(namespace string, kubeclient kubernetes.Interface) error { namespacePlugin, ok := krd.LoadedPlugins["namespace"] if !ok { return pkgerrors.New("No plugin for namespace resource found") } - symGetNamespaceFunc, err := namespacePlugin.Lookup("GetResource") + symGetNamespaceFunc, err := namespacePlugin.Lookup("Get") if err != nil { return pkgerrors.Wrap(err, "Error fetching get namespace function") } - exists, err := symGetNamespaceFunc.(func(string, *kubernetes.Clientset) (bool, error))( - namespace, kubeclient) + ns, err := symGetNamespaceFunc.(func(string, string, kubernetes.Interface) (string, error))( + namespace, "", kubeclient) if err != nil { return pkgerrors.Wrap(err, "An error ocurred during the get namespace execution") } - if !exists { + if ns == "" { log.Println("Creating " + namespace + " namespace") - symGetNamespaceFunc, err := namespacePlugin.Lookup("CreateResource") + symGetNamespaceFunc, err := namespacePlugin.Lookup("Create") if err != nil { return pkgerrors.Wrap(err, "Error fetching create namespace plugin") } - err = symGetNamespaceFunc.(func(string, *kubernetes.Clientset) error)( + err = symGetNamespaceFunc.(func(string, kubernetes.Interface) error)( namespace, kubeclient) if err != nil { return pkgerrors.Wrap(err, "Error creating "+namespace+" namespace") @@ -99,10 +99,10 @@ var CreateVNF = func(csarID string, cloudRegionID string, namespace string, kube } log.Println("Processing file: " + path) - genericKubeData := &krd.GenericKubeResourceData{ - YamlFilePath: path, - Namespace: namespace, - InternalVNFID: internalVNFID, + genericKubeData := &krd.ResourceData{ + YamlFilePath: path, + Namespace: namespace, + VnfId: internalVNFID, } typePlugin, ok := krd.LoadedPlugins[resource] @@ -110,12 +110,12 @@ var CreateVNF = func(csarID string, cloudRegionID string, namespace string, kube return "", nil, pkgerrors.New("No plugin for resource " + resource + " found") } - symCreateResourceFunc, err := typePlugin.Lookup("CreateResource") + symCreateResourceFunc, err := typePlugin.Lookup("Create") if err != nil { return "", nil, pkgerrors.Wrap(err, "Error fetching "+resource+" plugin") } - internalResourceName, err := symCreateResourceFunc.(func(*krd.GenericKubeResourceData, *kubernetes.Clientset) (string, error))( + internalResourceName, err := symCreateResourceFunc.(func(*krd.ResourceData, kubernetes.Interface) (string, error))( genericKubeData, kubeclient) if err != nil { return "", nil, pkgerrors.Wrap(err, "Error in plugin "+resource+" plugin") @@ -144,7 +144,7 @@ var DestroyVNF = func(data map[string][]string, namespace string, kubeclient *ku return pkgerrors.New("No plugin for resource " + resourceName + " found") } - symDeleteResourceFunc, err := typePlugin.Lookup("DeleteResource") + symDeleteResourceFunc, err := typePlugin.Lookup("Delete") if err != nil { return pkgerrors.Wrap(err, "Error fetching "+resourceName+" plugin") } @@ -153,7 +153,7 @@ var DestroyVNF = func(data map[string][]string, namespace string, kubeclient *ku log.Println("Deleting resource: " + resourceName) - err = symDeleteResourceFunc.(func(string, string, *kubernetes.Clientset) error)( + err = symDeleteResourceFunc.(func(string, string, kubernetes.Interface) error)( resourceName, namespace, kubeclient) if err != nil { return pkgerrors.Wrap(err, "Error destroying "+resourceName) diff --git a/src/k8splugin/krd/plugins.go b/src/k8splugin/krd/plugins.go index 612e3f6b..41b83226 100644 --- a/src/k8splugin/krd/plugins.go +++ b/src/k8splugin/krd/plugins.go @@ -14,31 +14,46 @@ limitations under the License. package krd import ( + "io/ioutil" + "log" + "os" "plugin" - appsV1 "k8s.io/api/apps/v1" - coreV1 "k8s.io/api/core/v1" - "k8s.io/client-go/kubernetes" + pkgerrors "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" ) // LoadedPlugins stores references to the stored plugins var LoadedPlugins = map[string]*plugin.Plugin{} -// KubeResourceClient has the signature methods to create Kubernetes reources -type KubeResourceClient interface { - CreateResource(GenericKubeResourceData, *kubernetes.Clientset) (string, error) - ListResources(string, string) (*[]string, error) - DeleteResource(string, string, *kubernetes.Clientset) error - GetResource(string, string, *kubernetes.Clientset) (string, error) +const ResourcesListLimit = 10 + +// ResourceData stores all supported Kubernetes plugin types +type ResourceData struct { + YamlFilePath string + Namespace string + VnfId string } -// GenericKubeResourceData stores all supported Kubernetes plugin types -type GenericKubeResourceData struct { - YamlFilePath string - Namespace string - InternalVNFID string +// DecodeYAML reads a YAMl file to extract the Kubernetes object definition +var DecodeYAML = func(path string) (runtime.Object, error) { + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, pkgerrors.New("File " + path + " not found") + } + + log.Println("Reading deployment YAML") + rawBytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, pkgerrors.Wrap(err, "Deployment YAML file read error") + } + + log.Println("Decoding deployment YAML") + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, _, err := decode(rawBytes, nil, nil) + if err != nil { + return nil, pkgerrors.Wrap(err, "Deserialize deployment error") + } - // Add additional Kubernetes plugins below kinds - DeploymentData *appsV1.Deployment - ServiceData *coreV1.Service + return obj, nil } diff --git a/src/k8splugin/mock_files/mock_plugins/mockplugin.go b/src/k8splugin/mock_files/mock_plugins/mockplugin.go index 9ceec342..c31e4fe2 100644 --- a/src/k8splugin/mock_files/mock_plugins/mockplugin.go +++ b/src/k8splugin/mock_files/mock_plugins/mockplugin.go @@ -21,23 +21,23 @@ import ( func main() {} -// CreateResource object in a specific Kubernetes resource -func CreateResource(kubedata *krd.GenericKubeResourceData, kubeclient *kubernetes.Clientset) (string, error) { +// Create object in a specific Kubernetes resource +func Create(data *krd.ResourceData, client kubernetes.Interface) (string, error) { return "externalUUID", nil } -// ListResources of existing resources -func ListResources(limit int64, namespace string, kubeclient *kubernetes.Clientset) (*[]string, error) { +// List of existing resources +func List(namespace string, client kubernetes.Interface) ([]string, error) { returnVal := []string{"cloud1-default-uuid1", "cloud1-default-uuid2"} - return &returnVal, nil + return returnVal, nil } -// DeleteResource existing resources -func DeleteResource(name string, namespace string, kubeclient *kubernetes.Clientset) error { +// Delete existing resources +func Delete(name string, namespace string, client kubernetes.Interface) error { return nil } -// GetResource existing resource host -func GetResource(namespace string, client *kubernetes.Clientset) (bool, error) { - return true, nil +// Get existing resource host +func Get(name string, namespace string, client kubernetes.Interface) (string, error) { + return name, nil } diff --git a/src/k8splugin/mock_files/mock_yamls/deployment.yaml b/src/k8splugin/mock_files/mock_yamls/deployment.yaml index eff2fc5a..49a30efc 100644 --- a/src/k8splugin/mock_files/mock_yamls/deployment.yaml +++ b/src/k8splugin/mock_files/mock_yamls/deployment.yaml @@ -12,7 +12,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: sise-deploy + name: mock-deployment spec: template: metadata: diff --git a/src/k8splugin/mock_files/mock_yamls/service.yaml b/src/k8splugin/mock_files/mock_yamls/service.yaml index 297ab1b7..71938949 100644 --- a/src/k8splugin/mock_files/mock_yamls/service.yaml +++ b/src/k8splugin/mock_files/mock_yamls/service.yaml @@ -12,7 +12,7 @@ apiVersion: v1 kind: Service metadata: - name: sise-svc + name: mock-service spec: ports: - port: 80 diff --git a/src/k8splugin/plugins/deployment/plugin.go b/src/k8splugin/plugins/deployment/plugin.go index 2b4c7cb7..97330b5b 100644 --- a/src/k8splugin/plugins/deployment/plugin.go +++ b/src/k8splugin/plugins/deployment/plugin.go @@ -14,55 +14,36 @@ limitations under the License. package main import ( - "io/ioutil" "log" - "os" - - "k8s.io/client-go/kubernetes" pkgerrors "github.com/pkg/errors" appsV1 "k8s.io/api/apps/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/kubernetes" "k8splugin/krd" ) -// CreateResource object in a specific Kubernetes Deployment -func CreateResource(kubedata *krd.GenericKubeResourceData, kubeclient *kubernetes.Clientset) (string, error) { - if kubedata.Namespace == "" { - kubedata.Namespace = "default" - } - - if _, err := os.Stat(kubedata.YamlFilePath); err != nil { - return "", pkgerrors.New("File " + kubedata.YamlFilePath + " not found") - } - - log.Println("Reading deployment YAML") - rawBytes, err := ioutil.ReadFile(kubedata.YamlFilePath) - if err != nil { - return "", pkgerrors.Wrap(err, "Deployment YAML file read error") +// Create deployment object in a specific Kubernetes cluster +func Create(data *krd.ResourceData, client kubernetes.Interface) (string, error) { + namespace := data.Namespace + if namespace == "" { + namespace = "default" } - - log.Println("Decoding deployment YAML") - decode := scheme.Codecs.UniversalDeserializer().Decode - obj, _, err := decode(rawBytes, nil, nil) + obj, err := krd.DecodeYAML(data.YamlFilePath) if err != nil { - return "", pkgerrors.Wrap(err, "Deserialize deployment error") + return "", pkgerrors.Wrap(err, "Decode deployment object error") } - switch o := obj.(type) { - case *appsV1.Deployment: - kubedata.DeploymentData = o - default: - return "", pkgerrors.New(kubedata.YamlFilePath + " contains another resource different than Deployment") + deployment, ok := obj.(*appsV1.Deployment) + if !ok { + return "", pkgerrors.New("Decoded object contains another resource different than Deployment") } + deployment.Namespace = namespace + deployment.Name = data.VnfId + "-" + deployment.Name - kubedata.DeploymentData.Namespace = kubedata.Namespace - kubedata.DeploymentData.Name = kubedata.InternalVNFID + "-" + kubedata.DeploymentData.Name - - result, err := kubeclient.AppsV1().Deployments(kubedata.Namespace).Create(kubedata.DeploymentData) + result, err := client.AppsV1().Deployments(namespace).Create(deployment) if err != nil { return "", pkgerrors.Wrap(err, "Create Deployment error") } @@ -70,14 +51,14 @@ func CreateResource(kubedata *krd.GenericKubeResourceData, kubeclient *kubernete return result.GetObjectMeta().GetName(), nil } -// ListResources of existing deployments hosted in a specific Kubernetes Deployment -func ListResources(limit int64, namespace string, kubeclient *kubernetes.Clientset) (*[]string, error) { +// List of existing deployments hosted in a specific Kubernetes cluster +func List(namespace string, kubeclient kubernetes.Interface) ([]string, error) { if namespace == "" { namespace = "default" } opts := metaV1.ListOptions{ - Limit: limit, + Limit: krd.ResourcesListLimit, } opts.APIVersion = "apps/v1" opts.Kind = "Deployment" @@ -87,38 +68,38 @@ func ListResources(limit int64, namespace string, kubeclient *kubernetes.Clients return nil, pkgerrors.Wrap(err, "Get Deployment list error") } - result := make([]string, 0, limit) + result := make([]string, 0, krd.ResourcesListLimit) if list != nil { for _, deployment := range list.Items { + log.Printf("%v", deployment.Name) result = append(result, deployment.Name) } } - return &result, nil + return result, nil } -// DeleteResource existing deployments hosting in a specific Kubernetes Deployment -func DeleteResource(name string, namespace string, kubeclient *kubernetes.Clientset) error { +// Delete an existing deployment hosted in a specific Kubernetes cluster +func Delete(name string, namespace string, kubeclient kubernetes.Interface) error { if namespace == "" { namespace = "default" } - log.Println("Deleting deployment: " + name) - deletePolicy := metaV1.DeletePropagationForeground - err := kubeclient.AppsV1().Deployments(namespace).Delete(name, &metaV1.DeleteOptions{ + opts := &metaV1.DeleteOptions{ PropagationPolicy: &deletePolicy, - }) + } - if err != nil { + log.Println("Deleting deployment: " + name) + if err := kubeclient.AppsV1().Deployments(namespace).Delete(name, opts); err != nil { return pkgerrors.Wrap(err, "Delete Deployment error") } return nil } -// GetResource existing deployment hosting in a specific Kubernetes Deployment -func GetResource(name string, namespace string, kubeclient *kubernetes.Clientset) (string, error) { +// Get an existing deployment hosted in a specific Kubernetes cluster +func Get(name string, namespace string, kubeclient kubernetes.Interface) (string, error) { if namespace == "" { namespace = "default" } diff --git a/src/k8splugin/plugins/deployment/plugin_test.go b/src/k8splugin/plugins/deployment/plugin_test.go new file mode 100644 index 00000000..636629a9 --- /dev/null +++ b/src/k8splugin/plugins/deployment/plugin_test.go @@ -0,0 +1,199 @@ +// +build unit + +/* +Copyright 2018 Intel Corporation. +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 main + +import ( + "reflect" + "strings" + "testing" + + "k8splugin/krd" + + appsV1 "k8s.io/api/apps/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + testclient "k8s.io/client-go/kubernetes/fake" +) + +func TestCreateDeployment(t *testing.T) { + namespace := "test1" + name := "mock-deployment" + internalVNFID := "1" + testCases := []struct { + label string + input *krd.ResourceData + clientOutput *appsV1.Deployment + expectedResult string + expectedError string + }{ + { + label: "Fail to create a deployment with non-existing file", + input: &krd.ResourceData{ + YamlFilePath: "non-existing_test_file.yaml", + }, + clientOutput: &appsV1.Deployment{}, + expectedError: "not found", + }, + { + label: "Fail to create a deployment with invalid type", + input: &krd.ResourceData{ + YamlFilePath: "../../mock_files/mock_yamls/service.yaml", + }, + clientOutput: &appsV1.Deployment{}, + expectedError: "contains another resource different than Deployment", + }, + { + label: "Successfully create a deployment", + input: &krd.ResourceData{ + VnfId: internalVNFID, + YamlFilePath: "../../mock_files/mock_yamls/deployment.yaml", + }, + clientOutput: &appsV1.Deployment{ + ObjectMeta: metaV1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + expectedResult: internalVNFID + "-" + name, + }, + } + + for _, testCase := range testCases { + client := testclient.NewSimpleClientset(testCase.clientOutput) + t.Run(testCase.label, func(t *testing.T) { + result, err := Create(testCase.input, client) + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Create method returned an error (%s)", err) + } + } + if !reflect.DeepEqual(testCase.expectedResult, result) { + t.Fatalf("Create method returned %v and it was expected (%v)", result, testCase.expectedResult) + } + }) + } +} + +func TestListDeployment(t *testing.T) { + namespace := "test1" + testCases := []struct { + label string + input string + clientOutput *appsV1.DeploymentList + expectedResult []string + }{ + { + label: "Sucessfully display an empty deployment list", + input: namespace, + clientOutput: &appsV1.DeploymentList{}, + expectedResult: []string{}, + }, + { + label: "Sucessfully display a list of existing deployments", + input: namespace, + clientOutput: &appsV1.DeploymentList{ + Items: []appsV1.Deployment{ + appsV1.Deployment{ + ObjectMeta: metaV1.ObjectMeta{ + Name: "test", + Namespace: namespace, + }, + }, + }, + }, + expectedResult: []string{"test"}, + }, + } + + for _, testCase := range testCases { + client := testclient.NewSimpleClientset(testCase.clientOutput) + t.Run(testCase.label, func(t *testing.T) { + result, err := List(testCase.input, client) + if err != nil { + t.Fatalf("List method returned an error (%s)", err) + } + if !reflect.DeepEqual(testCase.expectedResult, result) { + t.Fatalf("List method returned %v and it was expected (%v)", result, testCase.expectedResult) + } + }) + } +} + +func TestDeleteDeployment(t *testing.T) { + namespace := "test1" + name := "mock-deployment" + testCases := []struct { + label string + input string + clientOutput *appsV1.Deployment + }{ + { + label: "Sucessfully delete an existing deployment", + input: name, + clientOutput: &appsV1.Deployment{ + ObjectMeta: metaV1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + }, + } + + for _, testCase := range testCases { + client := testclient.NewSimpleClientset(testCase.clientOutput) + t.Run(testCase.label, func(t *testing.T) { + err := Delete(testCase.input, namespace, client) + if err != nil { + t.Fatalf("Delete method returned an error (%s)", err) + } + }) + } +} + +func TestGetDeployment(t *testing.T) { + namespace := "test1" + name := "mock-deployment" + testCases := []struct { + label string + input string + clientOutput *appsV1.Deployment + expectedResult string + }{ + { + label: "Sucessfully get an existing deployment", + input: name, + clientOutput: &appsV1.Deployment{ + ObjectMeta: metaV1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + expectedResult: name, + }, + } + + for _, testCase := range testCases { + client := testclient.NewSimpleClientset(testCase.clientOutput) + t.Run(testCase.label, func(t *testing.T) { + result, err := Get(testCase.input, namespace, client) + if err != nil { + t.Fatalf("Get method returned an error (%s)", err) + } + if !reflect.DeepEqual(testCase.expectedResult, result) { + t.Fatalf("Get method returned %v and it was expected (%v)", result, testCase.expectedResult) + } + }) + } +} diff --git a/src/k8splugin/plugins/namespace/plugin.go b/src/k8splugin/plugins/namespace/plugin.go index 986de863..e29ff43d 100644 --- a/src/k8splugin/plugins/namespace/plugin.go +++ b/src/k8splugin/plugins/namespace/plugin.go @@ -14,55 +14,85 @@ limitations under the License. package main import ( + "log" + + "k8s.io/client-go/kubernetes" + pkgerrors "github.com/pkg/errors" coreV1 "k8s.io/api/core/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" + + "k8splugin/krd" ) -// CreateResource is used to create a new Namespace -func CreateResource(namespace string, client *kubernetes.Clientset) error { - namespaceStruct := &coreV1.Namespace{ +// Create a namespace object in a specific Kubernetes cluster +func Create(data *krd.ResourceData, client kubernetes.Interface) (string, error) { + namespace := &coreV1.Namespace{ ObjectMeta: metaV1.ObjectMeta{ - Name: namespace, + Name: data.Namespace, }, } - _, err := client.CoreV1().Namespaces().Create(namespaceStruct) + _, err := client.CoreV1().Namespaces().Create(namespace) if err != nil { - return pkgerrors.Wrap(err, "Create Namespace error") + return "", pkgerrors.Wrap(err, "Create Namespace error") } - return nil + return data.Namespace, nil } -// GetResource is used to check if a given namespace actually exists in Kubernetes -func GetResource(namespace string, client *kubernetes.Clientset) (bool, error) { +// Get an existing namespace hosted in a specific Kubernetes cluster +func Get(name string, namespace string, client kubernetes.Interface) (string, error) { opts := metaV1.ListOptions{} - namespaceList, err := client.CoreV1().Namespaces().List(opts) + list, err := client.CoreV1().Namespaces().List(opts) if err != nil { - return false, pkgerrors.Wrap(err, "Get Namespace list error") + return "", pkgerrors.Wrap(err, "Get Namespace list error") } - for _, ns := range namespaceList.Items { + for _, ns := range list.Items { if namespace == ns.Name { - return true, nil + return ns.Name, nil } } - return false, nil + return "", nil } -// DeleteResource is used to delete a namespace -func DeleteResource(namespace string, client *kubernetes.Clientset) error { +// Delete an existing namespace hosted in a specific Kubernetes cluster +func Delete(name string, namespace string, client kubernetes.Interface) error { deletePolicy := metaV1.DeletePropagationForeground - - err := client.CoreV1().Namespaces().Delete(namespace, &metaV1.DeleteOptions{ + opts := &metaV1.DeleteOptions{ PropagationPolicy: &deletePolicy, - }) + } - if err != nil { - return pkgerrors.Wrap(err, "Delete Namespace error") + log.Println("Deleting namespace: " + name) + if err := client.CoreV1().Namespaces().Delete(name, opts); err != nil { + return pkgerrors.Wrap(err, "Delete namespace error") } + return nil } + +// List of existing namespaces hosted in a specific Kubernetes cluster +func List(namespace string, client kubernetes.Interface) ([]string, error) { + opts := metaV1.ListOptions{ + Limit: krd.ResourcesListLimit, + } + opts.APIVersion = "apps/v1" + opts.Kind = "Namespace" + + list, err := client.CoreV1().Namespaces().List(opts) + if err != nil { + return nil, pkgerrors.Wrap(err, "Get Namespace list error") + } + + result := make([]string, 0, krd.ResourcesListLimit) + if list != nil { + for _, deployment := range list.Items { + log.Printf("%v", deployment.Name) + result = append(result, deployment.Name) + } + } + + return result, nil +} diff --git a/src/k8splugin/plugins/namespace/plugin_test.go b/src/k8splugin/plugins/namespace/plugin_test.go new file mode 100644 index 00000000..fe60404d --- /dev/null +++ b/src/k8splugin/plugins/namespace/plugin_test.go @@ -0,0 +1,170 @@ +// +build unit + +/* +Copyright 2018 Intel Corporation. +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 main + +import ( + "reflect" + "strings" + "testing" + + "k8splugin/krd" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + testclient "k8s.io/client-go/kubernetes/fake" +) + +func TestCreateNamespace(t *testing.T) { + namespace := "test1" + testCases := []struct { + label string + input *krd.ResourceData + clientOutput *coreV1.Namespace + expectedResult string + expectedError string + }{ + { + label: "Successfully create a namespace", + input: &krd.ResourceData{ + Namespace: namespace, + }, + clientOutput: &coreV1.Namespace{}, + expectedResult: namespace, + }, + } + + for _, testCase := range testCases { + client := testclient.NewSimpleClientset(testCase.clientOutput) + t.Run(testCase.label, func(t *testing.T) { + result, err := Create(testCase.input, client) + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Create method returned an error (%s)", err) + } + } + if !reflect.DeepEqual(testCase.expectedResult, result) { + t.Fatalf("Create method returned %v and it was expected (%v)", result, testCase.expectedResult) + } + }) + } +} + +func TestListNamespace(t *testing.T) { + namespace := "test1" + testCases := []struct { + label string + input string + clientOutput *coreV1.NamespaceList + expectedResult []string + }{ + { + label: "Sucessfully to display an empty namespace list", + input: namespace, + clientOutput: &coreV1.NamespaceList{}, + expectedResult: []string{}, + }, + { + label: "Sucessfully to display a list of existing namespaces", + input: namespace, + clientOutput: &coreV1.NamespaceList{ + Items: []coreV1.Namespace{ + coreV1.Namespace{ + ObjectMeta: metaV1.ObjectMeta{ + Name: namespace, + }, + }, + }, + }, + expectedResult: []string{namespace}, + }, + } + + for _, testCase := range testCases { + client := testclient.NewSimpleClientset(testCase.clientOutput) + t.Run(testCase.label, func(t *testing.T) { + result, err := List(testCase.input, client) + if err != nil { + t.Fatalf("List method returned an error (%s)", err) + } + if !reflect.DeepEqual(testCase.expectedResult, result) { + t.Fatalf("List method returned %v and it was expected (%v)", result, testCase.expectedResult) + } + }) + } +} + +func TestDeleteNamespace(t *testing.T) { + namespace := "test1" + testCases := []struct { + label string + input string + clientOutput *coreV1.Namespace + }{ + { + label: "Sucessfully to delete an existing namespace", + input: namespace, + clientOutput: &coreV1.Namespace{ + ObjectMeta: metaV1.ObjectMeta{ + Name: namespace, + }, + }, + }, + } + + for _, testCase := range testCases { + client := testclient.NewSimpleClientset(testCase.clientOutput) + t.Run(testCase.label, func(t *testing.T) { + err := Delete(testCase.input, namespace, client) + if err != nil { + t.Fatalf("Delete method returned an error (%s)", err) + } + }) + } +} + +func TestGetNamespace(t *testing.T) { + namespace := "test1" + testCases := []struct { + label string + input string + clientOutput *coreV1.Namespace + expectedResult string + }{ + { + label: "Sucessfully to get an existing namespace", + input: namespace, + clientOutput: &coreV1.Namespace{ + ObjectMeta: metaV1.ObjectMeta{ + Name: namespace, + }, + }, + expectedResult: namespace, + }, + } + + for _, testCase := range testCases { + client := testclient.NewSimpleClientset(testCase.clientOutput) + t.Run(testCase.label, func(t *testing.T) { + result, err := Get(testCase.input, namespace, client) + if err != nil { + t.Fatalf("Get method returned an error (%s)", err) + } + if !reflect.DeepEqual(testCase.expectedResult, result) { + t.Fatalf("Get method returned %v and it was expected (%v)", result, testCase.expectedResult) + } + }) + } +} diff --git a/src/k8splugin/plugins/service/plugin.go b/src/k8splugin/plugins/service/plugin.go index 36ef24f6..61609e98 100644 --- a/src/k8splugin/plugins/service/plugin.go +++ b/src/k8splugin/plugins/service/plugin.go @@ -14,9 +14,7 @@ limitations under the License. package main import ( - "io/ioutil" "log" - "os" "k8s.io/client-go/kubernetes" @@ -24,58 +22,44 @@ import ( coreV1 "k8s.io/api/core/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/scheme" "k8splugin/krd" ) -// CreateResource object in a specific Kubernetes Deployment -func CreateResource(kubedata *krd.GenericKubeResourceData, kubeclient *kubernetes.Clientset) (string, error) { - if kubedata.Namespace == "" { - kubedata.Namespace = "default" - } - - if _, err := os.Stat(kubedata.YamlFilePath); err != nil { - return "", pkgerrors.New("File " + kubedata.YamlFilePath + " not found") - } - - log.Println("Reading service YAML") - rawBytes, err := ioutil.ReadFile(kubedata.YamlFilePath) - if err != nil { - return "", pkgerrors.Wrap(err, "Service YAML file read error") +// Create a service object in a specific Kubernetes cluster +func Create(data *krd.ResourceData, client kubernetes.Interface) (string, error) { + namespace := data.Namespace + if namespace == "" { + namespace = "default" } - - log.Println("Decoding service YAML") - decode := scheme.Codecs.UniversalDeserializer().Decode - obj, _, err := decode(rawBytes, nil, nil) + obj, err := krd.DecodeYAML(data.YamlFilePath) if err != nil { - return "", pkgerrors.Wrap(err, "Deserialize service error") + return "", pkgerrors.Wrap(err, "Decode service object error") } - switch o := obj.(type) { - case *coreV1.Service: - kubedata.ServiceData = o - default: - return "", pkgerrors.New(kubedata.YamlFilePath + " contains another resource different than Service") + service, ok := obj.(*coreV1.Service) + if !ok { + return "", pkgerrors.New("Decoded object contains another resource different than Service") } + service.Namespace = namespace + service.Name = data.VnfId + "-" + service.Name - kubedata.ServiceData.Namespace = kubedata.Namespace - kubedata.ServiceData.Name = kubedata.InternalVNFID + "-" + kubedata.ServiceData.Name - - result, err := kubeclient.CoreV1().Services(kubedata.Namespace).Create(kubedata.ServiceData) + result, err := client.CoreV1().Services(namespace).Create(service) if err != nil { return "", pkgerrors.Wrap(err, "Create Service error") } + return result.GetObjectMeta().GetName(), nil } -// ListResources of existing deployments hosted in a specific Kubernetes Deployment -func ListResources(limit int64, namespace string, kubeclient *kubernetes.Clientset) (*[]string, error) { +// List of existing services hosted in a specific Kubernetes cluster +func List(namespace string, kubeclient kubernetes.Interface) ([]string, error) { if namespace == "" { namespace = "default" } + opts := metaV1.ListOptions{ - Limit: limit, + Limit: krd.ResourcesListLimit, } opts.APIVersion = "apps/v1" opts.Kind = "Service" @@ -84,36 +68,39 @@ func ListResources(limit int64, namespace string, kubeclient *kubernetes.Clients if err != nil { return nil, pkgerrors.Wrap(err, "Get Service list error") } - result := make([]string, 0, limit) + + result := make([]string, 0, krd.ResourcesListLimit) if list != nil { - for _, service := range list.Items { - result = append(result, service.Name) + for _, deployment := range list.Items { + log.Printf("%v", deployment.Name) + result = append(result, deployment.Name) } } - return &result, nil + + return result, nil } -// DeleteResource deletes an existing Kubernetes service -func DeleteResource(name string, namespace string, kubeclient *kubernetes.Clientset) error { +// Delete an existing service hosted in a specific Kubernetes cluster +func Delete(name string, namespace string, kubeclient kubernetes.Interface) error { if namespace == "" { namespace = "default" } - log.Println("Deleting service: " + name) - deletePolicy := metaV1.DeletePropagationForeground - err := kubeclient.CoreV1().Services(namespace).Delete(name, &metaV1.DeleteOptions{ + opts := &metaV1.DeleteOptions{ PropagationPolicy: &deletePolicy, - }) - if err != nil { - return pkgerrors.Wrap(err, "Delete Service error") + } + + log.Println("Deleting service: " + name) + if err := kubeclient.CoreV1().Services(namespace).Delete(name, opts); err != nil { + return pkgerrors.Wrap(err, "Delete service error") } return nil } -// GetResource existing service hosting in a specific Kubernetes Service -func GetResource(name string, namespace string, kubeclient *kubernetes.Clientset) (string, error) { +// Get an existing service hosted in a specific Kubernetes cluster +func Get(name string, namespace string, kubeclient kubernetes.Interface) (string, error) { if namespace == "" { namespace = "default" } diff --git a/src/k8splugin/plugins/service/plugin_test.go b/src/k8splugin/plugins/service/plugin_test.go new file mode 100644 index 00000000..001467df --- /dev/null +++ b/src/k8splugin/plugins/service/plugin_test.go @@ -0,0 +1,199 @@ +// +build unit + +/* +Copyright 2018 Intel Corporation. +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 main + +import ( + "reflect" + "strings" + "testing" + + "k8splugin/krd" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + testclient "k8s.io/client-go/kubernetes/fake" +) + +func TestCreateService(t *testing.T) { + namespace := "test1" + name := "mock-service" + internalVNFID := "1" + testCases := []struct { + label string + input *krd.ResourceData + clientOutput *coreV1.Service + expectedResult string + expectedError string + }{ + { + label: "Fail to create a service with non-existing file", + input: &krd.ResourceData{ + YamlFilePath: "non-existing_test_file.yaml", + }, + clientOutput: &coreV1.Service{}, + expectedError: "not found", + }, + { + label: "Fail to create a service with invalid type", + input: &krd.ResourceData{ + YamlFilePath: "../../mock_files/mock_yamls/deployment.yaml", + }, + clientOutput: &coreV1.Service{}, + expectedError: "contains another resource different than Service", + }, + { + label: "Successfully create a service", + input: &krd.ResourceData{ + VnfId: internalVNFID, + YamlFilePath: "../../mock_files/mock_yamls/service.yaml", + }, + clientOutput: &coreV1.Service{ + ObjectMeta: metaV1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + expectedResult: internalVNFID + "-" + name, + }, + } + + for _, testCase := range testCases { + client := testclient.NewSimpleClientset(testCase.clientOutput) + t.Run(testCase.label, func(t *testing.T) { + result, err := Create(testCase.input, client) + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Create method returned an error (%s)", err) + } + } + if !reflect.DeepEqual(testCase.expectedResult, result) { + t.Fatalf("Create method returned %v and it was expected (%v)", result, testCase.expectedResult) + } + }) + } +} + +func TestListService(t *testing.T) { + namespace := "test1" + testCases := []struct { + label string + input string + clientOutput *coreV1.ServiceList + expectedResult []string + }{ + { + label: "Sucessfully to display an empty service list", + input: namespace, + clientOutput: &coreV1.ServiceList{}, + expectedResult: []string{}, + }, + { + label: "Sucessfully to display a list of existing services", + input: namespace, + clientOutput: &coreV1.ServiceList{ + Items: []coreV1.Service{ + coreV1.Service{ + ObjectMeta: metaV1.ObjectMeta{ + Name: "test", + Namespace: namespace, + }, + }, + }, + }, + expectedResult: []string{"test"}, + }, + } + + for _, testCase := range testCases { + client := testclient.NewSimpleClientset(testCase.clientOutput) + t.Run(testCase.label, func(t *testing.T) { + result, err := List(testCase.input, client) + if err != nil { + t.Fatalf("List method returned an error (%s)", err) + } + if !reflect.DeepEqual(testCase.expectedResult, result) { + t.Fatalf("List method returned %v and it was expected (%v)", result, testCase.expectedResult) + } + }) + } +} + +func TestDeleteService(t *testing.T) { + namespace := "test1" + name := "mock-service" + testCases := []struct { + label string + input string + clientOutput *coreV1.Service + }{ + { + label: "Sucessfully to delete an existing service", + input: name, + clientOutput: &coreV1.Service{ + ObjectMeta: metaV1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + }, + } + + for _, testCase := range testCases { + client := testclient.NewSimpleClientset(testCase.clientOutput) + t.Run(testCase.label, func(t *testing.T) { + err := Delete(testCase.input, namespace, client) + if err != nil { + t.Fatalf("Delete method returned an error (%s)", err) + } + }) + } +} + +func TestGetService(t *testing.T) { + namespace := "test1" + name := "mock-service" + testCases := []struct { + label string + input string + clientOutput *coreV1.Service + expectedResult string + }{ + { + label: "Sucessfully to get an existing service", + input: name, + clientOutput: &coreV1.Service{ + ObjectMeta: metaV1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + expectedResult: name, + }, + } + + for _, testCase := range testCases { + client := testclient.NewSimpleClientset(testCase.clientOutput) + t.Run(testCase.label, func(t *testing.T) { + result, err := Get(testCase.input, namespace, client) + if err != nil { + t.Fatalf("Get method returned an error (%s)", err) + } + if !reflect.DeepEqual(testCase.expectedResult, result) { + t.Fatalf("Get method returned %v and it was expected (%v)", result, testCase.expectedResult) + } + }) + } +} -- cgit 1.2.3-korg