From a3ebad94d9d92b8a7444272467077d074be22ea2 Mon Sep 17 00:00:00 2001 From: adrianc Date: Wed, 17 May 2023 16:33:31 +0300 Subject: [PATCH 1/9] Minor refactor of cmdutils Signed-off-by: adrianc --- pkg/cmdutils/cmdutils_suite_test.go | 7 +-- pkg/cmdutils/utils.go | 1 - pkg/cmdutils/utils_test.go | 94 ++++++++++++++++------------- 3 files changed, 55 insertions(+), 47 deletions(-) diff --git a/pkg/cmdutils/cmdutils_suite_test.go b/pkg/cmdutils/cmdutils_suite_test.go index f901f8b..d05faaf 100644 --- a/pkg/cmdutils/cmdutils_suite_test.go +++ b/pkg/cmdutils/cmdutils_suite_test.go @@ -11,8 +11,7 @@ limitations under the License. */ -// Package cmdutils is the package that contains utilities for nv-ipam command -package cmdutils +package cmdutils_test import ( . "github.com/onsi/ginkgo/v2" @@ -21,7 +20,7 @@ import ( "testing" ) -func TestServer(t *testing.T) { +func TestCmdUtils(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "cmdutils") + RunSpecs(t, "cmdutils Suite") } diff --git a/pkg/cmdutils/utils.go b/pkg/cmdutils/utils.go index ddb79ca..412f3f1 100644 --- a/pkg/cmdutils/utils.go +++ b/pkg/cmdutils/utils.go @@ -11,7 +11,6 @@ limitations under the License. */ -// Package cmdutils is the package that contains utilities for multus command package cmdutils import ( diff --git a/pkg/cmdutils/utils_test.go b/pkg/cmdutils/utils_test.go index 143e29c..dc92120 100644 --- a/pkg/cmdutils/utils_test.go +++ b/pkg/cmdutils/utils_test.go @@ -11,59 +11,69 @@ limitations under the License. */ -// Package cmdutils is the package that contains utilities for multus command -package cmdutils +package cmdutils_test import ( - "fmt" - "os" + "fmt" + "os" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Mellanox/nvidia-k8s-ipam/pkg/cmdutils" ) -var _ = Describe("thin entrypoint testing", func() { - It("Run CopyFileAtomic()", func() { - // create directory and files - tmpDir, err := os.MkdirTemp("", "multus_thin_entrypoint_tmp") - Expect(err).NotTo(HaveOccurred()) +var _ = Describe("cmdutils testing", func() { + var ( + tmpDir string + ) + + BeforeEach(func() { + var err error + tmpDir, err = os.MkdirTemp("", "nv_ipam_thin_entrypoint_tmp") + Expect(err).NotTo(HaveOccurred()) + }) - // create source directory - srcDir := fmt.Sprintf("%s/src", tmpDir) - err = os.Mkdir(srcDir, 0755) - Expect(err).NotTo(HaveOccurred()) + AfterEach(func() { + err := os.RemoveAll(tmpDir) + Expect(err).NotTo(HaveOccurred()) + }) - // create destination directory - destDir := fmt.Sprintf("%s/dest", tmpDir) - err = os.Mkdir(destDir, 0755) - Expect(err).NotTo(HaveOccurred()) + It("Run CopyFileAtomic()", func() { + var err error + // create source directory + srcDir := fmt.Sprintf("%s/src", tmpDir) + err = os.Mkdir(srcDir, 0755) + Expect(err).NotTo(HaveOccurred()) - // sample source file - srcFilePath := fmt.Sprintf("%s/sampleInput", srcDir) - err = os.WriteFile(srcFilePath, []byte("sampleInputABC"), 0744) - Expect(err).NotTo(HaveOccurred()) + // create destination directory + destDir := fmt.Sprintf("%s/dest", tmpDir) + err = os.Mkdir(destDir, 0755) + Expect(err).NotTo(HaveOccurred()) - // old files in dest - destFileName := "sampleInputDest" - destFilePath := fmt.Sprintf("%s/%s", destDir, destFileName) - err = os.WriteFile(destFilePath, []byte("inputOldXYZ"), 0611) - Expect(err).NotTo(HaveOccurred()) + // sample source file + srcFilePath := fmt.Sprintf("%s/sampleInput", srcDir) + err = os.WriteFile(srcFilePath, []byte("sampleInputABC"), 0744) + Expect(err).NotTo(HaveOccurred()) - tempFileName := "temp_file" - err = CopyFileAtomic(srcFilePath, destDir, tempFileName, destFileName) - Expect(err).NotTo(HaveOccurred()) + // old files in dest + destFileName := "sampleInputDest" + destFilePath := fmt.Sprintf("%s/%s", destDir, destFileName) + err = os.WriteFile(destFilePath, []byte("inputOldXYZ"), 0611) + Expect(err).NotTo(HaveOccurred()) - // check file mode - stat, err := os.Stat(destFilePath) - Expect(err).NotTo(HaveOccurred()) - Expect(stat.Mode()).To(Equal(os.FileMode(0744))) + tempFileName := "temp_file" + err = cmdutils.CopyFileAtomic(srcFilePath, destDir, tempFileName, destFileName) + Expect(err).NotTo(HaveOccurred()) - // check file contents - destFileByte, err := os.ReadFile(destFilePath) - Expect(err).NotTo(HaveOccurred()) - Expect(destFileByte).To(Equal([]byte("sampleInputABC"))) + // check file mode + stat, err := os.Stat(destFilePath) + Expect(err).NotTo(HaveOccurred()) + Expect(stat.Mode()).To(Equal(os.FileMode(0744))) - err = os.RemoveAll(tmpDir) - Expect(err).NotTo(HaveOccurred()) - }) + // check file contents + destFileByte, err := os.ReadFile(destFilePath) + Expect(err).NotTo(HaveOccurred()) + Expect(destFileByte).To(Equal([]byte("sampleInputABC"))) + }) }) From 5b74696c34fe3ef2d04f8c20fb9d29829f280e72 Mon Sep 17 00:00:00 2001 From: adrianc Date: Wed, 17 May 2023 17:04:59 +0300 Subject: [PATCH 2/9] Add k8sclient tests Signed-off-by: adrianc --- pkg/cni/k8sclient/k8sclient_suite_test.go | 26 +++++++ pkg/cni/k8sclient/k8sclient_test.go | 87 +++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 pkg/cni/k8sclient/k8sclient_suite_test.go create mode 100644 pkg/cni/k8sclient/k8sclient_test.go diff --git a/pkg/cni/k8sclient/k8sclient_suite_test.go b/pkg/cni/k8sclient/k8sclient_suite_test.go new file mode 100644 index 0000000..3e9e3d5 --- /dev/null +++ b/pkg/cni/k8sclient/k8sclient_suite_test.go @@ -0,0 +1,26 @@ +/* + Copyright 2023, NVIDIA CORPORATION & AFFILIATES + 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 k8sclient_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestK8sClient(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "K8sClient Suite") +} diff --git a/pkg/cni/k8sclient/k8sclient_test.go b/pkg/cni/k8sclient/k8sclient_test.go new file mode 100644 index 0000000..460fe1a --- /dev/null +++ b/pkg/cni/k8sclient/k8sclient_test.go @@ -0,0 +1,87 @@ +/* + Copyright 2023, NVIDIA CORPORATION & AFFILIATES + 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 k8sclient_test + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Mellanox/nvidia-k8s-ipam/pkg/cni/k8sclient" +) + +const ( + kubeconfigTestData = ` +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJeU1EVXlOREUwTURjeU5Gb1hEVE15TURVeU1URTBNRGN5TkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTkpJCnZUT2swNEtGVm51a0gyNUw3V2IvTFUxNVo4QUw4ZVFERXZZTnUyVys1eklaemxQTW5lUXYvWm1GcDloakpuUk0KQzc2N0s0cHpTMVNjcWlpUGxKaUlxNGhjcVJLdTA3bHZ6WTNiZWllNVhubXYzTm42YTM4THVHNVhud29pM3ZsdwpscUh4L3lHdEFicHRlUFA5K3FQRndySGxGMGlNMHZVb3BjNmt5cmUwR0RnVEkzUVdUQ3RmcDl2RDFydU1ER2krCnhjWnJueHVTcldkekdoWEV5d0FDWUtiaDEwUFQwbVJaK2p5MVViRFp3alk4VHN5WFl1WHpHci9KMUcvc0hOSFEKd1dmMnNDRVJnVjB1N2Z0eHhrZ0ZETXJOcllqcjJFUERhZ0lPeDdTTGFQVCtSNWN3bmc5Vml6ajNicnMzekppRwp3ZnNiZ1gxSTdlaHMzNmh6VXNVQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZJUjc1MWhlVG1Qcm4rb1N3NjltUXpWSEVpVWVNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQkZpaWNkOTdERGR5QjUvbjg0agpZb2pIN1dNT1dDeTRwQ2ZuNWx0L2MyR2VRcDA3Y3NWbXlwUnY4YzQ3ZGYxVklIL09zcDlVczBoVWh1NFYybjBhCnZwM2svM1pSTEd3NVVzZGZoTkJrOTZWd0lRRFhVdER6NDZQU2dMUSsvREJqSitFbTJCanlyWm9PUkFGTDhsWkQKNzl2SndadXl6RC90UFFPL2xSZjRWZHYxQTl4R3U2QlJyRzdybmhBUVlxNDBBbGZ2dExlM2dqa1VFRVROZWFJOAorTERmeUs0cmVnTGc1emFNeDVvQmNyVzhiWHBxdTNFSlZHUXhoM3pVOVhmeDlJckdidE1zUHY2bXBEenV1TmhBCm5WemlPbC95TDhTMGdRTUt4WU9YdHpKYWdhU3JzQm82K3ZmenZkRHIzUnFST0VCSXU4eC9yRmsxNTNMdlUrMmUKdjY0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + server: https://7.7.7.7:6443 + name: kubernetes +contexts: +- context: + cluster: kubernetes + user: kubernetes-admin + name: kubernetes-admin@kubernetes +current-context: kubernetes-admin@kubernetes +kind: Config +preferences: {} +users: +- name: kubernetes-admin + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJVENDQWdtZ0F3SUJBZ0lJUG9Bd0xoc2pRNFV3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TWpBMU1qUXhOREEzTWpSYUZ3MHlNekExTWpReE5EQTNNamRhTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXUyMWQ0V29kbGxTa2VhcDUKZUhqMnpIaGVFblNPaGxVbVdKanZ2VmtMdkd6RHR6ZkErMVo2b09xMkt1V0FtcVdjbjhIRjFuQU5aR0JyeFd2cgp3OHVPbzBYZHJpWEdDV1FkRi9sUTl0UXdIU1VWa1NTQzdIU0VwUHhXUVFjU3NoZnJVQ25YSjVkVllaWElkdHpnCnJURzVUZURENERnZCtPdUJhcmNiS096emFzcHpWalI0QStmUm1GQ1V2cVVSRTZSOTZRYmNVdGNzS05vQndpTisKcmtNNDFyOEJyUXBEZDNsUGhRWUdQdmN3TkdaNC8waUJFdDNLN1h3RUI2eFY4WDg1QjhoVkRvV0phRWV0NisragpUUEhUK3dmVzRoV3ovaFZ2S29HandiYlc5SEVPNnlYYitYRUZkL0o4R1JpeWtGZXVWYTJWWjJEc2tJRUdOUlpZCkpHYmE2UUlEQVFBQm8xWXdWREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RBWURWUjBUQVFIL0JBSXdBREFmQmdOVkhTTUVHREFXZ0JTRWUrZFlYazVqNjUvcUVzT3Zaa00xUnhJbApIakFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBTEdLbklSU05MaWtXenVpdDgyK0VTS21ZSWRZWVZwSVdJOFVQCmRnbXh0T0o2N3FWeWFtNnlqYzZRc3o4VE1jYk9lZzFwQ1hzbGVhd2VXZWpydmE3bzlhalVNTTNxNFBXSFg2aVMKTG13em5ockRiWTBsdC9qMGg1akJhMnlBOERoRml5WDBDR2ROMDVWSlg0aWZqL0I4ZC9tS1lXMzBDNHlJd2RFWApUdFZqRmgydzRFdlphNkpLZjRtMDRmSENtRjhMbzRNbkhJY0NObndGMmNwdHR4b1dHOWcxMEFjSnhkZy9SREhxClljZUR5bmUwYklDbGxrRlpFdStwdEIxcDkzVUI2RFY0VHh1VGpSZVFKYmt1Y2prQ1FtcTVmdXUrVjl5NE90TDMKK0VjVFpvSDUyV3dtZlk1TnJyT1hyTTl1UXpHSkQxaklYOE1LMVZlWjNDVmh4bkMxZ1E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdTIxZDRXb2RsbFNrZWFwNWVIajJ6SGhlRW5TT2hsVW1XSmp2dlZrTHZHekR0emZBCisxWjZvT3EyS3VXQW1xV2NuOEhGMW5BTlpHQnJ4V3Zydzh1T28wWGRyaVhHQ1dRZEYvbFE5dFF3SFNVVmtTU0MKN0hTRXBQeFdRUWNTc2hmclVDblhKNWRWWVpYSWR0emdyVEc1VGVERDREZ2QrT3VCYXJjYktPenphc3B6VmpSNApBK2ZSbUZDVXZxVVJFNlI5NlFiY1V0Y3NLTm9Cd2lOK3JrTTQxcjhCclFwRGQzbFBoUVlHUHZjd05HWjQvMGlCCkV0M0s3WHdFQjZ4VjhYODVCOGhWRG9XSmFFZXQ2KytqVFBIVCt3Zlc0aFd6L2hWdktvR2p3YmJXOUhFTzZ5WGIKK1hFRmQvSjhHUml5a0ZldVZhMlZaMkRza0lFR05SWllKR2JhNlFJREFRQUJBb0lCQVFDNXRLQnJZNEFUWHFSawo1SE5jMmNYM1RHQXhPd21vc2grL2N6RS9WRVpibVF6R3NkaGxjQWNxLzhRbGRxWlpGRFMrMmZrVEFvMVNZbk1GCjltdHc0WFRmODY2MUNUcFNQYjdSN1RhNmlmMXVNenRPUjNYdG9YbDRHNG0rL3FvWWliaWZBZ1hyZFhla3JBc24KTXF6dkRqQ0RxR3VMOHk1SFg5a29DbXMvdnA5a3M2WTlla3czdHNIMVNHZE5iNml2MU81bjV4OHRjNXQ3QUUyMgpzVHdEZThlZXBjZzV2MHhzRm9iU1dtNmo2a3ZCcnp2b2l4bmZ1ZWdMQ1FvcDMza1lKTUthSE95eFlZNW5YZnY5ClhNeHNPWldwcU9RU09ZVkdRZTdpUGc4aDYvOWxSZHYvbCtlTWd3MUtmaUlocENWNTdDbVMvd0pFY1lDSW1reFcKMXMweCs4OGRBb0dCQU9xMGtOU0lhSmRDaVJqaXZqWC9rSTY3K3lTN1ZGYStRbm5EN2ZvNHJGZ05nMzJGTWZmNgpQTGRvQThYRkJyYy9iZyttTGlFM3dldzBYR2lxZTlmdklVQXlnZ0FsNnlvdzdINnMwanZrcEd2UElaMzNyaHBOCmlCNzFmYVRZaldWYjgzWkk3YXVEeG1Mb1N3bStlbXJ5bWxlbXIwWlphZmI2QnN2VjVqSFkwV1ZyQW9HQkFNeHUKcjNxVmVEbzZmUTJKRDJrcUtDWmwybzRZaE9wZkxmUnJ3OFMxdFozWXAxRmRFcnVDMXN3NGRYRG9TcHJpcEdTQgo5a200VUdkSXg4ZjdCV1BhV0Nod0tXMCtiWnhZOHFoL2JKZUhRQTN2MEp4eTlaZUp1UEtBMnhtR3FoTkN3USsvClo4NGFWNkxuNnp6aFRqT0FjdStEb091TDV0akl3R3c1RlkzUzJRSDdBb0dBWFIzUVRCSG1kUVIzd2dETGVEN3gKaWo5NFQwVm5HNWNXWnByZVFxVFRjNGZCQUQ2azZYNUZNbnE0N0hEVHprWURFNEJaMHVIOU5RbzlFMlY0QnQ2ZgpzWW1ZWEJpdktTa0oydVFUOEtFd3Zua0tIRk1VcHVqVnRYcXVJNFdxNjJqRXVjd0xSejNicW9nQXBWZ0YxNEp5Cnk0MmRBbXNkQ0ZoLzg4VGtOQ2lTUXdzQ2dZRUFvQTU5QjEzbFhybVNWVG1kUGpwS2F4M055dmo1ZjhKN2FXWEMKUmMzNjN4WFVra2hydFRIUVdONVBYTklTTDBnSmE4T3cvN0QyQ3BlYUMwSEd5NUlVK2J3dlF4L2drOHUwV1NaQwo4RFJ0ZXp2cXVjTHI4L1JaUUV5UXZtQ0g0a0tlZzJUWnNpMC90Z1VjVVhNWlZndFljWncrTG8wL2RUVmdLcHRhCis4bzhLMmNDZ1lFQXk4dC9sQkZwRzNNdjBBaTlIT2VnUEhlSGZOekd1a0RpbVpmWEdqWW1qMEJOYVZjRkZya2gKaTZ6SzhPbml6bWlFLzI2bm45RG5vV3NDbm9vVVRjMnpuWlhJVCtKYXhGNnVDcG51M2JKaTVrUzBreFZYcWpmUQpWb2ZFeW9QQkdmYWVRSkhpNytYemh2UFVocDFUQkNMbGlLOUxmZkkrV0ZEU0poazZIMytWVFJBPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= +` +) + +var _ = Describe("k8sclient tests", func() { + var ( + tmpDir string + validKubeconfigPath string + invalidKubeconfigPath string + ) + + BeforeEach(func() { + var err error + tmpDir := GinkgoT().TempDir() + + validKubeconfigPath = filepath.Join(tmpDir, "kube.conf") + invalidKubeconfigPath = filepath.Join(tmpDir, "kube-invalid.conf") + + err = os.WriteFile(validKubeconfigPath, []byte(kubeconfigTestData), 0744) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(invalidKubeconfigPath, []byte("This is an invalid kubeconfig content"), 0744) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("FromKubeconfig", func() { + It("creates k8s deffered client if kubeconfig valid", func() { + _, err := k8sclient.FromKubeconfig(validKubeconfigPath) + Expect(err).ToNot(HaveOccurred()) + }) + + It("fails if kubeconfig path not found", func() { + _, err := k8sclient.FromKubeconfig(filepath.Join(tmpDir, "does-not-exist.conf")) + Expect(err).To(HaveOccurred()) + }) + + It("fails if kubeconfig file contains garbage", func() { + _, err := k8sclient.FromKubeconfig(invalidKubeconfigPath) + Expect(err).To(HaveOccurred()) + }) + }) +}) From 83038ea3b3f0c2120076aa18c693fc955c678335 Mon Sep 17 00:00:00 2001 From: adrianc Date: Wed, 17 May 2023 18:50:03 +0300 Subject: [PATCH 3/9] add pool tests Signed-off-by: adrianc --- pkg/pool/annotations.go | 6 +- pkg/pool/annotations_test.go | 111 +++++++++++++++++++++++++++++++++++ pkg/pool/pool.go | 10 ++-- pkg/pool/pool_suite_test.go | 26 ++++++++ pkg/pool/pool_test.go | 92 +++++++++++++++++++++++++++++ 5 files changed, 238 insertions(+), 7 deletions(-) create mode 100644 pkg/pool/annotations_test.go create mode 100644 pkg/pool/pool_suite_test.go create mode 100644 pkg/pool/pool_test.go diff --git a/pkg/pool/annotations.go b/pkg/pool/annotations.go index 15fc137..e8f6e47 100644 --- a/pkg/pool/annotations.go +++ b/pkg/pool/annotations.go @@ -30,20 +30,20 @@ func SetIPBlockAnnotation(node *v1.Node, pools map[string]*IPPool) error { if err != nil { return fmt.Errorf("failed to serialize pools config: %v", err) } - annotations[ipBlocksAnnotation] = string(data) + annotations[IPBlocksAnnotation] = string(data) node.SetAnnotations(annotations) return nil } // IPBlockAnnotationExists returns true if ip-block annotation exist func IPBlockAnnotationExists(node *v1.Node) bool { - _, exist := node.GetAnnotations()[ipBlocksAnnotation] + _, exist := node.GetAnnotations()[IPBlocksAnnotation] return exist } // RemoveIPBlockAnnotation removes annotation with ip-block from the node object func RemoveIPBlockAnnotation(node *v1.Node) { annotations := node.GetAnnotations() - delete(annotations, ipBlocksAnnotation) + delete(annotations, IPBlocksAnnotation) node.SetAnnotations(annotations) } diff --git a/pkg/pool/annotations_test.go b/pkg/pool/annotations_test.go new file mode 100644 index 0000000..c1b8297 --- /dev/null +++ b/pkg/pool/annotations_test.go @@ -0,0 +1,111 @@ +/* + Copyright 2023, NVIDIA CORPORATION & AFFILIATES + 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 pool_test + +import ( + "encoding/json" + + v1 "k8s.io/api/core/v1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Mellanox/nvidia-k8s-ipam/pkg/pool" +) + +var _ = Describe("annotations tests", func() { + Context("SetIPBlockAnnotation", func() { + testPools := make(map[string]*pool.IPPool) + testPools["my-pool-1"] = &pool.IPPool{ + Name: "my-pool-1", + Subnet: "192.168.0.0/16", + StartIP: "192.168.0.2", + EndIP: "192.168.0.254", + Gateway: "192.168.0.1", + } + testPools["my-pool-2"] = &pool.IPPool{ + Name: "my-pool-2", + Subnet: "10.100.0.0/16", + StartIP: "10.100.0.2", + EndIP: "10.100.0.254", + Gateway: "10.100.0.1", + } + + It("sets annotation successfully", func() { + n := v1.Node{} + pool.SetIPBlockAnnotation(&n, testPools) + Expect(n.GetAnnotations()).ToNot(BeNil()) + + data, err := json.Marshal(testPools) + Expect(err).ToNot(HaveOccurred()) + Expect(n.GetAnnotations()[pool.IPBlocksAnnotation]).To(Equal(string(data))) + }) + + It("overwrites annotation successfully", func() { + n := v1.Node{} + annot := map[string]string{ + pool.IPBlocksAnnotation: `{"my-pool": {"some": "content"}}`, + } + n.SetAnnotations(annot) + + pool.SetIPBlockAnnotation(&n, testPools) + Expect(n.GetAnnotations()).ToNot(BeNil()) + + data, err := json.Marshal(testPools) + Expect(err).ToNot(HaveOccurred()) + Expect(n.GetAnnotations()[pool.IPBlocksAnnotation]).To(Equal(string(data))) + }) + }) + + Context("IPBlockAnnotationExists", func() { + It("returns true if annotation exists", func() { + n := v1.Node{} + ipBlockAnnot := make(map[string]string) + ipBlockAnnot[pool.IPBlocksAnnotation] = "foobar" + n.SetAnnotations(ipBlockAnnot) + + Expect(pool.IPBlockAnnotationExists(&n)).To(BeTrue()) + }) + + It("returns false if annotation does not exists", func() { + Expect(pool.IPBlockAnnotationExists(&v1.Node{})).To(BeFalse()) + }) + }) + + Context("RemoveIPBlockAnnotation", func() { + It("Succeeds if annotation does not exist", func() { + n := v1.Node{} + annot := map[string]string{ + "foo": "bar", + } + n.SetAnnotations(annot) + + pool.RemoveIPBlockAnnotation(&n) + Expect(n.GetAnnotations()).To(HaveKey("foo")) + }) + + It("removes annotation if exists", func() { + n := v1.Node{} + annot := map[string]string{ + "foo": "bar", + pool.IPBlocksAnnotation: "baz", + } + n.SetAnnotations(annot) + + pool.RemoveIPBlockAnnotation(&n) + Expect(n.GetAnnotations()).To(HaveKey("foo")) + Expect(n.GetAnnotations()).ToNot(HaveKey(pool.IPBlocksAnnotation)) + }) + }) +}) diff --git a/pkg/pool/pool.go b/pkg/pool/pool.go index 90447c7..f9b48ec 100644 --- a/pkg/pool/pool.go +++ b/pkg/pool/pool.go @@ -21,9 +21,10 @@ import ( ) const ( - ipBlocksAnnotation = "ipam.nvidia.com/ip-blocks" + IPBlocksAnnotation = "ipam.nvidia.com/ip-blocks" ) +// IPPool represents a block of IPs from a given Subnet type IPPool struct { Name string `json:"-"` Subnet string `json:"subnet"` @@ -32,6 +33,7 @@ type IPPool struct { Gateway string `json:"gateway"` } +// Manager is an interface to manage IPPools type Manager interface { // GetPoolByName returns IPPool for the provided pool name or nil if pool doesnt exist GetPoolByName(name string) *IPPool @@ -48,15 +50,15 @@ func NewManagerImpl(node *v1.Node) (*ManagerImpl, error) { return nil, fmt.Errorf("nil node provided") } - blocks, ok := node.Annotations[ipBlocksAnnotation] + blocks, ok := node.Annotations[IPBlocksAnnotation] if !ok { - return nil, fmt.Errorf("%s node annotation not found", ipBlocksAnnotation) + return nil, fmt.Errorf("%s node annotation not found", IPBlocksAnnotation) } poolByName := make(map[string]*IPPool) err := json.Unmarshal([]byte(blocks), &poolByName) if err != nil { - return nil, fmt.Errorf("failed to parse %s annotation content. %w", ipBlocksAnnotation, err) + return nil, fmt.Errorf("failed to parse %s annotation content. %w", IPBlocksAnnotation, err) } for poolName, pool := range poolByName { diff --git a/pkg/pool/pool_suite_test.go b/pkg/pool/pool_suite_test.go new file mode 100644 index 0000000..707716e --- /dev/null +++ b/pkg/pool/pool_suite_test.go @@ -0,0 +1,26 @@ +/* + Copyright 2023, NVIDIA CORPORATION & AFFILIATES + 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 pool_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPool(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "pool Suite") +} diff --git a/pkg/pool/pool_test.go b/pkg/pool/pool_test.go new file mode 100644 index 0000000..def1f38 --- /dev/null +++ b/pkg/pool/pool_test.go @@ -0,0 +1,92 @@ +/* + Copyright 2023, NVIDIA CORPORATION & AFFILIATES + 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 pool_test + +import ( + v1 "k8s.io/api/core/v1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Mellanox/nvidia-k8s-ipam/pkg/pool" +) + +var _ = Describe("pool tests", func() { + Context("NewManagerImpl()", func() { + It("Creates a Manager successfully if node has ip-pool annotation", func() { + n := v1.Node{} + emptyAnnot := map[string]string{ + pool.IPBlocksAnnotation: "{}", + } + n.SetAnnotations(emptyAnnot) + m, err := pool.NewManagerImpl(&n) + Expect(err).ToNot(HaveOccurred()) + Expect(m.GetPools()).To(HaveLen(0)) + + annot := map[string]string{ + pool.IPBlocksAnnotation: `{"my-pool": + {"subnet": "192.168.0.0/16", "startIP": "192.168.0.2", + "endIP": "192.168.0.254", "gateway": "192.168.0.1"}}`, + } + n.SetAnnotations(annot) + m, err = pool.NewManagerImpl(&n) + Expect(err).ToNot(HaveOccurred()) + Expect(m.GetPools()).To(HaveLen(1)) + }) + + It("Fails to create Manager if node is missing ip-pool annotation", func() { + n := v1.Node{} + _, err := pool.NewManagerImpl(&n) + Expect(err).To(HaveOccurred()) + }) + + It("Fails to create Manager if node has empty ip-pool annotation", func() { + n := v1.Node{} + emptyAnnot := map[string]string{ + pool.IPBlocksAnnotation: "", + } + n.SetAnnotations(emptyAnnot) + _, err := pool.NewManagerImpl(&n) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("GetPoolByName()", func() { + var m pool.Manager + + BeforeEach(func() { + var err error + n := v1.Node{} + annot := map[string]string{ + pool.IPBlocksAnnotation: `{"my-pool": + {"subnet": "192.168.0.0/16", "startIP": "192.168.0.2", + "endIP": "192.168.0.254", "gateway": "192.168.0.1"}}`, + } + n.SetAnnotations(annot) + m, err = pool.NewManagerImpl(&n) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns nil if pool does not exist", func() { + p := m.GetPoolByName("non-existent-pool") + Expect(p).To(BeNil()) + }) + + It("returns pool if exists", func() { + p := m.GetPoolByName("my-pool") + Expect(p).ToNot(BeNil()) + Expect(p.Subnet).To(Equal("192.168.0.0/16")) + }) + }) +}) From cde8501305ec90defdc68377648d3bd055c5f0e8 Mon Sep 17 00:00:00 2001 From: adrianc Date: Thu, 18 May 2023 15:22:13 +0300 Subject: [PATCH 4/9] k8sclient tests to use testdata from file Signed-off-by: adrianc --- pkg/cni/k8sclient/k8sclient_test.go | 31 +++++------------------------ testdata/test.kubeconfig | 19 ++++++++++++++++++ 2 files changed, 24 insertions(+), 26 deletions(-) create mode 100644 testdata/test.kubeconfig diff --git a/pkg/cni/k8sclient/k8sclient_test.go b/pkg/cni/k8sclient/k8sclient_test.go index 460fe1a..0318241 100644 --- a/pkg/cni/k8sclient/k8sclient_test.go +++ b/pkg/cni/k8sclient/k8sclient_test.go @@ -15,6 +15,7 @@ package k8sclient_test import ( "os" + "path" "path/filepath" . "github.com/onsi/ginkgo/v2" @@ -23,30 +24,6 @@ import ( "github.com/Mellanox/nvidia-k8s-ipam/pkg/cni/k8sclient" ) -const ( - kubeconfigTestData = ` -apiVersion: v1 -clusters: -- cluster: - certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJeU1EVXlOREUwTURjeU5Gb1hEVE15TURVeU1URTBNRGN5TkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTkpJCnZUT2swNEtGVm51a0gyNUw3V2IvTFUxNVo4QUw4ZVFERXZZTnUyVys1eklaemxQTW5lUXYvWm1GcDloakpuUk0KQzc2N0s0cHpTMVNjcWlpUGxKaUlxNGhjcVJLdTA3bHZ6WTNiZWllNVhubXYzTm42YTM4THVHNVhud29pM3ZsdwpscUh4L3lHdEFicHRlUFA5K3FQRndySGxGMGlNMHZVb3BjNmt5cmUwR0RnVEkzUVdUQ3RmcDl2RDFydU1ER2krCnhjWnJueHVTcldkekdoWEV5d0FDWUtiaDEwUFQwbVJaK2p5MVViRFp3alk4VHN5WFl1WHpHci9KMUcvc0hOSFEKd1dmMnNDRVJnVjB1N2Z0eHhrZ0ZETXJOcllqcjJFUERhZ0lPeDdTTGFQVCtSNWN3bmc5Vml6ajNicnMzekppRwp3ZnNiZ1gxSTdlaHMzNmh6VXNVQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZJUjc1MWhlVG1Qcm4rb1N3NjltUXpWSEVpVWVNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQkZpaWNkOTdERGR5QjUvbjg0agpZb2pIN1dNT1dDeTRwQ2ZuNWx0L2MyR2VRcDA3Y3NWbXlwUnY4YzQ3ZGYxVklIL09zcDlVczBoVWh1NFYybjBhCnZwM2svM1pSTEd3NVVzZGZoTkJrOTZWd0lRRFhVdER6NDZQU2dMUSsvREJqSitFbTJCanlyWm9PUkFGTDhsWkQKNzl2SndadXl6RC90UFFPL2xSZjRWZHYxQTl4R3U2QlJyRzdybmhBUVlxNDBBbGZ2dExlM2dqa1VFRVROZWFJOAorTERmeUs0cmVnTGc1emFNeDVvQmNyVzhiWHBxdTNFSlZHUXhoM3pVOVhmeDlJckdidE1zUHY2bXBEenV1TmhBCm5WemlPbC95TDhTMGdRTUt4WU9YdHpKYWdhU3JzQm82K3ZmenZkRHIzUnFST0VCSXU4eC9yRmsxNTNMdlUrMmUKdjY0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== - server: https://7.7.7.7:6443 - name: kubernetes -contexts: -- context: - cluster: kubernetes - user: kubernetes-admin - name: kubernetes-admin@kubernetes -current-context: kubernetes-admin@kubernetes -kind: Config -preferences: {} -users: -- name: kubernetes-admin - user: - client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJVENDQWdtZ0F3SUJBZ0lJUG9Bd0xoc2pRNFV3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TWpBMU1qUXhOREEzTWpSYUZ3MHlNekExTWpReE5EQTNNamRhTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXUyMWQ0V29kbGxTa2VhcDUKZUhqMnpIaGVFblNPaGxVbVdKanZ2VmtMdkd6RHR6ZkErMVo2b09xMkt1V0FtcVdjbjhIRjFuQU5aR0JyeFd2cgp3OHVPbzBYZHJpWEdDV1FkRi9sUTl0UXdIU1VWa1NTQzdIU0VwUHhXUVFjU3NoZnJVQ25YSjVkVllaWElkdHpnCnJURzVUZURENERnZCtPdUJhcmNiS096emFzcHpWalI0QStmUm1GQ1V2cVVSRTZSOTZRYmNVdGNzS05vQndpTisKcmtNNDFyOEJyUXBEZDNsUGhRWUdQdmN3TkdaNC8waUJFdDNLN1h3RUI2eFY4WDg1QjhoVkRvV0phRWV0NisragpUUEhUK3dmVzRoV3ovaFZ2S29HandiYlc5SEVPNnlYYitYRUZkL0o4R1JpeWtGZXVWYTJWWjJEc2tJRUdOUlpZCkpHYmE2UUlEQVFBQm8xWXdWREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RBWURWUjBUQVFIL0JBSXdBREFmQmdOVkhTTUVHREFXZ0JTRWUrZFlYazVqNjUvcUVzT3Zaa00xUnhJbApIakFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBTEdLbklSU05MaWtXenVpdDgyK0VTS21ZSWRZWVZwSVdJOFVQCmRnbXh0T0o2N3FWeWFtNnlqYzZRc3o4VE1jYk9lZzFwQ1hzbGVhd2VXZWpydmE3bzlhalVNTTNxNFBXSFg2aVMKTG13em5ockRiWTBsdC9qMGg1akJhMnlBOERoRml5WDBDR2ROMDVWSlg0aWZqL0I4ZC9tS1lXMzBDNHlJd2RFWApUdFZqRmgydzRFdlphNkpLZjRtMDRmSENtRjhMbzRNbkhJY0NObndGMmNwdHR4b1dHOWcxMEFjSnhkZy9SREhxClljZUR5bmUwYklDbGxrRlpFdStwdEIxcDkzVUI2RFY0VHh1VGpSZVFKYmt1Y2prQ1FtcTVmdXUrVjl5NE90TDMKK0VjVFpvSDUyV3dtZlk1TnJyT1hyTTl1UXpHSkQxaklYOE1LMVZlWjNDVmh4bkMxZ1E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== - client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdTIxZDRXb2RsbFNrZWFwNWVIajJ6SGhlRW5TT2hsVW1XSmp2dlZrTHZHekR0emZBCisxWjZvT3EyS3VXQW1xV2NuOEhGMW5BTlpHQnJ4V3Zydzh1T28wWGRyaVhHQ1dRZEYvbFE5dFF3SFNVVmtTU0MKN0hTRXBQeFdRUWNTc2hmclVDblhKNWRWWVpYSWR0emdyVEc1VGVERDREZ2QrT3VCYXJjYktPenphc3B6VmpSNApBK2ZSbUZDVXZxVVJFNlI5NlFiY1V0Y3NLTm9Cd2lOK3JrTTQxcjhCclFwRGQzbFBoUVlHUHZjd05HWjQvMGlCCkV0M0s3WHdFQjZ4VjhYODVCOGhWRG9XSmFFZXQ2KytqVFBIVCt3Zlc0aFd6L2hWdktvR2p3YmJXOUhFTzZ5WGIKK1hFRmQvSjhHUml5a0ZldVZhMlZaMkRza0lFR05SWllKR2JhNlFJREFRQUJBb0lCQVFDNXRLQnJZNEFUWHFSawo1SE5jMmNYM1RHQXhPd21vc2grL2N6RS9WRVpibVF6R3NkaGxjQWNxLzhRbGRxWlpGRFMrMmZrVEFvMVNZbk1GCjltdHc0WFRmODY2MUNUcFNQYjdSN1RhNmlmMXVNenRPUjNYdG9YbDRHNG0rL3FvWWliaWZBZ1hyZFhla3JBc24KTXF6dkRqQ0RxR3VMOHk1SFg5a29DbXMvdnA5a3M2WTlla3czdHNIMVNHZE5iNml2MU81bjV4OHRjNXQ3QUUyMgpzVHdEZThlZXBjZzV2MHhzRm9iU1dtNmo2a3ZCcnp2b2l4bmZ1ZWdMQ1FvcDMza1lKTUthSE95eFlZNW5YZnY5ClhNeHNPWldwcU9RU09ZVkdRZTdpUGc4aDYvOWxSZHYvbCtlTWd3MUtmaUlocENWNTdDbVMvd0pFY1lDSW1reFcKMXMweCs4OGRBb0dCQU9xMGtOU0lhSmRDaVJqaXZqWC9rSTY3K3lTN1ZGYStRbm5EN2ZvNHJGZ05nMzJGTWZmNgpQTGRvQThYRkJyYy9iZyttTGlFM3dldzBYR2lxZTlmdklVQXlnZ0FsNnlvdzdINnMwanZrcEd2UElaMzNyaHBOCmlCNzFmYVRZaldWYjgzWkk3YXVEeG1Mb1N3bStlbXJ5bWxlbXIwWlphZmI2QnN2VjVqSFkwV1ZyQW9HQkFNeHUKcjNxVmVEbzZmUTJKRDJrcUtDWmwybzRZaE9wZkxmUnJ3OFMxdFozWXAxRmRFcnVDMXN3NGRYRG9TcHJpcEdTQgo5a200VUdkSXg4ZjdCV1BhV0Nod0tXMCtiWnhZOHFoL2JKZUhRQTN2MEp4eTlaZUp1UEtBMnhtR3FoTkN3USsvClo4NGFWNkxuNnp6aFRqT0FjdStEb091TDV0akl3R3c1RlkzUzJRSDdBb0dBWFIzUVRCSG1kUVIzd2dETGVEN3gKaWo5NFQwVm5HNWNXWnByZVFxVFRjNGZCQUQ2azZYNUZNbnE0N0hEVHprWURFNEJaMHVIOU5RbzlFMlY0QnQ2ZgpzWW1ZWEJpdktTa0oydVFUOEtFd3Zua0tIRk1VcHVqVnRYcXVJNFdxNjJqRXVjd0xSejNicW9nQXBWZ0YxNEp5Cnk0MmRBbXNkQ0ZoLzg4VGtOQ2lTUXdzQ2dZRUFvQTU5QjEzbFhybVNWVG1kUGpwS2F4M055dmo1ZjhKN2FXWEMKUmMzNjN4WFVra2hydFRIUVdONVBYTklTTDBnSmE4T3cvN0QyQ3BlYUMwSEd5NUlVK2J3dlF4L2drOHUwV1NaQwo4RFJ0ZXp2cXVjTHI4L1JaUUV5UXZtQ0g0a0tlZzJUWnNpMC90Z1VjVVhNWlZndFljWncrTG8wL2RUVmdLcHRhCis4bzhLMmNDZ1lFQXk4dC9sQkZwRzNNdjBBaTlIT2VnUEhlSGZOekd1a0RpbVpmWEdqWW1qMEJOYVZjRkZya2gKaTZ6SzhPbml6bWlFLzI2bm45RG5vV3NDbm9vVVRjMnpuWlhJVCtKYXhGNnVDcG51M2JKaTVrUzBreFZYcWpmUQpWb2ZFeW9QQkdmYWVRSkhpNytYemh2UFVocDFUQkNMbGlLOUxmZkkrV0ZEU0poazZIMytWVFJBPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= -` -) - var _ = Describe("k8sclient tests", func() { var ( tmpDir string @@ -61,10 +38,12 @@ var _ = Describe("k8sclient tests", func() { validKubeconfigPath = filepath.Join(tmpDir, "kube.conf") invalidKubeconfigPath = filepath.Join(tmpDir, "kube-invalid.conf") - err = os.WriteFile(validKubeconfigPath, []byte(kubeconfigTestData), 0744) + data, err := os.ReadFile(path.Join("..", "..", "..", "testdata", "test.kubeconfig")) + Expect(err).ToNot(HaveOccurred()) + err = os.WriteFile(validKubeconfigPath, data, 0o644) Expect(err).NotTo(HaveOccurred()) - err = os.WriteFile(invalidKubeconfigPath, []byte("This is an invalid kubeconfig content"), 0744) + err = os.WriteFile(invalidKubeconfigPath, []byte("This is an invalid kubeconfig content"), 0o644) Expect(err).NotTo(HaveOccurred()) }) diff --git a/testdata/test.kubeconfig b/testdata/test.kubeconfig new file mode 100644 index 0000000..f2424f6 --- /dev/null +++ b/testdata/test.kubeconfig @@ -0,0 +1,19 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJeU1EVXlOREUwTURjeU5Gb1hEVE15TURVeU1URTBNRGN5TkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTkpJCnZUT2swNEtGVm51a0gyNUw3V2IvTFUxNVo4QUw4ZVFERXZZTnUyVys1eklaemxQTW5lUXYvWm1GcDloakpuUk0KQzc2N0s0cHpTMVNjcWlpUGxKaUlxNGhjcVJLdTA3bHZ6WTNiZWllNVhubXYzTm42YTM4THVHNVhud29pM3ZsdwpscUh4L3lHdEFicHRlUFA5K3FQRndySGxGMGlNMHZVb3BjNmt5cmUwR0RnVEkzUVdUQ3RmcDl2RDFydU1ER2krCnhjWnJueHVTcldkekdoWEV5d0FDWUtiaDEwUFQwbVJaK2p5MVViRFp3alk4VHN5WFl1WHpHci9KMUcvc0hOSFEKd1dmMnNDRVJnVjB1N2Z0eHhrZ0ZETXJOcllqcjJFUERhZ0lPeDdTTGFQVCtSNWN3bmc5Vml6ajNicnMzekppRwp3ZnNiZ1gxSTdlaHMzNmh6VXNVQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZJUjc1MWhlVG1Qcm4rb1N3NjltUXpWSEVpVWVNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQkZpaWNkOTdERGR5QjUvbjg0agpZb2pIN1dNT1dDeTRwQ2ZuNWx0L2MyR2VRcDA3Y3NWbXlwUnY4YzQ3ZGYxVklIL09zcDlVczBoVWh1NFYybjBhCnZwM2svM1pSTEd3NVVzZGZoTkJrOTZWd0lRRFhVdER6NDZQU2dMUSsvREJqSitFbTJCanlyWm9PUkFGTDhsWkQKNzl2SndadXl6RC90UFFPL2xSZjRWZHYxQTl4R3U2QlJyRzdybmhBUVlxNDBBbGZ2dExlM2dqa1VFRVROZWFJOAorTERmeUs0cmVnTGc1emFNeDVvQmNyVzhiWHBxdTNFSlZHUXhoM3pVOVhmeDlJckdidE1zUHY2bXBEenV1TmhBCm5WemlPbC95TDhTMGdRTUt4WU9YdHpKYWdhU3JzQm82K3ZmenZkRHIzUnFST0VCSXU4eC9yRmsxNTNMdlUrMmUKdjY0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + server: https://7.7.7.7:6443 + name: kubernetes +contexts: +- context: + cluster: kubernetes + user: kubernetes-admin + name: kubernetes-admin@kubernetes +current-context: kubernetes-admin@kubernetes +kind: Config +preferences: {} +users: +- name: kubernetes-admin + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJVENDQWdtZ0F3SUJBZ0lJUG9Bd0xoc2pRNFV3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TWpBMU1qUXhOREEzTWpSYUZ3MHlNekExTWpReE5EQTNNamRhTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXUyMWQ0V29kbGxTa2VhcDUKZUhqMnpIaGVFblNPaGxVbVdKanZ2VmtMdkd6RHR6ZkErMVo2b09xMkt1V0FtcVdjbjhIRjFuQU5aR0JyeFd2cgp3OHVPbzBYZHJpWEdDV1FkRi9sUTl0UXdIU1VWa1NTQzdIU0VwUHhXUVFjU3NoZnJVQ25YSjVkVllaWElkdHpnCnJURzVUZURENERnZCtPdUJhcmNiS096emFzcHpWalI0QStmUm1GQ1V2cVVSRTZSOTZRYmNVdGNzS05vQndpTisKcmtNNDFyOEJyUXBEZDNsUGhRWUdQdmN3TkdaNC8waUJFdDNLN1h3RUI2eFY4WDg1QjhoVkRvV0phRWV0NisragpUUEhUK3dmVzRoV3ovaFZ2S29HandiYlc5SEVPNnlYYitYRUZkL0o4R1JpeWtGZXVWYTJWWjJEc2tJRUdOUlpZCkpHYmE2UUlEQVFBQm8xWXdWREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RBWURWUjBUQVFIL0JBSXdBREFmQmdOVkhTTUVHREFXZ0JTRWUrZFlYazVqNjUvcUVzT3Zaa00xUnhJbApIakFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBTEdLbklSU05MaWtXenVpdDgyK0VTS21ZSWRZWVZwSVdJOFVQCmRnbXh0T0o2N3FWeWFtNnlqYzZRc3o4VE1jYk9lZzFwQ1hzbGVhd2VXZWpydmE3bzlhalVNTTNxNFBXSFg2aVMKTG13em5ockRiWTBsdC9qMGg1akJhMnlBOERoRml5WDBDR2ROMDVWSlg0aWZqL0I4ZC9tS1lXMzBDNHlJd2RFWApUdFZqRmgydzRFdlphNkpLZjRtMDRmSENtRjhMbzRNbkhJY0NObndGMmNwdHR4b1dHOWcxMEFjSnhkZy9SREhxClljZUR5bmUwYklDbGxrRlpFdStwdEIxcDkzVUI2RFY0VHh1VGpSZVFKYmt1Y2prQ1FtcTVmdXUrVjl5NE90TDMKK0VjVFpvSDUyV3dtZlk1TnJyT1hyTTl1UXpHSkQxaklYOE1LMVZlWjNDVmh4bkMxZ1E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdTIxZDRXb2RsbFNrZWFwNWVIajJ6SGhlRW5TT2hsVW1XSmp2dlZrTHZHekR0emZBCisxWjZvT3EyS3VXQW1xV2NuOEhGMW5BTlpHQnJ4V3Zydzh1T28wWGRyaVhHQ1dRZEYvbFE5dFF3SFNVVmtTU0MKN0hTRXBQeFdRUWNTc2hmclVDblhKNWRWWVpYSWR0emdyVEc1VGVERDREZ2QrT3VCYXJjYktPenphc3B6VmpSNApBK2ZSbUZDVXZxVVJFNlI5NlFiY1V0Y3NLTm9Cd2lOK3JrTTQxcjhCclFwRGQzbFBoUVlHUHZjd05HWjQvMGlCCkV0M0s3WHdFQjZ4VjhYODVCOGhWRG9XSmFFZXQ2KytqVFBIVCt3Zlc0aFd6L2hWdktvR2p3YmJXOUhFTzZ5WGIKK1hFRmQvSjhHUml5a0ZldVZhMlZaMkRza0lFR05SWllKR2JhNlFJREFRQUJBb0lCQVFDNXRLQnJZNEFUWHFSawo1SE5jMmNYM1RHQXhPd21vc2grL2N6RS9WRVpibVF6R3NkaGxjQWNxLzhRbGRxWlpGRFMrMmZrVEFvMVNZbk1GCjltdHc0WFRmODY2MUNUcFNQYjdSN1RhNmlmMXVNenRPUjNYdG9YbDRHNG0rL3FvWWliaWZBZ1hyZFhla3JBc24KTXF6dkRqQ0RxR3VMOHk1SFg5a29DbXMvdnA5a3M2WTlla3czdHNIMVNHZE5iNml2MU81bjV4OHRjNXQ3QUUyMgpzVHdEZThlZXBjZzV2MHhzRm9iU1dtNmo2a3ZCcnp2b2l4bmZ1ZWdMQ1FvcDMza1lKTUthSE95eFlZNW5YZnY5ClhNeHNPWldwcU9RU09ZVkdRZTdpUGc4aDYvOWxSZHYvbCtlTWd3MUtmaUlocENWNTdDbVMvd0pFY1lDSW1reFcKMXMweCs4OGRBb0dCQU9xMGtOU0lhSmRDaVJqaXZqWC9rSTY3K3lTN1ZGYStRbm5EN2ZvNHJGZ05nMzJGTWZmNgpQTGRvQThYRkJyYy9iZyttTGlFM3dldzBYR2lxZTlmdklVQXlnZ0FsNnlvdzdINnMwanZrcEd2UElaMzNyaHBOCmlCNzFmYVRZaldWYjgzWkk3YXVEeG1Mb1N3bStlbXJ5bWxlbXIwWlphZmI2QnN2VjVqSFkwV1ZyQW9HQkFNeHUKcjNxVmVEbzZmUTJKRDJrcUtDWmwybzRZaE9wZkxmUnJ3OFMxdFozWXAxRmRFcnVDMXN3NGRYRG9TcHJpcEdTQgo5a200VUdkSXg4ZjdCV1BhV0Nod0tXMCtiWnhZOHFoL2JKZUhRQTN2MEp4eTlaZUp1UEtBMnhtR3FoTkN3USsvClo4NGFWNkxuNnp6aFRqT0FjdStEb091TDV0akl3R3c1RlkzUzJRSDdBb0dBWFIzUVRCSG1kUVIzd2dETGVEN3gKaWo5NFQwVm5HNWNXWnByZVFxVFRjNGZCQUQ2azZYNUZNbnE0N0hEVHprWURFNEJaMHVIOU5RbzlFMlY0QnQ2ZgpzWW1ZWEJpdktTa0oydVFUOEtFd3Zua0tIRk1VcHVqVnRYcXVJNFdxNjJqRXVjd0xSejNicW9nQXBWZ0YxNEp5Cnk0MmRBbXNkQ0ZoLzg4VGtOQ2lTUXdzQ2dZRUFvQTU5QjEzbFhybVNWVG1kUGpwS2F4M055dmo1ZjhKN2FXWEMKUmMzNjN4WFVra2hydFRIUVdONVBYTklTTDBnSmE4T3cvN0QyQ3BlYUMwSEd5NUlVK2J3dlF4L2drOHUwV1NaQwo4RFJ0ZXp2cXVjTHI4L1JaUUV5UXZtQ0g0a0tlZzJUWnNpMC90Z1VjVVhNWlZndFljWncrTG8wL2RUVmdLcHRhCis4bzhLMmNDZ1lFQXk4dC9sQkZwRzNNdjBBaTlIT2VnUEhlSGZOekd1a0RpbVpmWEdqWW1qMEJOYVZjRkZya2gKaTZ6SzhPbml6bWlFLzI2bm45RG5vV3NDbm9vVVRjMnpuWlhJVCtKYXhGNnVDcG51M2JKaTVrUzBreFZYcWpmUQpWb2ZFeW9QQkdmYWVRSkhpNytYemh2UFVocDFUQkNMbGlLOUxmZkkrV0ZEU0poazZIMytWVFJBPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= \ No newline at end of file From c4330e327e19200eb1e8e468dd0449fd89e7aa3c Mon Sep 17 00:00:00 2001 From: adrianc Date: Thu, 18 May 2023 15:22:46 +0300 Subject: [PATCH 5/9] Add types tests Signed-off-by: adrianc --- pkg/cni/types/host-local_test.go | 57 +++++++++++++ pkg/cni/types/types_suite_test.go | 26 ++++++ pkg/cni/types/types_test.go | 131 ++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 pkg/cni/types/host-local_test.go create mode 100644 pkg/cni/types/types_suite_test.go create mode 100644 pkg/cni/types/types_test.go diff --git a/pkg/cni/types/host-local_test.go b/pkg/cni/types/host-local_test.go new file mode 100644 index 0000000..53fa722 --- /dev/null +++ b/pkg/cni/types/host-local_test.go @@ -0,0 +1,57 @@ +/* + Copyright 2023, NVIDIA CORPORATION & AFFILIATES + 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 types_test + +import ( + "path" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Mellanox/nvidia-k8s-ipam/pkg/cni/types" + "github.com/Mellanox/nvidia-k8s-ipam/pkg/pool" +) + +var _ = Describe("host-local types tests", func() { + Context("HostLocalNetConfFromNetConfAndPool", func() { + It("Converts to host-local netconf", func() { + conf := types.NetConf{ + Name: "my-net", + CNIVersion: "0.4.0", + IPAM: &types.IPAMConf{ + PoolName: "my-pool", + DataDir: "/foo/bar", + }, + } + pool := pool.IPPool{ + Name: "my-pool", + Subnet: "192.168.0.0/16", + StartIP: "192.168.0.2", + EndIP: "192.168.0.254", + Gateway: "192.168.0.1", + } + + hostlocalConf := types.HostLocalNetConfFromNetConfAndPool(&conf, &pool) + + Expect(hostlocalConf.Name).To(Equal(pool.Name)) + Expect(hostlocalConf.CNIVersion).To(Equal(conf.CNIVersion)) + Expect(hostlocalConf.IPAM.Type).To(Equal("host-local")) + Expect(hostlocalConf.IPAM.DataDir).To(Equal(path.Join(conf.IPAM.DataDir, types.HostLocalDataDir))) + Expect(hostlocalConf.IPAM.Ranges[0][0].Subnet).To(Equal(pool.Subnet)) + Expect(hostlocalConf.IPAM.Ranges[0][0].RangeStart).To(Equal(pool.StartIP)) + Expect(hostlocalConf.IPAM.Ranges[0][0].RangeEnd).To(Equal(pool.EndIP)) + Expect(hostlocalConf.IPAM.Ranges[0][0].Gateway).To(Equal(pool.Gateway)) + }) + }) +}) diff --git a/pkg/cni/types/types_suite_test.go b/pkg/cni/types/types_suite_test.go new file mode 100644 index 0000000..37557f3 --- /dev/null +++ b/pkg/cni/types/types_suite_test.go @@ -0,0 +1,26 @@ +/* + Copyright 2023, NVIDIA CORPORATION & AFFILIATES + 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 types_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestTypes(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Types Suite") +} diff --git a/pkg/cni/types/types_test.go b/pkg/cni/types/types_test.go new file mode 100644 index 0000000..4230114 --- /dev/null +++ b/pkg/cni/types/types_test.go @@ -0,0 +1,131 @@ +/* + Copyright 2023, NVIDIA CORPORATION & AFFILIATES + 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 types_test + +import ( + "fmt" + "os" + "path" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Mellanox/nvidia-k8s-ipam/pkg/cni/types" +) + +var _ = Describe("Types Tests", func() { + var ( + tmpDir string + testConfDir string + ) + + BeforeEach(func() { + tmpDir = GinkgoT().TempDir() + testConfDir = path.Join(tmpDir, "nv-ipam.d") + err := os.Mkdir(testConfDir, 0o755) + Expect(err).ToNot(HaveOccurred()) + err = os.WriteFile(path.Join(testConfDir, types.K8sNodeNameFile), []byte("test-node"), 0o644) + Expect(err).ToNot(HaveOccurred()) + }) + + Context("LoadConf()", func() { + It("loads default configuration when no overwrites provided", func() { + // write empty config file + err := os.WriteFile(path.Join(testConfDir, types.ConfFileName), []byte("{}"), 0o644) + Expect(err).ToNot(HaveOccurred()) + + // write kubeconfig file + data, err := os.ReadFile(path.Join("..", "..", "..", "testdata", "test.kubeconfig")) + Expect(err).ToNot(HaveOccurred()) + err = os.WriteFile(path.Join(testConfDir, types.DefaultKubeConfigFileName), []byte(data), 0o644) + Expect(err).ToNot(HaveOccurred()) + + // Load config + testConf := fmt.Sprintf(`{"name": "my-net", "ipam": {"confDir": %q}}`, testConfDir) + conf, err := types.LoadConf([]byte(testConf)) + + // Validate + Expect(err).ToNot(HaveOccurred()) + Expect(conf.IPAM.PoolName).To(Equal(conf.Name)) + Expect(conf.IPAM.ConfDir).To(Equal(testConfDir)) + Expect(conf.IPAM.DataDir).To(Equal(types.DefaultDataDir)) + Expect(conf.IPAM.LogFile).To(Equal(types.DefaultLogFile)) + Expect(conf.IPAM.LogLevel).To(Equal("info")) + Expect(conf.IPAM.Kubeconfig).To(Equal(path.Join(conf.IPAM.ConfDir, types.DefaultKubeConfigFileName))) + }) + + It("overwrites configuration from file", func() { + // write config file + confData := fmt.Sprintf(` + {"logLevel": "debug", "logFile": "some/path.log", "dataDir": "some/data/path", + "kubeconfig": "%s/alternate.kubeconfig"}`, testConfDir) + err := os.WriteFile(path.Join(testConfDir, types.ConfFileName), []byte(confData), 0o644) + Expect(err).ToNot(HaveOccurred()) + + // write kubeconfig file + data, err := os.ReadFile(path.Join("..", "..", "..", "testdata", "test.kubeconfig")) + Expect(err).ToNot(HaveOccurred()) + err = os.WriteFile(path.Join(testConfDir, "alternate.kubeconfig"), []byte(data), 0o644) + Expect(err).ToNot(HaveOccurred()) + + // Load config + testConf := fmt.Sprintf(`{"name": "my-net", "ipam": {"confDir": %q}}`, testConfDir) + conf, err := types.LoadConf([]byte(testConf)) + + // Validate + Expect(err).ToNot(HaveOccurred()) + Expect(conf.IPAM.ConfDir).To(Equal(testConfDir)) + Expect(conf.IPAM.DataDir).To(Equal("some/data/path")) + Expect(conf.IPAM.LogFile).To(Equal("some/path.log")) + Expect(conf.IPAM.LogLevel).To(Equal("debug")) + Expect(conf.IPAM.Kubeconfig).To(Equal(path.Join(conf.IPAM.ConfDir, "alternate.kubeconfig"))) + }) + + It("overwrites configuration from json input", func() { + // write config file + confData := `{"logLevel": "debug", "dataDir": "some/data/path"}` + err := os.WriteFile(path.Join(testConfDir, types.ConfFileName), []byte(confData), 0o644) + Expect(err).ToNot(HaveOccurred()) + + // write kubeconfig file + data, err := os.ReadFile(path.Join("..", "..", "..", "testdata", "test.kubeconfig")) + Expect(err).ToNot(HaveOccurred()) + err = os.WriteFile(path.Join(testConfDir, types.DefaultKubeConfigFileName), []byte(data), 0o644) + Expect(err).ToNot(HaveOccurred()) + + // Load config + testConf := fmt.Sprintf(`{"name": "my-net", "ipam": {"confDir": %q, "poolName": "my-pool", "logLevel": "error"}}`, testConfDir) + conf, err := types.LoadConf([]byte(testConf)) + + // Validate + Expect(err).ToNot(HaveOccurred()) + Expect(conf.IPAM.PoolName).To(Equal("my-pool")) + Expect(conf.IPAM.ConfDir).To(Equal(testConfDir)) + Expect(conf.IPAM.DataDir).To(Equal("some/data/path")) + Expect(conf.IPAM.LogFile).To(Equal(types.DefaultLogFile)) + Expect(conf.IPAM.LogLevel).To(Equal("error")) + Expect(conf.IPAM.Kubeconfig).To(Equal(path.Join(conf.IPAM.ConfDir, types.DefaultKubeConfigFileName))) + }) + + It("Fails if config is invalid json", func() { + _, err := types.LoadConf([]byte("{garbage%^&*")) + Expect(err).To(HaveOccurred()) + }) + + It("Fails if config does not contain ipam key", func() { + _, err := types.LoadConf([]byte(`{"name": "my-net", "type": "sriov"}`)) + Expect(err).To(HaveOccurred()) + }) + }) +}) From a6855aeb71be547bae8598fec84e2d0366221d67 Mon Sep 17 00:00:00 2001 From: adrianc Date: Thu, 18 May 2023 18:29:01 +0300 Subject: [PATCH 6/9] Add support for generating Mocks - use mockery to generate mocks: add makefile targets - refactor types and plugin, use interfaces for parsing config file and executing IPAM plugin as prep work for unit tests - generate mocks Signed-off-by: adrianc --- .mockery.yaml | 4 + Makefile | 10 ++ go.mod | 4 + go.sum | 2 + pkg/cni/plugin/mocks/IPAMExecutor.go | 178 +++++++++++++++++++++++++++ pkg/cni/plugin/plugin.go | 61 +++++++-- pkg/cni/types/mocks/ConfLoader.go | 90 ++++++++++++++ pkg/cni/types/types.go | 30 +++-- pkg/cni/types/types_test.go | 10 +- 9 files changed, 365 insertions(+), 24 deletions(-) create mode 100644 .mockery.yaml create mode 100644 pkg/cni/plugin/mocks/IPAMExecutor.go create mode 100644 pkg/cni/types/mocks/ConfLoader.go diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 0000000..8192703 --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,4 @@ +inpackage: False +testonly: False +with-expecter: True +keeptree: True \ No newline at end of file diff --git a/Makefile b/Makefile index 34e606c..f83d56c 100644 --- a/Makefile +++ b/Makefile @@ -111,6 +111,10 @@ KIND_CLUSTER ?= kind kind-load-image: ## Load ipam image to kind cluster kind load docker-image --name $(KIND_CLUSTER) $(IMG) +.PHONY: generate-mocks +generate-mocks: ## generate mock objects + PATH=$(PATH):$(LOCALBIN) go generate ./... + ## Location to install dependencies to LOCALBIN ?= $(PROJECT_DIR)/bin $(LOCALBIN): @@ -120,10 +124,12 @@ $(LOCALBIN): ENVTEST ?= $(LOCALBIN)/setup-envtest GOLANGCILINT ?= $(LOCALBIN)/golangci-lint GCOV2LCOV ?= $(LOCALBIN)/gcov2lcov +MOCKERY ?= $(LOCALBIN)/mockery ## Tool Versions GOLANGCILINT_VERSION ?= v1.52.2 GCOV2LCOV_VERSION ?= v1.0.5 +MOCKERY_VERSION ?= v2.27.1 .PHONY: envtest envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. @@ -140,6 +146,10 @@ gcov2lcov: $(GCOV2LCOV) ## Download gcov2lcov locally if necessary. $(GCOV2LCOV): | $(LOCALBIN) GOBIN=$(LOCALBIN) go install github.com/jandelgado/gcov2lcov@$(GCOV2LCOV_VERSION) +.PHONY: mockery +mockery: $(MOCKERY) ## Download gcov2lcov locally if necessary. +$(MOCKERY): | $(LOCALBIN) + GOBIN=$(LOCALBIN) go install github.com/vektra/mockery/v2@$(MOCKERY_VERSION) .PHONY: clean clean: ## Remove downloaded tools and compiled binaries diff --git a/go.mod b/go.mod index e9676eb..0452a98 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/onsi/gomega v1.24.2 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.8.1 k8s.io/api v0.26.4 k8s.io/apimachinery v0.26.4 k8s.io/client-go v0.26.4 @@ -27,6 +28,7 @@ require ( github.com/coreos/go-iptables v0.6.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logr/zapr v1.2.3 // indirect @@ -51,11 +53,13 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/safchain/ethtool v0.2.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/vishvananda/netlink v1.2.1-beta.2 // indirect github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect go.uber.org/atomic v1.7.0 // indirect diff --git a/go.sum b/go.sum index 84fcfc9..555e0c5 100644 --- a/go.sum +++ b/go.sum @@ -79,6 +79,7 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -294,6 +295,7 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/pkg/cni/plugin/mocks/IPAMExecutor.go b/pkg/cni/plugin/mocks/IPAMExecutor.go new file mode 100644 index 0000000..bc08711 --- /dev/null +++ b/pkg/cni/plugin/mocks/IPAMExecutor.go @@ -0,0 +1,178 @@ +// Code generated by mockery v2.27.1. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + + types "github.com/containernetworking/cni/pkg/types" +) + +// IPAMExecutor is an autogenerated mock type for the IPAMExecutor type +type IPAMExecutor struct { + mock.Mock +} + +type IPAMExecutor_Expecter struct { + mock *mock.Mock +} + +func (_m *IPAMExecutor) EXPECT() *IPAMExecutor_Expecter { + return &IPAMExecutor_Expecter{mock: &_m.Mock} +} + +// ExecAdd provides a mock function with given fields: pluginName, data +func (_m *IPAMExecutor) ExecAdd(pluginName string, data []byte) (types.Result, error) { + ret := _m.Called(pluginName, data) + + var r0 types.Result + var r1 error + if rf, ok := ret.Get(0).(func(string, []byte) (types.Result, error)); ok { + return rf(pluginName, data) + } + if rf, ok := ret.Get(0).(func(string, []byte) types.Result); ok { + r0 = rf(pluginName, data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.Result) + } + } + + if rf, ok := ret.Get(1).(func(string, []byte) error); ok { + r1 = rf(pluginName, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IPAMExecutor_ExecAdd_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExecAdd' +type IPAMExecutor_ExecAdd_Call struct { + *mock.Call +} + +// ExecAdd is a helper method to define mock.On call +// - pluginName string +// - data []byte +func (_e *IPAMExecutor_Expecter) ExecAdd(pluginName interface{}, data interface{}) *IPAMExecutor_ExecAdd_Call { + return &IPAMExecutor_ExecAdd_Call{Call: _e.mock.On("ExecAdd", pluginName, data)} +} + +func (_c *IPAMExecutor_ExecAdd_Call) Run(run func(pluginName string, data []byte)) *IPAMExecutor_ExecAdd_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].([]byte)) + }) + return _c +} + +func (_c *IPAMExecutor_ExecAdd_Call) Return(_a0 types.Result, _a1 error) *IPAMExecutor_ExecAdd_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *IPAMExecutor_ExecAdd_Call) RunAndReturn(run func(string, []byte) (types.Result, error)) *IPAMExecutor_ExecAdd_Call { + _c.Call.Return(run) + return _c +} + +// ExecCheck provides a mock function with given fields: pluginName, data +func (_m *IPAMExecutor) ExecCheck(pluginName string, data []byte) error { + ret := _m.Called(pluginName, data) + + var r0 error + if rf, ok := ret.Get(0).(func(string, []byte) error); ok { + r0 = rf(pluginName, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// IPAMExecutor_ExecCheck_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExecCheck' +type IPAMExecutor_ExecCheck_Call struct { + *mock.Call +} + +// ExecCheck is a helper method to define mock.On call +// - pluginName string +// - data []byte +func (_e *IPAMExecutor_Expecter) ExecCheck(pluginName interface{}, data interface{}) *IPAMExecutor_ExecCheck_Call { + return &IPAMExecutor_ExecCheck_Call{Call: _e.mock.On("ExecCheck", pluginName, data)} +} + +func (_c *IPAMExecutor_ExecCheck_Call) Run(run func(pluginName string, data []byte)) *IPAMExecutor_ExecCheck_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].([]byte)) + }) + return _c +} + +func (_c *IPAMExecutor_ExecCheck_Call) Return(_a0 error) *IPAMExecutor_ExecCheck_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *IPAMExecutor_ExecCheck_Call) RunAndReturn(run func(string, []byte) error) *IPAMExecutor_ExecCheck_Call { + _c.Call.Return(run) + return _c +} + +// ExecDel provides a mock function with given fields: pluginName, data +func (_m *IPAMExecutor) ExecDel(pluginName string, data []byte) error { + ret := _m.Called(pluginName, data) + + var r0 error + if rf, ok := ret.Get(0).(func(string, []byte) error); ok { + r0 = rf(pluginName, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// IPAMExecutor_ExecDel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExecDel' +type IPAMExecutor_ExecDel_Call struct { + *mock.Call +} + +// ExecDel is a helper method to define mock.On call +// - pluginName string +// - data []byte +func (_e *IPAMExecutor_Expecter) ExecDel(pluginName interface{}, data interface{}) *IPAMExecutor_ExecDel_Call { + return &IPAMExecutor_ExecDel_Call{Call: _e.mock.On("ExecDel", pluginName, data)} +} + +func (_c *IPAMExecutor_ExecDel_Call) Run(run func(pluginName string, data []byte)) *IPAMExecutor_ExecDel_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].([]byte)) + }) + return _c +} + +func (_c *IPAMExecutor_ExecDel_Call) Return(_a0 error) *IPAMExecutor_ExecDel_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *IPAMExecutor_ExecDel_Call) RunAndReturn(run func(string, []byte) error) *IPAMExecutor_ExecDel_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewIPAMExecutor interface { + mock.TestingT + Cleanup(func()) +} + +// NewIPAMExecutor creates a new instance of IPAMExecutor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewIPAMExecutor(t mockConstructorTestingTNewIPAMExecutor) *IPAMExecutor { + mock := &IPAMExecutor{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/cni/plugin/plugin.go b/pkg/cni/plugin/plugin.go index e1037ab..374727c 100644 --- a/pkg/cni/plugin/plugin.go +++ b/pkg/cni/plugin/plugin.go @@ -38,20 +38,59 @@ const ( delegateIPAMPluginName = "host-local" ) +// IPAMExecutor is an interface that executes IPAM CNI Plugin +// +//go:generate mockery --name IPAMExecutor +type IPAMExecutor interface { + // ExecAdd executes IPAM plugin named pluginName ADD call + ExecAdd(pluginName string, data []byte) (cnitypes.Result, error) + // ExecDel executes IPAM plugin named pluginName DEL call + ExecDel(pluginName string, data []byte) error + // ExecCheck executes IPAM plugin named pluginName CHECK call + ExecCheck(pluginName string, data []byte) error +} + +// NewIPAMExecutor creates a new instance that implements IPAMExecutor +func NewIPAMExecutor() IPAMExecutor { + return &cniIpamExecutor{} +} + +// cniIpamExecutor implements IPAMExecutor using cni plugins ipam package +type cniIpamExecutor struct{} + +// ExecAdd implements IPAMExecutor interface +func (ie *cniIpamExecutor) ExecAdd(pluginName string, data []byte) (cnitypes.Result, error) { + return ipam.ExecAdd(pluginName, data) +} + +// ExecDel implements IPAMExecutor interface +func (ie *cniIpamExecutor) ExecDel(pluginName string, data []byte) error { + return ipam.ExecDel(pluginName, data) +} + +// ExecCheck implements IPAMExecutor interface +func (ie *cniIpamExecutor) ExecCheck(pluginName string, data []byte) error { + return ipam.ExecCheck(pluginName, data) +} + func NewPlugin() *Plugin { return &Plugin{ - Name: CNIPluginName, - Version: version.GetVersionString(), + Name: CNIPluginName, + Version: version.GetVersionString(), + ConfLoader: types.NewConfLoader(), + IPAMExecutor: NewIPAMExecutor(), } } type Plugin struct { - Name string - Version string + Name string + Version string + ConfLoader types.ConfLoader + IPAMExecutor IPAMExecutor } func (p *Plugin) CmdAdd(args *skel.CmdArgs) error { - conf, err := types.LoadConf(args.StdinData) + conf, err := p.ConfLoader.LoadConf(args.StdinData) if err != nil { return fmt.Errorf("failed to load config. %w", err) } @@ -76,7 +115,7 @@ func (p *Plugin) CmdAdd(args *skel.CmdArgs) error { if err != nil { return err } - res, err := ipam.ExecAdd(delegateIPAMPluginName, data) + res, err := p.IPAMExecutor.ExecAdd(delegateIPAMPluginName, data) if err != nil { return fmt.Errorf("failed to exec ADD host-local CNI plugin. %w", err) } @@ -85,7 +124,7 @@ func (p *Plugin) CmdAdd(args *skel.CmdArgs) error { } func (p *Plugin) CmdDel(args *skel.CmdArgs) error { - conf, err := types.LoadConf(args.StdinData) + conf, err := p.ConfLoader.LoadConf(args.StdinData) if err != nil { return fmt.Errorf("failed to load config. %w", err) } @@ -110,7 +149,7 @@ func (p *Plugin) CmdDel(args *skel.CmdArgs) error { if err != nil { return err } - err = ipam.ExecDel(delegateIPAMPluginName, data) + err = p.IPAMExecutor.ExecDel(delegateIPAMPluginName, data) if err != nil { return fmt.Errorf("failed to exec DEL host-local CNI plugin. %w", err) } @@ -119,7 +158,7 @@ func (p *Plugin) CmdDel(args *skel.CmdArgs) error { } func (p *Plugin) CmdCheck(args *skel.CmdArgs) error { - conf, err := types.LoadConf(args.StdinData) + conf, err := p.ConfLoader.LoadConf(args.StdinData) if err != nil { return fmt.Errorf("failed to load config. %w", err) } @@ -144,7 +183,7 @@ func (p *Plugin) CmdCheck(args *skel.CmdArgs) error { if err != nil { return err } - err = ipam.ExecCheck(delegateIPAMPluginName, data) + err = p.IPAMExecutor.ExecCheck(delegateIPAMPluginName, data) if err != nil { return fmt.Errorf("failed to exec CHECK host-local CNI plugin. %w", err) } @@ -169,7 +208,7 @@ func logCall(cmd string, args *skel.CmdArgs, conf *types.IPAMConf) { log.Debugf("CMD %s: Parsed IPAM conf: %+v", cmd, conf) } -func getPoolbyName(kclient *kubernetes.Clientset, nodeName, poolName string) (*pool.IPPool, error) { +func getPoolbyName(kclient kubernetes.Interface, nodeName, poolName string) (*pool.IPPool, error) { // get pool info from node node, err := kclient.CoreV1().Nodes().Get(context.TODO(), nodeName, metav1.GetOptions{}) if err != nil { diff --git a/pkg/cni/types/mocks/ConfLoader.go b/pkg/cni/types/mocks/ConfLoader.go new file mode 100644 index 0000000..4873481 --- /dev/null +++ b/pkg/cni/types/mocks/ConfLoader.go @@ -0,0 +1,90 @@ +// Code generated by mockery v2.27.1. DO NOT EDIT. + +package mocks + +import ( + types "github.com/Mellanox/nvidia-k8s-ipam/pkg/cni/types" + mock "github.com/stretchr/testify/mock" +) + +// ConfLoader is an autogenerated mock type for the ConfLoader type +type ConfLoader struct { + mock.Mock +} + +type ConfLoader_Expecter struct { + mock *mock.Mock +} + +func (_m *ConfLoader) EXPECT() *ConfLoader_Expecter { + return &ConfLoader_Expecter{mock: &_m.Mock} +} + +// LoadConf provides a mock function with given fields: bytes +func (_m *ConfLoader) LoadConf(bytes []byte) (*types.NetConf, error) { + ret := _m.Called(bytes) + + var r0 *types.NetConf + var r1 error + if rf, ok := ret.Get(0).(func([]byte) (*types.NetConf, error)); ok { + return rf(bytes) + } + if rf, ok := ret.Get(0).(func([]byte) *types.NetConf); ok { + r0 = rf(bytes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.NetConf) + } + } + + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(bytes) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConfLoader_LoadConf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadConf' +type ConfLoader_LoadConf_Call struct { + *mock.Call +} + +// LoadConf is a helper method to define mock.On call +// - bytes []byte +func (_e *ConfLoader_Expecter) LoadConf(bytes interface{}) *ConfLoader_LoadConf_Call { + return &ConfLoader_LoadConf_Call{Call: _e.mock.On("LoadConf", bytes)} +} + +func (_c *ConfLoader_LoadConf_Call) Run(run func(bytes []byte)) *ConfLoader_LoadConf_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *ConfLoader_LoadConf_Call) Return(_a0 *types.NetConf, _a1 error) *ConfLoader_LoadConf_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ConfLoader_LoadConf_Call) RunAndReturn(run func([]byte) (*types.NetConf, error)) *ConfLoader_LoadConf_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewConfLoader interface { + mock.TestingT + Cleanup(func()) +} + +// NewConfLoader creates a new instance of ConfLoader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewConfLoader(t mockConstructorTestingTNewConfLoader) *ConfLoader { + mock := &ConfLoader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/cni/types/types.go b/pkg/cni/types/types.go index 140186a..da9fa8b 100644 --- a/pkg/cni/types/types.go +++ b/pkg/cni/types/types.go @@ -44,6 +44,14 @@ const ( ConfFileName = "nv-ipam.conf" ) +// ConfLoader loads CNI configuration +// +//go:generate mockery --name ConfLoader +type ConfLoader interface { + // LoadConf loads CNI configuration from Json data + LoadConf(bytes []byte) (*NetConf, error) +} + // IPAMConf is the configuration supported by our CNI plugin type IPAMConf struct { types.IPAM @@ -58,7 +66,7 @@ type IPAMConf struct { // internal configuration NodeName string - K8sClient *kubernetes.Clientset + K8sClient kubernetes.Interface } // NetConf is CNI network config @@ -68,8 +76,14 @@ type NetConf struct { IPAM *IPAMConf `json:"ipam"` } +type confLoader struct{} + +func NewConfLoader() ConfLoader { + return &confLoader{} +} + // LoadConf Loads NetConf from json string provided as []byte -func LoadConf(bytes []byte) (*NetConf, error) { +func (cl *confLoader) LoadConf(bytes []byte) (*NetConf, error) { n := &NetConf{} if err := json.Unmarshal(bytes, &n); err != nil { @@ -86,9 +100,9 @@ func LoadConf(bytes []byte) (*NetConf, error) { // overlay config from conf file if exists. confFilePath := filepath.Join(n.IPAM.ConfDir, ConfFileName) - fileConf, err := LoadFromConfFile(confFilePath) + fileConf, err := cl.loadFromConfFile(confFilePath) if err == nil { - overlayConf(fileConf, n.IPAM) + cl.overlayConf(fileConf, n.IPAM) } else if !os.IsNotExist(err) { return nil, fmt.Errorf("failed to read/parse config file(%s). %w", confFilePath, err) } @@ -103,7 +117,7 @@ func LoadConf(bytes []byte) (*NetConf, error) { LogFile: DefaultLogFile, LogLevel: "info", } - overlayConf(defaultConf, n.IPAM) + cl.overlayConf(defaultConf, n.IPAM) // get Node name p := filepath.Join(n.IPAM.ConfDir, K8sNodeNameFile) @@ -125,8 +139,8 @@ func LoadConf(bytes []byte) (*NetConf, error) { return n, nil } -// LoadFromConfFile returns *IPAMConf with values from config file located in filePath. -func LoadFromConfFile(filePath string) (*IPAMConf, error) { +// loadFromConfFile returns *IPAMConf with values from config file located in filePath. +func (cl *confLoader) loadFromConfFile(filePath string) (*IPAMConf, error) { data, err := os.ReadFile(filePath) if err != nil { return nil, err @@ -143,7 +157,7 @@ func LoadFromConfFile(filePath string) (*IPAMConf, error) { // overlayConf overlays IPAMConf "from" onto "to" // fields in to are overlayed if they are empty in "to". -func overlayConf(from, to *IPAMConf) { +func (cl *confLoader) overlayConf(from, to *IPAMConf) { if to.ConfDir == "" { to.ConfDir = from.ConfDir } diff --git a/pkg/cni/types/types_test.go b/pkg/cni/types/types_test.go index 4230114..d7581c7 100644 --- a/pkg/cni/types/types_test.go +++ b/pkg/cni/types/types_test.go @@ -53,7 +53,7 @@ var _ = Describe("Types Tests", func() { // Load config testConf := fmt.Sprintf(`{"name": "my-net", "ipam": {"confDir": %q}}`, testConfDir) - conf, err := types.LoadConf([]byte(testConf)) + conf, err := types.NewConfLoader().LoadConf([]byte(testConf)) // Validate Expect(err).ToNot(HaveOccurred()) @@ -81,7 +81,7 @@ var _ = Describe("Types Tests", func() { // Load config testConf := fmt.Sprintf(`{"name": "my-net", "ipam": {"confDir": %q}}`, testConfDir) - conf, err := types.LoadConf([]byte(testConf)) + conf, err := types.NewConfLoader().LoadConf([]byte(testConf)) // Validate Expect(err).ToNot(HaveOccurred()) @@ -106,7 +106,7 @@ var _ = Describe("Types Tests", func() { // Load config testConf := fmt.Sprintf(`{"name": "my-net", "ipam": {"confDir": %q, "poolName": "my-pool", "logLevel": "error"}}`, testConfDir) - conf, err := types.LoadConf([]byte(testConf)) + conf, err := types.NewConfLoader().LoadConf([]byte(testConf)) // Validate Expect(err).ToNot(HaveOccurred()) @@ -119,12 +119,12 @@ var _ = Describe("Types Tests", func() { }) It("Fails if config is invalid json", func() { - _, err := types.LoadConf([]byte("{garbage%^&*")) + _, err := types.NewConfLoader().LoadConf([]byte("{garbage%^&*")) Expect(err).To(HaveOccurred()) }) It("Fails if config does not contain ipam key", func() { - _, err := types.LoadConf([]byte(`{"name": "my-net", "type": "sriov"}`)) + _, err := types.NewConfLoader().LoadConf([]byte(`{"name": "my-net", "type": "sriov"}`)) Expect(err).To(HaveOccurred()) }) }) From 3ff5af72c9c69ab3ccb1d9d0a841ccfa473d87fa Mon Sep 17 00:00:00 2001 From: adrianc Date: Thu, 18 May 2023 18:33:12 +0300 Subject: [PATCH 7/9] Add tests for plugin package Signed-off-by: adrianc --- pkg/cni/plugin/plugin_suite_test.go | 26 +++++ pkg/cni/plugin/plugin_test.go | 149 ++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 pkg/cni/plugin/plugin_suite_test.go create mode 100644 pkg/cni/plugin/plugin_test.go diff --git a/pkg/cni/plugin/plugin_suite_test.go b/pkg/cni/plugin/plugin_suite_test.go new file mode 100644 index 0000000..27ce8d0 --- /dev/null +++ b/pkg/cni/plugin/plugin_suite_test.go @@ -0,0 +1,26 @@ +/* + Copyright 2023, NVIDIA CORPORATION & AFFILIATES + 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 plugin_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestPlugin(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Plugin Suite") +} diff --git a/pkg/cni/plugin/plugin_test.go b/pkg/cni/plugin/plugin_test.go new file mode 100644 index 0000000..1bc3776 --- /dev/null +++ b/pkg/cni/plugin/plugin_test.go @@ -0,0 +1,149 @@ +/* + Copyright 2023, NVIDIA CORPORATION & AFFILIATES + 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 plugin_test + +import ( + "encoding/json" + "path" + + "github.com/containernetworking/cni/pkg/skel" + cnitypes "github.com/containernetworking/cni/pkg/types" + cnitypes100 "github.com/containernetworking/cni/pkg/types/100" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + + "github.com/Mellanox/nvidia-k8s-ipam/pkg/cni/plugin" + "github.com/Mellanox/nvidia-k8s-ipam/pkg/cni/types" + "github.com/Mellanox/nvidia-k8s-ipam/pkg/pool" + "github.com/Mellanox/nvidia-k8s-ipam/pkg/version" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" + + pluginMocks "github.com/Mellanox/nvidia-k8s-ipam/pkg/cni/plugin/mocks" + typesMocks "github.com/Mellanox/nvidia-k8s-ipam/pkg/cni/types/mocks" +) + +var _ = Describe("plugin tests", func() { + var ( + tmpDir string + p plugin.Plugin + fakeClient *fake.Clientset + testNode *v1.Node + mockExecutor *pluginMocks.IPAMExecutor + mockConfLoader *typesMocks.ConfLoader + testConf *types.NetConf + args *skel.CmdArgs + ) + + BeforeEach(func() { + tmpDir = GinkgoT().TempDir() + + testNode = &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + }, + } + nodeAnnot := map[string]string{ + pool.IPBlocksAnnotation: `{"my-pool": + {"subnet": "192.168.0.0/16", "startIP": "192.168.0.2", + "endIP": "192.168.0.254", "gateway": "192.168.0.1"}}`, + } + testNode.SetAnnotations(nodeAnnot) + + fakeClient = fake.NewSimpleClientset(testNode) + mockExecutor = pluginMocks.NewIPAMExecutor(GinkgoT()) + mockConfLoader = typesMocks.NewConfLoader(GinkgoT()) + + p = plugin.Plugin{ + Name: plugin.CNIPluginName, + Version: version.GetVersionString(), + ConfLoader: mockConfLoader, + IPAMExecutor: mockExecutor, + } + + testConf = &types.NetConf{ + Name: "my-net", + CNIVersion: "0.4.0", + IPAM: &types.IPAMConf{ + IPAM: cnitypes.IPAM{ + Type: "nv-ipam", + }, + PoolName: "my-pool", + DataDir: "/foo/bar", + LogFile: path.Join(tmpDir, "nv-ipam.log"), + LogLevel: "debug", + NodeName: "test-node", + K8sClient: fakeClient, + }, + } + + args = &skel.CmdArgs{ + ContainerID: "1234", + Netns: "/proc/19783/ns", + IfName: "net1", + StdinData: []byte("doesnt-matter"), + } + }) + + Context("CmdAdd()", func() { + It("executes successfully", func() { + mockConfLoader.On("LoadConf", args.StdinData).Return(testConf, nil) + mockExecutor.On("ExecAdd", "host-local", mock.Anything).Return(&cnitypes100.Result{}, func(_ string, data []byte) error { + // fail if we cannot unmarshal data to host-local config + hostLocalConf := &types.HostLocalNetConf{} + return json.Unmarshal(data, hostLocalConf) + }) + + err := p.CmdAdd(args) + Expect(err).ToNot(HaveOccurred()) + mockConfLoader.AssertExpectations(GinkgoT()) + mockExecutor.AssertExpectations(GinkgoT()) + }) + }) + + Context("CmdDel()", func() { + It("executes successfully", func() { + mockConfLoader.On("LoadConf", args.StdinData).Return(testConf, nil) + mockExecutor.On("ExecDel", "host-local", mock.Anything).Return(func(_ string, data []byte) error { + // fail if we cannot unmarshal data to host-local config + hostLocalConf := &types.HostLocalNetConf{} + return json.Unmarshal(data, hostLocalConf) + }) + + err := p.CmdDel(args) + Expect(err).ToNot(HaveOccurred()) + mockConfLoader.AssertExpectations(GinkgoT()) + mockExecutor.AssertExpectations(GinkgoT()) + }) + }) + + Context("CmdCheck()", func() { + It("executes successfully", func() { + mockConfLoader.On("LoadConf", args.StdinData).Return(testConf, nil) + mockExecutor.On("ExecCheck", "host-local", mock.Anything).Return(func(_ string, data []byte) error { + // fail if we cannot unmarshal data to host-local config + hostLocalConf := &types.HostLocalNetConf{} + return json.Unmarshal(data, hostLocalConf) + }) + + err := p.CmdCheck(args) + Expect(err).ToNot(HaveOccurred()) + mockConfLoader.AssertExpectations(GinkgoT()) + mockExecutor.AssertExpectations(GinkgoT()) + }) + }) +}) From 8f6cbf4cfbbfb5d3c959f41ae2483d6a3ae008b4 Mon Sep 17 00:00:00 2001 From: adrianc Date: Sun, 21 May 2023 18:37:36 +0300 Subject: [PATCH 8/9] Skip Mock packages while running tests - generate list of packages to test - run test only on those packages - remove -coverpkg flag Signed-off-by: adrianc --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f83d56c..1e7991e 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,8 @@ GO_BUILD_OPTS ?= CGO_ENABLED=0 GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) GO_LDFLAGS = $(VERSION_LDFLAGS) +PKGS = $(or $(PKG),$(shell cd $(PROJECT_DIR) && go list ./... | grep -v "^nvidia-k8s-ipam/vendor/" | grep -v ".*/mocks")) + .PHONY: all all: build @@ -69,7 +71,7 @@ LCOV_PATH = $(PROJECT_DIR)/lcov.info .PHONY: unit-test unit-test: envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverpkg=./... -covermode=$(COVERAGE_MODE) -coverprofile=$(COVER_PROFILE) + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -covermode=$(COVERAGE_MODE) -coverprofile=$(COVER_PROFILE) $(PKGS) .PHONY: test test: lint unit-test From 33218b13decb5a29d833fc08683b8a4a0158667c Mon Sep 17 00:00:00 2001 From: adrianc Date: Mon, 22 May 2023 17:00:51 +0300 Subject: [PATCH 9/9] Address review comments - newline in .mockery.yaml file - fix typo in Makefile - update test description pool test - use Ginkgo TempDir() in cmdtutils test - bump testify version to v1.8.3 Signed-off-by: adrianc --- .mockery.yaml | 2 +- Makefile | 2 +- go.mod | 2 +- go.sum | 3 ++- pkg/cmdutils/utils_test.go | 9 +-------- pkg/pool/pool_test.go | 2 +- 6 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.mockery.yaml b/.mockery.yaml index 8192703..f79e317 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -1,4 +1,4 @@ inpackage: False testonly: False with-expecter: True -keeptree: True \ No newline at end of file +keeptree: True diff --git a/Makefile b/Makefile index 1e7991e..41147f8 100644 --- a/Makefile +++ b/Makefile @@ -149,7 +149,7 @@ $(GCOV2LCOV): | $(LOCALBIN) GOBIN=$(LOCALBIN) go install github.com/jandelgado/gcov2lcov@$(GCOV2LCOV_VERSION) .PHONY: mockery -mockery: $(MOCKERY) ## Download gcov2lcov locally if necessary. +mockery: $(MOCKERY) ## Download mockery locally if necessary. $(MOCKERY): | $(LOCALBIN) GOBIN=$(LOCALBIN) go install github.com/vektra/mockery/v2@$(MOCKERY_VERSION) diff --git a/go.mod b/go.mod index 0452a98..99a895b 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/onsi/gomega v1.24.2 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.3 k8s.io/api v0.26.4 k8s.io/apimachinery v0.26.4 k8s.io/client-go v0.26.4 diff --git a/go.sum b/go.sum index 555e0c5..6c48b7f 100644 --- a/go.sum +++ b/go.sum @@ -304,8 +304,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= diff --git a/pkg/cmdutils/utils_test.go b/pkg/cmdutils/utils_test.go index dc92120..7309f0b 100644 --- a/pkg/cmdutils/utils_test.go +++ b/pkg/cmdutils/utils_test.go @@ -29,14 +29,7 @@ var _ = Describe("cmdutils testing", func() { ) BeforeEach(func() { - var err error - tmpDir, err = os.MkdirTemp("", "nv_ipam_thin_entrypoint_tmp") - Expect(err).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - err := os.RemoveAll(tmpDir) - Expect(err).NotTo(HaveOccurred()) + tmpDir = GinkgoT().TempDir() }) It("Run CopyFileAtomic()", func() { diff --git a/pkg/pool/pool_test.go b/pkg/pool/pool_test.go index def1f38..33d4527 100644 --- a/pkg/pool/pool_test.go +++ b/pkg/pool/pool_test.go @@ -51,7 +51,7 @@ var _ = Describe("pool tests", func() { Expect(err).To(HaveOccurred()) }) - It("Fails to create Manager if node has empty ip-pool annotation", func() { + It("Fails to create Manager if node has empty/invalid ip-pool annotation", func() { n := v1.Node{} emptyAnnot := map[string]string{ pool.IPBlocksAnnotation: "",