/* Copyright 2021 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 windows import ( "context" "fmt" "strings" "time" semver "github.com/blang/semver/v4" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/apimachinery/pkg/util/wait" clientset "k8s.io/client-go/kubernetes" "k8s.io/kubernetes/test/e2e/feature" "k8s.io/kubernetes/test/e2e/framework" e2ekubelet "k8s.io/kubernetes/test/e2e/framework/kubelet" e2emetrics "k8s.io/kubernetes/test/e2e/framework/metrics" e2epod "k8s.io/kubernetes/test/e2e/framework/pod" e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper" imageutils "k8s.io/kubernetes/test/utils/image" admissionapi "k8s.io/pod-security-admission/api" ) const ( validation_script = `if (-not(Test-Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\emptydir)) { throw "Cannot find emptydir volume" } if (-not(Test-Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\configmap\text.txt)) { throw "Cannot find text.txt in configmap-volume" } $c = Get-Content -Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\configmap\text.txt if ($c -ne "Lorem ipsum dolor sit amet") { throw "Contents of /etc/configmap/text.txt are not as expected" } if (-not(Test-Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\hostpath)) { throw "Cannot find hostpath volume" } if (-not(Test-Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\downwardapi\podname)) { throw "Cannot find podname file in downward-api volume" } $c = Get-Content -Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\downwardapi\podname if ($c -ne "host-process-volume-mounts") { throw "Contents of /etc/downward-api/podname are not as expected" } if (-not(Test-Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\secret\foo.txt)) { throw "Cannot find file foo.txt in secret volume" } $c = Get-Content $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\secret\foo.txt if ($c -ne "bar") { Write-Output $c throw "Contents of /etc/secret/foo.txt are not as expected" } # Windows ComputerNames cannot exceed 15 characters, which means that the $env:COMPUTERNAME # can only be a substring of $env:NODE_NAME_TEST. We compare it with the hostname instead. # The following comparison is case insensitive. if ($env:NODE_NAME_TEST -ine "$(hostname)") { throw "NODE_NAME_TEST env var ($env:NODE_NAME_TEST) does not equal hostname" } Write-Output "SUCCESS"` ) var ( trueVar = true User_NTAuthorityLocalService = "NT AUTHORITY\\Local Service" User_NTAuthoritySystem = "NT AUTHORITY\\SYSTEM" ) var _ = sigDescribe(feature.WindowsHostProcessContainers, "[MinimumKubeletVersion:1.22] HostProcess containers", skipUnlessWindows(func() { ginkgo.BeforeEach(func() { e2eskipper.SkipUnlessNodeOSDistroIs("windows") }) f := framework.NewDefaultFramework("host-process-test-windows") f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged ginkgo.It("should run as a process on the host/node", func(ctx context.Context) { ginkgo.By("selecting a Windows node") targetNode, err := findWindowsNode(ctx, f) framework.ExpectNoError(err, "Error finding Windows node") framework.Logf("Using node: %v", targetNode.Name) ginkgo.By("scheduling a pod with a container that verifies %COMPUTERNAME% matches selected node name") image := imageutils.GetConfig(imageutils.BusyBox) podName := "host-process-test-pod" // We're passing this to powershell.exe -Command. Inside, we must use apostrophes for strings. command := fmt.Sprintf(`& {if ('%s' -ine "$(hostname)") { exit -1 }}`, targetNode.Name) pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: podName, }, Spec: v1.PodSpec{ SecurityContext: &v1.PodSecurityContext{ WindowsOptions: &v1.WindowsSecurityContextOptions{ HostProcess: &trueVar, RunAsUserName: &User_NTAuthoritySystem, }, }, HostNetwork: true, Containers: []v1.Container{ { Image: image.GetE2EImage(), Name: "computer-name-test", Command: []string{"powershell.exe", "-Command", command}, }, }, RestartPolicy: v1.RestartPolicyNever, NodeName: targetNode.Name, }, } e2epod.NewPodClient(f).Create(ctx, pod) ginkgo.By("Waiting for pod to run") e2epod.NewPodClient(f).WaitForFinish(ctx, podName, 3*time.Minute) ginkgo.By("Then ensuring pod finished running successfully") p, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Get( ctx, podName, metav1.GetOptions{}) framework.ExpectNoError(err, "Error retrieving pod") gomega.Expect(p.Status.Phase).To(gomega.Equal(v1.PodSucceeded)) }) ginkgo.It("should support init containers", func(ctx context.Context) { ginkgo.By("scheduling a pod with a container that verifies init container can configure the node") podName := "host-process-init-pods" filename := fmt.Sprintf("/testfile%s.txt", string(uuid.NewUUID())) pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: podName, }, Spec: v1.PodSpec{ SecurityContext: &v1.PodSecurityContext{ WindowsOptions: &v1.WindowsSecurityContextOptions{ HostProcess: &trueVar, RunAsUserName: &User_NTAuthoritySystem, }, }, HostNetwork: true, InitContainers: []v1.Container{ { Image: imageutils.GetE2EImage(imageutils.BusyBox), Name: "configure-node", Command: []string{"powershell", "-c", "Set-content", "-Path", filename, "-V", "test"}, }, }, Containers: []v1.Container{ { Image: imageutils.GetE2EImage(imageutils.BusyBox), Name: "read-configuration", Command: []string{"powershell", "-c", "ls", filename}, }, }, RestartPolicy: v1.RestartPolicyNever, NodeSelector: map[string]string{ "kubernetes.io/os": "windows", }, }, } e2epod.NewPodClient(f).Create(ctx, pod) ginkgo.By("Waiting for pod to run") e2epod.NewPodClient(f).WaitForFinish(ctx, podName, 3*time.Minute) ginkgo.By("Then ensuring pod finished running successfully") p, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Get( ctx, podName, metav1.GetOptions{}) framework.ExpectNoError(err, "Error retrieving pod") if p.Status.Phase != v1.PodSucceeded { logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, podName, "read-configuration") if err != nil { framework.Logf("Error pulling logs: %v", err) } framework.Logf("Pod phase: %v\nlogs:\n%s", p.Status.Phase, logs) } gomega.Expect(p.Status.Phase).To(gomega.Equal(v1.PodSucceeded)) }) ginkgo.It("container command path validation", func(ctx context.Context) { // The way hostprocess containers are created is being updated in container // v1.7 to better support volume mounts and part of these changes include // updates how the container's starting process is invoked. // These test cases are only valid for containerd v1.6. // See https://github.com/kubernetes/enhancements/blob/master/keps/sig-windows/1981-windows-privileged-container-support/README.md // for more details. ginkgo.By("Ensuring Windows nodes are running containerd v1.6.x") windowsNode, err := findWindowsNode(ctx, f) framework.ExpectNoError(err, "error finding Windows node") r, v, err := getNodeContainerRuntimeAndVersion(windowsNode) framework.ExpectNoError(err, "error getting node container runtime and version") framework.Logf("Got runtime: %s, version %v, node: %s", r, v, windowsNode.Name) if !strings.EqualFold(r, "containerd") { e2eskipper.Skipf("container runtime is not containerd") } v1dot7 := semver.MustParse("1.7.0") if v.GTE(v1dot7) { e2eskipper.Skipf("container runtime is >= 1.7.0") } // The following test cases are broken into batches to speed up the test. // Each batch will be scheduled as a single pod with a container for each test case. // Pods will be scheduled sequentially since the start-up cost of containers is high // on Windows and ginkgo may also schedule test cases in parallel. tests := [][]struct { command []string args []string workingDir string }{ { { command: []string{"cmd.exe", "/c", "ver"}, }, { command: []string{"System32\\cmd.exe", "/c", "ver"}, workingDir: "c:\\Windows", }, { command: []string{"System32\\cmd.exe", "/c", "ver"}, workingDir: "c:\\Windows\\", }, { command: []string{"%CONTAINER_SANDBOX_MOUNT_POINT%\\bin\\uname.exe", "-o"}, }, }, { { command: []string{"%CONTAINER_SANDBOX_MOUNT_POINT%/bin/uname.exe", "-o"}, }, { command: []string{"%CONTAINER_SANDBOX_MOUNT_POINT%\\bin/uname.exe", "-o"}, }, { command: []string{"bin/uname.exe", "-o"}, }, { command: []string{"bin/uname.exe", "-o"}, workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%", }, }, { { command: []string{"bin\\uname.exe", "-o"}, workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%", }, { command: []string{"uname.exe", "-o"}, workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%/bin", }, { command: []string{"uname.exe", "-o"}, workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%/bin/", }, { command: []string{"uname.exe", "-o"}, workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%\\bin\\", }, }, { { command: []string{"powershell", "cmd.exe", "/ver"}, }, { command: []string{"powershell", "c:/Windows/System32/cmd.exe", "/c", "ver"}, }, { command: []string{"powershell", "c:\\Windows\\System32/cmd.exe", "/c", "ver"}, }, { command: []string{"powershell", "%CONTAINER_SANDBOX_MOUNT_POINT%\\bin\\uname.exe", "-o"}, }, }, { { command: []string{"powershell", "$env:CONTAINER_SANDBOX_MOUNT_POINT\\bin\\uname.exe", "-o"}, }, { command: []string{"powershell", "%CONTAINER_SANDBOX_MOUNT_POINT%/bin/uname.exe", "-o"}, }, { command: []string{"powershell", "$env:CONTAINER_SANDBOX_MOUNT_POINT/bin/uname.exe", "-o"}, }, { command: []string{"powershell", "bin/uname.exe", "-o"}, workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%", }, }, { { command: []string{"powershell", "bin/uname.exe", "-o"}, workingDir: "$env:CONTAINER_SANDBOX_MOUNT_POINT", }, { command: []string{"powershell", "bin\\uname.exe", "-o"}, workingDir: "$env:CONTAINER_SANDBOX_MOUNT_POINT", }, { command: []string{"powershell", ".\\uname.exe", "-o"}, workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%/bin", }, { command: []string{"powershell", "./uname.exe", "-o"}, workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%/bin", }, }, { { command: []string{"powershell", "./uname.exe", "-o"}, workingDir: "$env:CONTAINER_SANDBOX_MOUNT_POINT\\bin\\", }, { command: []string{"%CONTAINER_SANDBOX_MOUNT_POINT%\\bin\\uname.exe"}, args: []string{"-o"}, }, { command: []string{"bin\\uname.exe"}, args: []string{"-o"}, workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%", }, { command: []string{"uname.exe"}, args: []string{"-o"}, workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%\\bin", }, }, { { command: []string{"cmd.exe"}, args: []string{"/c", "dir", "%CONTAINER_SANDBOX_MOUNT_POINT%\\bin\\uname.exe"}, }, { command: []string{"cmd.exe"}, args: []string{"/c", "dir", "bin\\uname.exe"}, workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%", }, { command: []string{"powershell"}, args: []string{"Get-ChildItem", "-Path", "$env:CONTAINER_SANDBOX_MOUNT_POINT\\bin\\uname.exe"}, }, { command: []string{"powershell"}, args: []string{"Get-ChildItem", "-Path", "bin\\uname.exe"}, workingDir: "$env:CONTAINER_SANDBOX_MOUNT_POINT", }, }, { { command: []string{"powershell"}, args: []string{"Get-ChildItem", "-Path", "uname.exe"}, workingDir: "$env:CONTAINER_SANDBOX_MOUNT_POINT/bin", }, }, } for podIndex, testCaseBatch := range tests { image := imageutils.GetConfig(imageutils.BusyBox) podName := fmt.Sprintf("host-process-command-%d", podIndex) containers := []v1.Container{} for containerIndex, testCase := range testCaseBatch { containerName := fmt.Sprintf("host-process-command-%d-%d", podIndex, containerIndex) ginkgo.By(fmt.Sprintf("Adding a container '%s' to pod '%s' with command: %s, args: %s, workingDir: %s", containerName, podName, strings.Join(testCase.command, " "), strings.Join(testCase.args, " "), testCase.workingDir)) container := v1.Container{ Image: image.GetE2EImage(), Name: containerName, Command: testCase.command, Args: testCase.args, WorkingDir: testCase.workingDir, } containers = append(containers, container) } pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: podName, }, Spec: v1.PodSpec{ SecurityContext: &v1.PodSecurityContext{ WindowsOptions: &v1.WindowsSecurityContextOptions{ HostProcess: &trueVar, RunAsUserName: &User_NTAuthorityLocalService, }, }, HostNetwork: true, Containers: containers, RestartPolicy: v1.RestartPolicyNever, NodeSelector: map[string]string{ "kubernetes.io/os": "windows", }, }, } e2epod.NewPodClient(f).Create(ctx, pod) ginkgo.By(fmt.Sprintf("Waiting for pod '%s' to run", podName)) e2epod.NewPodClient(f).WaitForFinish(ctx, podName, 3*time.Minute) ginkgo.By("Then ensuring pod finished running successfully") p, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Get( ctx, podName, metav1.GetOptions{}) framework.ExpectNoError(err, "Error retrieving pod") if p.Status.Phase != v1.PodSucceeded { framework.Logf("Getting pod events") options := metav1.ListOptions{ FieldSelector: fields.Set{ "involvedObject.kind": "Pod", "involvedObject.name": podName, "involvedObject.namespace": f.Namespace.Name, }.AsSelector().String(), } events, err := f.ClientSet.CoreV1().Events(f.Namespace.Name).List(ctx, options) framework.ExpectNoError(err, "Error getting events for failed pod") for _, event := range events.Items { framework.Logf("%s: %s", event.Reason, event.Message) } framework.Failf("Pod '%s' did failed.", p.Name) } } }) ginkgo.It("should support various volume mount types", func(ctx context.Context) { ns := f.Namespace ginkgo.By("Creating a configmap containing test data and a validation script") configMap := &v1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "sample-config-map", }, Data: map[string]string{ "text": "Lorem ipsum dolor sit amet", "validation-script": validation_script, }, } _, err := f.ClientSet.CoreV1().ConfigMaps(ns.Name).Create(ctx, configMap, metav1.CreateOptions{}) framework.ExpectNoError(err, "unable to create create configmap") ginkgo.By("Creating a secret containing test data") secret := &v1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "sample-secret", }, Type: v1.SecretTypeOpaque, Data: map[string][]byte{ "foo": []byte("bar"), }, } _, err = f.ClientSet.CoreV1().Secrets(ns.Name).Create(ctx, secret, metav1.CreateOptions{}) framework.ExpectNoError(err, "unable to create secret") ginkgo.By("Creating a pod with a HostProcess container that uses various types of volume mounts") podAndContainerName := "host-process-volume-mounts" pod := makeTestPodWithVolumeMounts(podAndContainerName) e2epod.NewPodClient(f).Create(ctx, pod) ginkgo.By("Waiting for pod to run") e2epod.NewPodClient(f).WaitForFinish(ctx, podAndContainerName, 3*time.Minute) logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, ns.Name, podAndContainerName, podAndContainerName) framework.ExpectNoError(err, "Error getting pod logs") framework.Logf("Container logs: %s", logs) ginkgo.By("Then ensuring pod finished running successfully") p, err := f.ClientSet.CoreV1().Pods(ns.Name).Get( ctx, podAndContainerName, metav1.GetOptions{}) framework.ExpectNoError(err, "Error retrieving pod") gomega.Expect(p.Status.Phase).To(gomega.Equal(v1.PodSucceeded)) }) ginkgo.It("metrics should report count of started and failed to start HostProcess containers", func(ctx context.Context) { ginkgo.By("Selecting a Windows node") targetNode, err := findWindowsNode(ctx, f) framework.ExpectNoError(err, "Error finding Windows node") framework.Logf("Using node: %v", targetNode.Name) ginkgo.By("Getting initial kubelet metrics values") beforeMetrics, err := getCurrentHostProcessMetrics(ctx, f, targetNode.Name) framework.ExpectNoError(err, "Error getting initial kubelet metrics for node") framework.Logf("Initial HostProcess container metrics -- StartedContainers: %v, StartedContainersErrors: %v, StartedInitContainers: %v, StartedInitContainersErrors: %v", beforeMetrics.StartedContainersCount, beforeMetrics.StartedContainersErrorCount, beforeMetrics.StartedInitContainersCount, beforeMetrics.StartedInitContainersErrorCount) ginkgo.By("Scheduling a pod with a HostProcess init container that will fail") badUserName := "bad-user-name" podName := "host-process-metrics-pod-failing-init-container" pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: podName, }, Spec: v1.PodSpec{ SecurityContext: &v1.PodSecurityContext{ WindowsOptions: &v1.WindowsSecurityContextOptions{ HostProcess: &trueVar, RunAsUserName: &User_NTAuthoritySystem, }, }, HostNetwork: true, InitContainers: []v1.Container{ { SecurityContext: &v1.SecurityContext{ WindowsOptions: &v1.WindowsSecurityContextOptions{ RunAsUserName: &badUserName, }, }, Image: imageutils.GetE2EImage(imageutils.BusyBox), Name: "failing-init-container", Command: []string{"foobar.exe"}, }, }, Containers: []v1.Container{ { Image: imageutils.GetE2EImage(imageutils.BusyBox), Name: "container", Command: []string{"cmd.exe", "/c", "exit", "/b", "0"}, }, }, RestartPolicy: v1.RestartPolicyNever, NodeName: targetNode.Name, }, } e2epod.NewPodClient(f).Create(ctx, pod) e2epod.NewPodClient(f).WaitForFinish(ctx, podName, 3*time.Minute) ginkgo.By("Scheduling a pod with a HostProcess container that will fail") podName = "host-process-metrics-pod-failing-container" pod = &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: podName, }, Spec: v1.PodSpec{ SecurityContext: &v1.PodSecurityContext{ WindowsOptions: &v1.WindowsSecurityContextOptions{ HostProcess: &trueVar, RunAsUserName: &User_NTAuthoritySystem, }, }, HostNetwork: true, Containers: []v1.Container{ { SecurityContext: &v1.SecurityContext{ WindowsOptions: &v1.WindowsSecurityContextOptions{ RunAsUserName: &badUserName, }, }, Image: imageutils.GetE2EImage(imageutils.BusyBox), Name: "failing-container", Command: []string{"foobar.exe"}, }, }, RestartPolicy: v1.RestartPolicyNever, NodeName: targetNode.Name, }, } e2epod.NewPodClient(f).Create(ctx, pod) e2epod.NewPodClient(f).WaitForFinish(ctx, podName, 3*time.Minute) ginkgo.By("Getting subsequent kubelet metrics values") afterMetrics, err := getCurrentHostProcessMetrics(ctx, f, targetNode.Name) framework.ExpectNoError(err, "Error getting subsequent kubelet metrics for node") framework.Logf("Subsequent HostProcess container metrics -- StartedContainers: %v, StartedContainersErrors: %v, StartedInitContainers: %v, StartedInitContainersErrors: %v", afterMetrics.StartedContainersCount, afterMetrics.StartedContainersErrorCount, afterMetrics.StartedInitContainersCount, afterMetrics.StartedInitContainersErrorCount) // Note: This test performs relative comparisons to ensure metrics values were logged and does not validate specific values. // This done so the test can be run in parallel with other tests which may start HostProcess containers on the same node. ginkgo.By("Ensuring metrics were updated") gomega.Expect(beforeMetrics.StartedContainersCount).To(gomega.BeNumerically("<", afterMetrics.StartedContainersCount), "Count of started HostProcess containers should increase") gomega.Expect(beforeMetrics.StartedContainersErrorCount).To(gomega.BeNumerically("<", afterMetrics.StartedContainersErrorCount), "Count of started HostProcess errors containers should increase") gomega.Expect(beforeMetrics.StartedInitContainersCount).To(gomega.BeNumerically("<", afterMetrics.StartedInitContainersCount), "Count of started HostProcess init containers should increase") gomega.Expect(beforeMetrics.StartedInitContainersErrorCount).To(gomega.BeNumerically("<", afterMetrics.StartedInitContainersErrorCount), "Count of started HostProcess errors init containers should increase") }) ginkgo.It("container stats validation", func(ctx context.Context) { ginkgo.By("selecting a Windows node") targetNode, err := findWindowsNode(ctx, f) framework.ExpectNoError(err, "Error finding Windows node") framework.Logf("Using node: %v", targetNode.Name) ginkgo.By("schedule a pod with a HostProcess container") image := imageutils.GetConfig(imageutils.BusyBox) podName := "host-process-stats-pod" pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: podName, }, Spec: v1.PodSpec{ SecurityContext: &v1.PodSecurityContext{ WindowsOptions: &v1.WindowsSecurityContextOptions{ HostProcess: &trueVar, RunAsUserName: &User_NTAuthorityLocalService, }, }, HostNetwork: true, Containers: []v1.Container{ { Image: image.GetE2EImage(), Name: "host-process-stats", Command: []string{"powershell.exe", "-Command", "Write-Host 'Hello'; sleep -Seconds 600"}, }, }, RestartPolicy: v1.RestartPolicyNever, NodeName: targetNode.Name, }, } e2epod.NewPodClient(f).Create(ctx, pod) ginkgo.By("Waiting for the pod to start running") timeout := 3 * time.Minute e2epod.WaitForPodsRunningReady(ctx, f.ClientSet, f.Namespace.Name, 1, 0, timeout) ginkgo.By("Getting container stats for pod") statsChecked := false wait.Poll(2*time.Second, timeout, func() (bool, error) { return ensurePodsStatsOnNode(ctx, f.ClientSet, f.Namespace.Name, targetNode.Name, &statsChecked) }) if !statsChecked { framework.Failf("Failed to get stats for pod %s/%s", f.Namespace.Name, podName) } }) ginkgo.It("should support querying api-server using in-cluster config", func(ctx context.Context) { // This functionality is only support on containerd v1.7+ skipUnlessContainerdOneSevenOrGreater(ctx, f) ginkgo.By("Scheduling a pod that runs agnhost inclusterclient") podName := "host-process-agnhost-icc" pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: podName, }, Spec: v1.PodSpec{ SecurityContext: &v1.PodSecurityContext{ WindowsOptions: &v1.WindowsSecurityContextOptions{ HostProcess: &trueVar, RunAsUserName: &User_NTAuthoritySystem, }, }, HostNetwork: true, Containers: []v1.Container{ { Image: imageutils.GetE2EImage(imageutils.Agnhost), Name: "hpc-agnhost", // TODO: Figure out why we need to copy agnhost as agnhost.exe here // Possibly related to https://github.com/microsoft/hcsshim/issues/1128 and // https://github.com/microsoft/hcsshim/pull/1174 updating PATHEXT here doesn't // seem address the issue. Command: []string{"cmd", "/C", "copy", "c:\\hpc\\agnhost", "c:\\hpc\\agnhost.exe", "&&", "c:\\hpc\\agnhost.exe", "inclusterclient"}, }, }, RestartPolicy: v1.RestartPolicyNever, NodeSelector: map[string]string{ "kubernetes.io/os": "windows", }, }, } pc := e2epod.NewPodClient(f) pc.Create(ctx, pod) ginkgo.By("Waiting for pod to run") e2epod.WaitForPodsRunningReady(ctx, f.ClientSet, f.Namespace.Name, 1, 0, 3*time.Minute) ginkgo.By("Waiting for 60 seconds") // We wait an additional 60 seconds after the pod is Running because the // `InClusterClient` app in the agnhost image waits 30 seconds before // starting to make queries - https://github.com/kubernetes/kubernetes/blob/ceee3783ed963497db669504241af2ca6a11610f/test/images/agnhost/inclusterclient/main.go#L51 time.Sleep(60 * time.Second) ginkgo.By("Ensuring the test app was able to successfully query the api-server") logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, podName, "hpc-agnhost") framework.ExpectNoError(err, "Error getting pod logs") framework.Logf("Logs: %s\n", logs) gomega.Expect(logs).Should(gomega.ContainSubstring("calling /healthz"), "app logs should contain 'calling /healthz'") gomega.Expect(logs).ShouldNot(gomega.ContainSubstring("status=failed"), "app logs should not contain 'status=failed'") }) ginkgo.It("should run as localgroup accounts", func(ctx context.Context) { // This functionality is only supported on containerd v1.7+ skipUnlessContainerdOneSevenOrGreater(ctx, f) ginkgo.By("Scheduling a pod that creates a localgroup from an init container then starts a container using that group") localGroupName := getRandomUserGrounName() podName := "host-process-localgroup-pod" pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: podName, }, Spec: v1.PodSpec{ SecurityContext: &v1.PodSecurityContext{ WindowsOptions: &v1.WindowsSecurityContextOptions{ HostProcess: &trueVar, RunAsUserName: &User_NTAuthoritySystem, }, }, HostNetwork: true, InitContainers: []v1.Container{ { Image: imageutils.GetE2EImage(imageutils.BusyBox), Name: "setup", Command: []string{"cmd", "/C", "net", "localgroup", localGroupName, "/add"}, }, }, Containers: []v1.Container{ { Image: imageutils.GetE2EImage(imageutils.BusyBox), Name: "localgroup-container", Command: []string{"cmd", "/C", "whoami"}, SecurityContext: &v1.SecurityContext{ WindowsOptions: &v1.WindowsSecurityContextOptions{ RunAsUserName: &localGroupName, }, }, }, }, RestartPolicy: v1.RestartPolicyNever, NodeSelector: map[string]string{ "kubernetes.io/os": "windows", }, }, } e2epod.NewPodClient(f).Create(ctx, pod) ginkgo.By("Waiting for pod to run") e2epod.NewPodClient(f).WaitForFinish(ctx, podName, 3*time.Minute) ginkgo.By("Then ensuring pod finished running successfully") p, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Get( context.TODO(), podName, metav1.GetOptions{}) framework.ExpectNoError(err, "error retrieving pod") gomega.Expect(p.Status.Phase).To(gomega.Equal(v1.PodSucceeded)) // whoami will output %COMPUTER_NAME%/{randomly generated username} here. // It is sufficient to just check that the logs do not container `nt authority` // because all of the 'built-in' accounts that can be used with HostProcess // are prefixed with this. ginkgo.By("Then ensuring pod was not running as a system account") logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, podName, "localgroup-container") framework.ExpectNoError(err, "error retrieving container logs") framework.Logf("Pod logs: %s", logs) gomega.Expect(strings.ToLower(logs)).ShouldNot(gomega.ContainSubstring("nt authority"), "Container runs 'whoami' and logs should not contain 'nt authority'") }) })) func makeTestPodWithVolumeMounts(name string) *v1.Pod { hostPathDirectoryOrCreate := v1.HostPathDirectoryOrCreate return &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, Spec: v1.PodSpec{ SecurityContext: &v1.PodSecurityContext{ WindowsOptions: &v1.WindowsSecurityContextOptions{ HostProcess: &trueVar, RunAsUserName: &User_NTAuthoritySystem, }, }, HostNetwork: true, Containers: []v1.Container{ { Image: imageutils.GetE2EImage(imageutils.BusyBox), Name: name, Command: []string{"powershell.exe", "./etc/configmap/validationscript.ps1"}, Env: []v1.EnvVar{ { Name: "NODE_NAME_TEST", ValueFrom: &v1.EnvVarSource{ FieldRef: &v1.ObjectFieldSelector{ FieldPath: "spec.nodeName", }, }, }, }, VolumeMounts: []v1.VolumeMount{ { Name: "emptydir-volume", MountPath: "/etc/emptydir", }, { Name: "configmap-volume", MountPath: "/etc/configmap", }, { Name: "hostpath-volume", MountPath: "/etc/hostpath", }, { Name: "downwardapi-volume", MountPath: "/etc/downwardapi", }, { Name: "secret-volume", MountPath: "/etc/secret", }, }, }, }, RestartPolicy: v1.RestartPolicyNever, NodeSelector: map[string]string{ "kubernetes.io/os": "windows", }, Volumes: []v1.Volume{ { Name: "emptydir-volume", VolumeSource: v1.VolumeSource{ EmptyDir: &v1.EmptyDirVolumeSource{ Medium: v1.StorageMediumDefault, }, }, }, { Name: "configmap-volume", VolumeSource: v1.VolumeSource{ ConfigMap: &v1.ConfigMapVolumeSource{ LocalObjectReference: v1.LocalObjectReference{ Name: "sample-config-map", }, Items: []v1.KeyToPath{ { Key: "text", Path: "text.txt", }, { Key: "validation-script", Path: "validationscript.ps1", }, }, }, }, }, { Name: "hostpath-volume", VolumeSource: v1.VolumeSource{ HostPath: &v1.HostPathVolumeSource{ Path: "/var/hostpath", Type: &hostPathDirectoryOrCreate, }, }, }, { Name: "downwardapi-volume", VolumeSource: v1.VolumeSource{ DownwardAPI: &v1.DownwardAPIVolumeSource{ Items: []v1.DownwardAPIVolumeFile{ { Path: "podname", FieldRef: &v1.ObjectFieldSelector{ FieldPath: "metadata.name", }, }, }, }, }, }, { Name: "secret-volume", VolumeSource: v1.VolumeSource{ Secret: &v1.SecretVolumeSource{ SecretName: "sample-secret", Items: []v1.KeyToPath{ { Key: "foo", Path: "foo.txt", }, }, }, }, }, }, }, } } type HostProcessContainersMetrics struct { StartedContainersCount int64 StartedContainersErrorCount int64 StartedInitContainersCount int64 StartedInitContainersErrorCount int64 } // ensurePodsStatsOnNode returns a boolean after checking Pods statistics on a particular node func ensurePodsStatsOnNode(ctx context.Context, c clientset.Interface, namespace, nodeName string, statsChecked *bool) (bool, error) { nodeStats, err := e2ekubelet.GetStatsSummary(ctx, c, nodeName) framework.ExpectNoError(err, "Error getting node stats") for _, podStats := range nodeStats.Pods { if podStats.PodRef.Namespace != namespace { continue } // check various pod stats if *podStats.CPU.UsageCoreNanoSeconds <= 0 { framework.Logf("Pod %s/%s stats report cpu usage equal to %v but should be greater than 0", podStats.PodRef.Namespace, podStats.PodRef.Name, *podStats.CPU.UsageCoreNanoSeconds) return false, nil } if *podStats.Memory.WorkingSetBytes <= 0 { framework.Logf("Pod %s/%s stats report memory usage equal to %v but should be greater than 0", podStats.PodRef.Namespace, podStats.PodRef.Name, *podStats.CPU.UsageCoreNanoSeconds) return false, nil } for _, containerStats := range podStats.Containers { *statsChecked = true // check various container stats if *containerStats.CPU.UsageCoreNanoSeconds <= 0 { framework.Logf("Container %s stats report cpu usage equal to %v but should be greater than 0", containerStats.Name, *containerStats.CPU.UsageCoreNanoSeconds) return false, nil } if *containerStats.Memory.WorkingSetBytes <= 0 { framework.Logf("Container %s stats report memory usage equal to %v but should be greater than 0", containerStats.Name, *containerStats.Memory.WorkingSetBytes) return false, nil } if *containerStats.Logs.UsedBytes <= 0 { framework.Logf("Container %s stats log usage equal to %v but should be greater than 0", containerStats.Name, *containerStats.Logs.UsedBytes) return false, nil } } } return true, nil } // getCurrentHostProcessMetrics returns a HostPRocessContainersMetrics object. Any metrics that do not have any // values reported will be set to 0. func getCurrentHostProcessMetrics(ctx context.Context, f *framework.Framework, nodeName string) (HostProcessContainersMetrics, error) { var result HostProcessContainersMetrics metrics, err := e2emetrics.GetKubeletMetrics(ctx, f.ClientSet, nodeName) if err != nil { return result, err } for _, sample := range metrics["started_host_process_containers_total"] { switch sample.Metric["container_type"] { case "container": result.StartedContainersCount = int64(sample.Value) case "init_container": result.StartedInitContainersCount = int64(sample.Value) } } // note: accumulate failures of all types (ErrImagePull, RunContainerError, etc) // for each container type here. for _, sample := range metrics["started_host_process_containers_errors_total"] { switch sample.Metric["container_type"] { case "container": result.StartedContainersErrorCount += int64(sample.Value) case "init_container": result.StartedInitContainersErrorCount += int64(sample.Value) } } return result, nil }