diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 00000000..43ddf646 --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,23 @@ +name: unit-tests +on: + push: + branches: + - master + pull_request: + +permissions: + contents: read + +jobs: + unit-tests: + name: unit-tests + runs-on: hugepage-runner + steps: + - name: Set up Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version: 1.20.1 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: make unit-test + run: make unit-test + \ No newline at end of file diff --git a/Makefile b/Makefile index 74dac8be..3986fe3a 100644 --- a/Makefile +++ b/Makefile @@ -46,3 +46,12 @@ undeploy: testpmd: @$(IMAGE_BUILDER) build -t $(IMAGE_REGISTRY)testpmd:latest -f ./docker/testpmd/Dockerfile ./docker/testpmd/ @$(IMAGE_BUILDER) push $(IMAGE_REGISTRY)testpmd:latest + +unit-test: + @$(IMAGE_BUILDER) rm -f userspacecni-unittest + @$(IMAGE_BUILDER) build . -f ./docker/userspacecni/Dockerfile.unittest -t userspacecni-unittest:latest + @$(IMAGE_BUILDER) run -m 100g --privileged -v ./examples/sample-vpp-host-config/startup.conf:/etc/vpp/startup.conf --name userspacecni-unittest -itd userspacecni-unittest:latest + @$(IMAGE_BUILDER) cp userspacecni-unittest:/root/userspace-cni-network-plugin/cnivpp ./ + @$(IMAGE_BUILDER) exec userspacecni-unittest bash -c "go test ./cnivpp/ -v -cover" + @$(IMAGE_BUILDER) rm -f userspacecni-unittest + \ No newline at end of file diff --git a/cnivpp/cnivpp.go b/cnivpp/cnivpp.go index f28964c4..15ac0980 100644 --- a/cnivpp/cnivpp.go +++ b/cnivpp/cnivpp.go @@ -149,6 +149,8 @@ func (cniVpp CniVpp) AddOnHost(conf *types.NetConf, err = errors.New("ERROR: Unknown HostConf.NetType:" + conf.HostConf.NetType) logging.Debugf("AddOnHost(vpp): %v", err) return err + } else { + return fmt.Errorf("ERROR: NetType must be provided") } // @@ -321,7 +323,7 @@ func delLocalDeviceMemif(vppCh vppinfra.ConnectionData, conf *types.NetConf, arg err = vppmemif.DeleteMemifInterface(vppCh.Ch, interface_types.InterfaceIndex(data.InterfaceSwIfIndex)) if err != nil { logging.Debugf("delLocalDeviceMemif(vpp): Error deleting memif inteface: %v", err) - return + return logging.Errorf("delLocalDeviceMemif(vpp): Error deleting memif inteface: %v", err) } else { if dbgInterface { logging.Verbosef("INTERFACE %d deleted\n", data.InterfaceSwIfIndex) diff --git a/cnivpp/cnivpp_test.go b/cnivpp/cnivpp_test.go new file mode 100644 index 00000000..a54af7f5 --- /dev/null +++ b/cnivpp/cnivpp_test.go @@ -0,0 +1,318 @@ +package cnivpp + +import ( + "errors" + "fmt" + "os" + "path" + "path/filepath" + "testing" + + "github.com/containernetworking/cni/pkg/skel" + current "github.com/containernetworking/cni/pkg/types/100" + "github.com/google/uuid" + "github.com/intel/userspace-cni-network-plugin/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apitypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/fake" +) + +func GetTestPod(sharedDir string) *v1.Pod { + id, _ := uuid.NewUUID() + pod := &v1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + UID: apitypes.UID(id.String()), + Name: fmt.Sprintf("pod-%v", id[:8]), + Namespace: fmt.Sprintf("namespace-%v", id[:8]), + }, + } + if sharedDir != "" { + pod.Spec.Volumes = append(pod.Spec.Volumes, + v1.Volume{ + Name: "shared-dir", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: sharedDir, + }, + }, + }) + pod.Spec.Containers = append(pod.Spec.Containers, + v1.Container{ + Name: "container", + VolumeMounts: []v1.VolumeMount{{Name: "shared-dir", MountPath: sharedDir}}, + }) + } + return pod +} + +func GetTestArgs() *skel.CmdArgs { + id, _ := uuid.NewUUID() + return &skel.CmdArgs{ + ContainerID: id.String(), + IfName: fmt.Sprintf("eth%v", int(id[7])), + StdinData: []byte("{}"), + } +} + +func TestGetMemifSocketfileName(t *testing.T) { + t.Run("get Memif Socker File Name", func(t *testing.T) { + args := GetTestArgs() + + sharedDir, dirErr := os.MkdirTemp("/tmp", "test-cniovs-") + require.NoError(t, dirErr, "Can't create temporary directory") + defer os.RemoveAll(sharedDir) + + memifSockFileName := getMemifSocketfileName(&types.NetConf{}, sharedDir, args.ContainerID, args.IfName) + assert.Equal(t, filepath.Join(sharedDir, fmt.Sprintf("memif-%s-%s.sock", args.ContainerID[:12], args.IfName)), memifSockFileName, "Unexpected error") + + conf := &types.NetConf{} + conf.HostConf.MemifConf.Socketfile = "socketFile.sock" + + memifSockFileName = getMemifSocketfileName(conf, sharedDir, args.ContainerID, args.IfName) + assert.Equal(t, filepath.Join(sharedDir, conf.HostConf.MemifConf.Socketfile), memifSockFileName, "Unexpected error") + }) +} + +func TestAddOnContainer(t *testing.T) { + t.Run("save container data to file", func(t *testing.T) { + var result *current.Result + args := GetTestArgs() + cniVpp := CniVpp{} + + sharedDir, dirErr := os.MkdirTemp("/tmp", "test-cniovs-") + require.NoError(t, dirErr, "Can't create temporary directory") + defer os.RemoveAll(sharedDir) + + pod := GetTestPod(sharedDir) + resPod, resErr := cniVpp.AddOnContainer(&types.NetConf{}, args, nil, sharedDir, pod, result) + assert.NoError(t, resErr, "Unexpected error") + assert.Equal(t, pod, resPod, "Unexpected change of pod data") + fileName := fmt.Sprintf("configData-%s-%s.json", args.ContainerID[:12], args.IfName) + assert.FileExists(t, path.Join(sharedDir, fileName), "Container data were not saved to file") + }) +} + +func TestDelOnContainer(t *testing.T) { + t.Run("remove container configuration", func(t *testing.T) { + args := GetTestArgs() + cniVpp := CniVpp{} + + sharedDir, dirErr := os.MkdirTemp("/tmp", "test-cniovs-") + require.NoError(t, dirErr, "Can't create temporary directory") + // just in case DelFromContainer fails + defer os.RemoveAll(sharedDir) + + err := cniVpp.DelFromContainer(&types.NetConf{}, args, sharedDir, nil) + assert.NoError(t, err, "Unexpected error") + assert.NoDirExists(t, sharedDir, "Container data were not removed") + }) +} + +func TestAddOnHost(t *testing.T) { + cniVpp := CniVpp{} + + testCases := []struct { + name string + netConf *types.NetConf + fakeErr error + expErr error + }{ + { + name: "Happy path", + netConf: &types.NetConf{ + HostConf: types.UserSpaceConf{Engine: "vpp", IfType: "memif", NetType: "interface", + VhostConf: types.VhostConf{Mode: "client"}, + MemifConf: types.MemifConf{ + Role: "master", // Role of memif: master|slave + Mode: "ip", // Mode of memif: ip|ethernet|inject-punt + }}}, + expErr: nil, + }, + { + name: "Invalid MEMIF Role", + netConf: &types.NetConf{ + HostConf: types.UserSpaceConf{Engine: "vpp", IfType: "memif", NetType: "interface", + VhostConf: types.VhostConf{Mode: "client"}, + MemifConf: types.MemifConf{ + Role: "", // Role of memif: master|slave + Mode: "ip", // Mode of memif: ip|ethernet|inject-punt + }}}, + expErr: errors.New("ERROR: Invalid MEMIF Role"), + }, + { + name: "Unknown IfType", + netConf: &types.NetConf{ + HostConf: types.UserSpaceConf{Engine: "vpp", IfType: "", NetType: "interface", + VhostConf: types.VhostConf{Mode: "client"}, + MemifConf: types.MemifConf{ + Role: "", // Role of memif: master|slave + Mode: "ip", // Mode of memif: ip|ethernet|inject-punt + }}}, + expErr: errors.New("Unknown HostConf.IfType"), + }, + { + name: "Unknown NetType", + netConf: &types.NetConf{ + HostConf: types.UserSpaceConf{Engine: "vpp", IfType: "memif", NetType: "UnkownNetType", + VhostConf: types.VhostConf{Mode: "client"}, + MemifConf: types.MemifConf{ + Role: "master", // Role of memif: master|slave + Mode: "ip", // Mode of memif: ip|ethernet|inject-punt + }}}, + expErr: errors.New("Unknown HostConf.NetType"), + }, + { + name: "Bridge already exists", + netConf: &types.NetConf{ + HostConf: types.UserSpaceConf{Engine: "vpp", IfType: "memif", NetType: "bridge", + VhostConf: types.VhostConf{Mode: "client"}, + MemifConf: types.MemifConf{ + Role: "master", // Role of memif: master|slave + Mode: "ip", // Mode of memif: ip|ethernet|inject-punt + }}}, + expErr: errors.New("Bridge domain already exists"), + }, + { + name: "Create 12345 Bridge", + netConf: &types.NetConf{ + HostConf: types.UserSpaceConf{Engine: "vpp", IfType: "memif", NetType: "bridge", + BridgeConf: types.BridgeConf{ + BridgeName: "12345", + BridgeId: 12345, + }, + VhostConf: types.VhostConf{Mode: "client"}, + MemifConf: types.MemifConf{ + Role: "master", // Role of memif: master|slave + Mode: "ip", // Mode of memif: ip|ethernet|inject-punt + }}}, + expErr: nil, + }, + { + name: "NetType set to empty", + netConf: &types.NetConf{ + HostConf: types.UserSpaceConf{Engine: "vpp", IfType: "memif", NetType: "", + VhostConf: types.VhostConf{Mode: "client"}, + MemifConf: types.MemifConf{ + Role: "master", // Role of memif: master|slave + Mode: "ip", // Mode of memif: ip|ethernet|inject-punt + }}}, + expErr: errors.New("ERROR: NetType must be provided"), + }, + { + name: "interface slave and ip mode", + netConf: &types.NetConf{ + HostConf: types.UserSpaceConf{Engine: "vpp", IfType: "memif", NetType: "interface", + VhostConf: types.VhostConf{Mode: "client"}, + MemifConf: types.MemifConf{ + Role: "slave", // Role of memif: master|slave + Mode: "ip", // Mode of memif: ip|ethernet|inject-punt + }}}, + expErr: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var result *current.Result + args := GetTestArgs() + + sharedDir, dirErr := os.MkdirTemp("/tmp", "test-cnivpp-") + require.NoError(t, dirErr, "Can't create temporary directory") + defer os.RemoveAll(sharedDir) + + pod := GetTestPod(sharedDir) + kubeClient := fake.NewSimpleClientset(pod) + + err := cniVpp.AddOnHost(tc.netConf, args, kubeClient, sharedDir, result) + if tc.expErr == nil { + assert.Equal(t, tc.expErr, err, "Unexpected result") + // on success there shall be saved ovs data + var data VppSavedData + assert.NoError(t, LoadVppConfig(tc.netConf, args, &data)) + assert.NotEmpty(t, data.MemifSocketId) + } else { + require.Error(t, err, "Unexpected result") + assert.Contains(t, err.Error(), tc.expErr.Error(), "Unexpected result") + } + }) + } +} + +func TestDelFromHost(t *testing.T) { + cniVpp := CniVpp{} + + testCases := []struct { + name string + netConf *types.NetConf + savedData string + fakeErr error + expErr error + }{ + { + name: "Happy path", + netConf: &types.NetConf{ + HostConf: types.UserSpaceConf{Engine: "vpp", IfType: "memif", NetType: "interface", + VhostConf: types.VhostConf{Mode: "client"}, + MemifConf: types.MemifConf{ + Role: "master", // Role of memif: master|slave + Mode: "ip", // Mode of memif: ip|ethernet|inject-punt + }}}, + expErr: nil, + }, + { + name: "Unknown HostConf Type", + netConf: &types.NetConf{ + HostConf: types.UserSpaceConf{Engine: "vpp", IfType: "Unknown", NetType: "interface", + VhostConf: types.VhostConf{Mode: "client"}, + MemifConf: types.MemifConf{ + Role: "master", // Role of memif: master|slave + Mode: "ip", // Mode of memif: ip|ethernet|inject-punt + }}}, + expErr: fmt.Errorf("ERROR: Unknown HostConf.Type"), + }, + { + name: "Delete Bridge with IfType set to vhostUser", + netConf: &types.NetConf{ + HostConf: types.UserSpaceConf{Engine: "vpp", IfType: "vhostuser", NetType: "bridge", + VhostConf: types.VhostConf{Mode: "client"}, + BridgeConf: types.BridgeConf{ + BridgeName: "12345", + BridgeId: 12345, + }, + MemifConf: types.MemifConf{ + Role: "master", // Role of memif: master|slave + Mode: "ip", // Mode of memif: ip|ethernet|inject-punt + }}}, + expErr: fmt.Errorf("GOOD: Found HostConf.Type:vhostuser"), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + args := GetTestArgs() + sharedDir, dirErr := os.MkdirTemp("/tmp", "test-cnivpp-") + require.NoError(t, dirErr, "Can't create temporary directory") + defer os.RemoveAll(sharedDir) + + var result *current.Result + + pod := GetTestPod(sharedDir) + kubeClient := fake.NewSimpleClientset(pod) + + _ = cniVpp.AddOnHost(tc.netConf, args, kubeClient, sharedDir, result) + + err := cniVpp.DelFromHost(tc.netConf, args, sharedDir) + if tc.expErr == nil { + assert.Equal(t, tc.expErr, err, "Unexpected result") + } else { + require.Error(t, err, "Unexpected result") + assert.Contains(t, err.Error(), tc.expErr.Error(), "Unexpected result") + } + }) + } +} diff --git a/docker/userspacecni/Dockerfile.unittest b/docker/userspacecni/Dockerfile.unittest new file mode 100644 index 00000000..6c8e4938 --- /dev/null +++ b/docker/userspacecni/Dockerfile.unittest @@ -0,0 +1,14 @@ +FROM ligato/vpp-base:23.06@sha256:f68272b0aebe106673c7fffe94b6e6ccd06ecc9afd123ebcbbdc22b350bd2774 as builder +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +COPY . /root/userspace-cni-network-plugin +WORKDIR /root/userspace-cni-network-plugin +RUN apt-get update -y \ + && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y binutils bash wget make git \ + && wget -qO- https://golang.org/dl/go1.20.1.linux-amd64.tar.gz | tar -C /usr/local -xz \ + && rm -rf /var/lib/apt/lists/* +ENV PATH="${PATH}:/usr/local/go/bin" +RUN go mod download \ + && go get go.fd.io/govpp/binapigen/vppapi@v0.7.0 \ + && make generate \ + && go mod tidy \ + && make generate-bin \ No newline at end of file diff --git a/go.mod b/go.mod index 230ac29c..e55eb4d6 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/containernetworking/cni v1.1.2 github.com/containernetworking/plugins v1.3.0 github.com/go-logfmt/logfmt v0.6.0 + github.com/google/uuid v1.3.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 @@ -32,7 +33,6 @@ require ( github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/imdario/mergo v0.3.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect