From 7e6e31577e41c9bed1bd6c65b6c0307695c643d3 Mon Sep 17 00:00:00 2001 From: Claudiu Belu Date: Fri, 8 Jul 2022 13:48:26 +0300 Subject: [PATCH] unittests: Adds Windows unittests Adds unit tests for a few functions that are not covered. --- .../kuberuntime_container_windows_test.go | 111 +++++++++++++ .../security_context_windows_test.go | 7 +- pkg/volume/awsebs/attacher_windows_test.go | 96 +++++++++++ pkg/volume/azuredd/azure_common_windows.go | 9 +- .../azuredd/azure_common_windows_test.go | 154 ++++++++++++++++++ .../vsphere_volume_util_windows_test.go | 43 +++++ 6 files changed, 415 insertions(+), 5 deletions(-) create mode 100644 pkg/kubelet/kuberuntime/kuberuntime_container_windows_test.go create mode 100644 pkg/volume/awsebs/attacher_windows_test.go create mode 100644 pkg/volume/azuredd/azure_common_windows_test.go create mode 100644 pkg/volume/vsphere_volume/vsphere_volume_util_windows_test.go diff --git a/pkg/kubelet/kuberuntime/kuberuntime_container_windows_test.go b/pkg/kubelet/kuberuntime/kuberuntime_container_windows_test.go new file mode 100644 index 00000000000..47f40d62693 --- /dev/null +++ b/pkg/kubelet/kuberuntime/kuberuntime_container_windows_test.go @@ -0,0 +1,111 @@ +//go:build windows +// +build windows + +/* +Copyright 2022 The Kubernetes Authors. + +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 kuberuntime + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" + runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1" + "k8s.io/kubernetes/pkg/features" +) + +func TestApplyPlatformSpecificContainerConfig(t *testing.T) { + _, _, fakeRuntimeSvc, err := createTestRuntimeManager() + require.NoError(t, err) + + containerConfig := &runtimeapi.ContainerConfig{} + + resources := v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("128Mi"), + v1.ResourceCPU: resource.MustParse("1"), + }, + Limits: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("256Mi"), + v1.ResourceCPU: resource.MustParse("3"), + }, + } + + gmsaCredSpecName := "gmsa spec name" + gmsaCredSpec := "credential spec" + username := "ContainerAdministrator" + asHostProcess := true + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + UID: "12345678", + Name: "bar", + Namespace: "new", + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "foo", + Image: "busybox", + ImagePullPolicy: v1.PullIfNotPresent, + Command: []string{"testCommand"}, + WorkingDir: "testWorkingDir", + Resources: resources, + SecurityContext: &v1.SecurityContext{ + WindowsOptions: &v1.WindowsSecurityContextOptions{ + GMSACredentialSpecName: &gmsaCredSpecName, + GMSACredentialSpec: &gmsaCredSpec, + RunAsUserName: &username, + HostProcess: &asHostProcess, + }, + }, + }, + }, + }, + } + + err = fakeRuntimeSvc.applyPlatformSpecificContainerConfig(containerConfig, &pod.Spec.Containers[0], pod, new(int64), "foo", nil) + require.NoError(t, err) + + expectedCpuMax := ((10000 * 3000) / int64(runtime.NumCPU()) / 1000) + expectedWindowsConfig := &runtimeapi.WindowsContainerConfig{ + Resources: &runtimeapi.WindowsContainerResources{ + CpuMaximum: expectedCpuMax, + MemoryLimitInBytes: 256 * 1024 * 1024, + }, + SecurityContext: &runtimeapi.WindowsContainerSecurityContext{ + CredentialSpec: gmsaCredSpec, + RunAsUsername: "ContainerAdministrator", + HostProcess: true, + }, + } + assert.Equal(t, expectedWindowsConfig, containerConfig.Windows) + + // Check if it fails if we require HostProcess but the feature is not enabled. + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.WindowsHostProcessContainers, false)() + err = fakeRuntimeSvc.applyPlatformSpecificContainerConfig(containerConfig, &pod.Spec.Containers[0], pod, new(int64), "foo", nil) + expectedErrMsg := "pod contains HostProcess containers but feature 'WindowsHostProcessContainers' is not enabled" + if err == nil || err.Error() != expectedErrMsg { + t.Errorf("expected error message `%s` but got `%v`", expectedErrMsg, err) + } +} diff --git a/pkg/kubelet/kuberuntime/security_context_windows_test.go b/pkg/kubelet/kuberuntime/security_context_windows_test.go index 6cca733b5d9..9842e7bdc2d 100644 --- a/pkg/kubelet/kuberuntime/security_context_windows_test.go +++ b/pkg/kubelet/kuberuntime/security_context_windows_test.go @@ -50,6 +50,7 @@ func TestVerifyRunAsNonRoot(t *testing.T) { anyUser := "anyone" runAsNonRootTrue := true runAsNonRootFalse := false + uid := int64(0) for _, test := range []struct { desc string sc *v1.SecurityContext @@ -120,7 +121,11 @@ func TestVerifyRunAsNonRoot(t *testing.T) { { desc: "Pass if container's user and image's user aren't set and RunAsNonRoot is true", sc: &v1.SecurityContext{ - RunAsNonRoot: &runAsNonRootTrue, + // verifyRunAsNonRoot should ignore the RunAsUser, SELinuxOptions, and RunAsGroup options. + RunAsUser: &uid, + SELinuxOptions: &v1.SELinuxOptions{}, + RunAsGroup: &uid, + RunAsNonRoot: &runAsNonRootTrue, }, fail: false, }, diff --git a/pkg/volume/awsebs/attacher_windows_test.go b/pkg/volume/awsebs/attacher_windows_test.go new file mode 100644 index 00000000000..b27fbaab827 --- /dev/null +++ b/pkg/volume/awsebs/attacher_windows_test.go @@ -0,0 +1,96 @@ +//go:build !providerless && windows +// +build !providerless,windows + +/* +Copyright 2022 The Kubernetes Authors. + +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 awsebs + +import ( + "errors" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + volumetest "k8s.io/kubernetes/pkg/volume/testing" + "k8s.io/utils/exec" + exectest "k8s.io/utils/exec/testing" +) + +func TestGetDevicePath(t *testing.T) { + testCases := []struct { + commandOutput string + commandError error + expectedOutput string + expectedError bool + expectedErrMsg string + }{ + { + commandOutput: "", + commandError: errors.New("expected error."), + expectedError: true, + expectedErrMsg: "error calling ebsnvme-id.exe: expected error.", + }, + { + commandOutput: "foolish output.", + expectedError: true, + expectedErrMsg: `disk not found in ebsnvme-id.exe output: "foolish output."`, + }, + { + commandOutput: "Disk Number: 42\nVolume ID: vol-fake-id", + expectedOutput: "42", + }, + } + + fakeHost := volumetest.NewFakeVolumeHost(t, os.TempDir(), nil, nil) + fakeExec := fakeHost.GetExec("").(*exectest.FakeExec) + + // This will enable fakeExec to "run" commands. + fakeExec.DisableScripts = false + attacher := &awsElasticBlockStoreAttacher{ + host: fakeHost, + } + + for _, tc := range testCases { + fakeCmd := &exectest.FakeCmd{ + CombinedOutputScript: []exectest.FakeAction{ + func() ([]byte, []byte, error) { + return []byte(tc.commandOutput), []byte(""), tc.commandError + }, + }, + } + fakeExec.CommandScript = []exectest.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { + return fakeCmd + }, + } + fakeExec.CommandCalls = 0 + + fakeVolID := "aws://us-west-2b/vol-fake-id" + devPath, err := attacher.getDevicePath(fakeVolID, "fake-partition", "fake-device-path") + if tc.expectedError { + if err == nil || err.Error() != tc.expectedErrMsg { + t.Errorf("expected error message `%s` but got `%v`", tc.expectedErrMsg, err) + } + continue + } + + require.NoError(t, err) + assert.Equal(t, tc.expectedOutput, devPath) + } +} diff --git a/pkg/volume/azuredd/azure_common_windows.go b/pkg/volume/azuredd/azure_common_windows.go index 3a1bc487397..c137b438ad5 100644 --- a/pkg/volume/azuredd/azure_common_windows.go +++ b/pkg/volume/azuredd/azure_common_windows.go @@ -100,10 +100,10 @@ func findDiskByLun(lun int, iohandler ioHandler, exec utilexec.Interface) (strin return "", nil } -func formatIfNotFormatted(disk string, fstype string, exec utilexec.Interface) { +func formatIfNotFormatted(disk string, fstype string, exec utilexec.Interface) error { if err := mount.ValidateDiskNumber(disk); err != nil { klog.Errorf("azureDisk Mount: formatIfNotFormatted failed, err: %v\n", err) - return + return err } if len(fstype) == 0 { @@ -115,7 +115,8 @@ func formatIfNotFormatted(disk string, fstype string, exec utilexec.Interface) { output, err := exec.Command("powershell", "/c", cmd).CombinedOutput() if err != nil { klog.Errorf("azureDisk Mount: Get-Disk failed, error: %v, output: %q", err, string(output)) - } else { - klog.Infof("azureDisk Mount: Disk successfully formatted, disk: %q, fstype: %q\n", disk, fstype) + return err } + klog.Infof("azureDisk Mount: Disk successfully formatted, disk: %q, fstype: %q\n", disk, fstype) + return nil } diff --git a/pkg/volume/azuredd/azure_common_windows_test.go b/pkg/volume/azuredd/azure_common_windows_test.go new file mode 100644 index 00000000000..9e8d7dcf042 --- /dev/null +++ b/pkg/volume/azuredd/azure_common_windows_test.go @@ -0,0 +1,154 @@ +//go:build !providerless && windows +// +build !providerless,windows + +/* +Copyright 2022 The Kubernetes Authors. + +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 azuredd + +import ( + "encoding/json" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "k8s.io/utils/exec" + exectest "k8s.io/utils/exec/testing" +) + +func newFakeExec(stdout []byte, err error) *exectest.FakeExec { + fakeCmd := &exectest.FakeCmd{ + CombinedOutputScript: []exectest.FakeAction{ + func() ([]byte, []byte, error) { + return stdout, []byte(""), err + }, + }, + } + return &exectest.FakeExec{ + CommandScript: []exectest.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { + return fakeCmd + }, + }, + } +} + +func TestScsiHostRescan(t *testing.T) { + // NOTE: We don't have any assertions we can make for this test. + fakeExec := newFakeExec([]byte("expected output."), errors.New("expected error.")) + scsiHostRescan(nil, fakeExec) +} + +func TestGetDevicePath(t *testing.T) { + diskNoLun := make(map[string]interface{}, 0) + diskNoLun["location"] = "incorrect location" + + // The expectation is that the string will contain at least 2 spaces + diskIncorrectLun := make(map[string]interface{}, 0) + diskIncorrectLun["location"] = " LUN 1" + + diskNoIntegerLun := make(map[string]interface{}, 0) + diskNoIntegerLun["location"] = "Integrated : Adapter 1 : Port 0 : Target 0 : LUN A" + + lun := 42 + invalidDiskNumberLun := make(map[string]interface{}, 0) + invalidDiskNumberLun["location"] = "Integrated : Adapter 1 : Port 0 : Target 0 : LUN 42" + invalidDiskNumberLun["number"] = "not a float" + + validLun := make(map[string]interface{}, 0) + validLun["location"] = "Integrated : Adapter 1 : Port 0 : Target 0 : LUN 42" + validLun["number"] = 1.5 + + noDiskFoundJson, _ := json.Marshal([]map[string]interface{}{diskNoLun, diskIncorrectLun, diskNoIntegerLun}) + invaliDiskJson, _ := json.Marshal([]map[string]interface{}{invalidDiskNumberLun}) + validJson, _ := json.Marshal([]map[string]interface{}{validLun}) + + testCases := []struct { + commandOutput []byte + commandError error + expectedOutput string + expectedError bool + expectedErrMsg string + }{ + { + commandOutput: []byte("foolish output."), + commandError: errors.New("expected error."), + expectedError: true, + expectedErrMsg: "expected error.", + }, + { + commandOutput: []byte("too short"), + expectedError: true, + expectedErrMsg: `Get-Disk output is too short, output: "too short"`, + }, + { + commandOutput: []byte("not a json"), + expectedError: true, + expectedErrMsg: `invalid character 'o' in literal null (expecting 'u')`, + }, + { + commandOutput: noDiskFoundJson, + expectedOutput: "", + }, + { + commandOutput: invaliDiskJson, + expectedError: true, + expectedErrMsg: fmt.Sprintf("LUN(%d) found, but could not get disk number, location: %q", lun, invalidDiskNumberLun["location"]), + }, + { + commandOutput: validJson, + expectedOutput: "/dev/disk1", + }, + } + + for _, tc := range testCases { + fakeExec := newFakeExec(tc.commandOutput, tc.commandError) + disk, err := findDiskByLun(lun, nil, fakeExec) + + if tc.expectedError { + if err == nil || err.Error() != tc.expectedErrMsg { + t.Errorf("expected error message `%s` but got `%v`", tc.expectedErrMsg, err) + } + continue + } + + require.NoError(t, err) + assert.Equal(t, tc.expectedOutput, disk) + } +} + +func TestFormatIfNotFormatted(t *testing.T) { + fakeExec := newFakeExec([]byte{}, errors.New("expected error.")) + + err := formatIfNotFormatted("fake disk number", "", fakeExec) + expectedErrMsg := `wrong disk number format: "fake disk number", err: strconv.Atoi: parsing "fake disk number": invalid syntax` + if err == nil || err.Error() != expectedErrMsg { + t.Errorf("expected error message `%s` but got `%v`", expectedErrMsg, err) + } + + err = formatIfNotFormatted("1", "", fakeExec) + expectedErrMsg = "expected error." + if err == nil || err.Error() != expectedErrMsg { + t.Errorf("expected error message `%s` but got `%v`", expectedErrMsg, err) + } + + fakeExec = newFakeExec([]byte{}, nil) + err = formatIfNotFormatted("1", "", fakeExec) + require.NoError(t, err) +} diff --git a/pkg/volume/vsphere_volume/vsphere_volume_util_windows_test.go b/pkg/volume/vsphere_volume/vsphere_volume_util_windows_test.go new file mode 100644 index 00000000000..06f072301d3 --- /dev/null +++ b/pkg/volume/vsphere_volume/vsphere_volume_util_windows_test.go @@ -0,0 +1,43 @@ +//go:build !providerless && windows +// +build !providerless,windows + +/* +Copyright 2022 The Kubernetes Authors. + +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 vsphere_volume + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFormatIfNotFormatted(t *testing.T) { + // If this volume has already been mounted then + // its devicePath will have already been converted to a disk number, + // meaning that the original path is returned. + devPath, err := verifyDevicePath("foo") + require.NoError(t, err) + assert.Equal(t, "foo", devPath) + + // Won't match any serial number, meaning that an error will be returned. + devPath, err = verifyDevicePath(diskByIDPath + diskSCSIPrefix + "fake-serial") + expectedErrMsg := `unable to find vSphere disk with serial fake-serial` + if err == nil || err.Error() != expectedErrMsg { + t.Errorf("expected error message `%s` but got `%v`", expectedErrMsg, err) + } +}