k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/e2e/windows/host_process.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package windows 18 19 import ( 20 "context" 21 "fmt" 22 "strings" 23 "time" 24 25 semver "github.com/blang/semver/v4" 26 "github.com/onsi/ginkgo/v2" 27 "github.com/onsi/gomega" 28 v1 "k8s.io/api/core/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/fields" 31 "k8s.io/apimachinery/pkg/util/uuid" 32 "k8s.io/apimachinery/pkg/util/wait" 33 clientset "k8s.io/client-go/kubernetes" 34 "k8s.io/kubernetes/test/e2e/feature" 35 "k8s.io/kubernetes/test/e2e/framework" 36 e2ekubelet "k8s.io/kubernetes/test/e2e/framework/kubelet" 37 e2emetrics "k8s.io/kubernetes/test/e2e/framework/metrics" 38 e2epod "k8s.io/kubernetes/test/e2e/framework/pod" 39 e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper" 40 imageutils "k8s.io/kubernetes/test/utils/image" 41 admissionapi "k8s.io/pod-security-admission/api" 42 ) 43 44 const ( 45 validation_script = `if (-not(Test-Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\emptydir)) { 46 throw "Cannot find emptydir volume" 47 } 48 if (-not(Test-Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\configmap\text.txt)) { 49 throw "Cannot find text.txt in configmap-volume" 50 } 51 $c = Get-Content -Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\configmap\text.txt 52 if ($c -ne "Lorem ipsum dolor sit amet") { 53 throw "Contents of /etc/configmap/text.txt are not as expected" 54 } 55 if (-not(Test-Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\hostpath)) { 56 throw "Cannot find hostpath volume" 57 } 58 if (-not(Test-Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\downwardapi\podname)) { 59 throw "Cannot find podname file in downward-api volume" 60 } 61 $c = Get-Content -Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\downwardapi\podname 62 if ($c -ne "host-process-volume-mounts") { 63 throw "Contents of /etc/downward-api/podname are not as expected" 64 } 65 if (-not(Test-Path $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\secret\foo.txt)) { 66 throw "Cannot find file foo.txt in secret volume" 67 } 68 $c = Get-Content $env:CONTAINER_SANDBOX_MOUNT_POINT\etc\secret\foo.txt 69 if ($c -ne "bar") { 70 Write-Output $c 71 throw "Contents of /etc/secret/foo.txt are not as expected" 72 } 73 # Windows ComputerNames cannot exceed 15 characters, which means that the $env:COMPUTERNAME 74 # can only be a substring of $env:NODE_NAME_TEST. We compare it with the hostname instead. 75 # The following comparison is case insensitive. 76 if ($env:NODE_NAME_TEST -ine "$(hostname)") { 77 throw "NODE_NAME_TEST env var ($env:NODE_NAME_TEST) does not equal hostname" 78 } 79 Write-Output "SUCCESS"` 80 ) 81 82 var ( 83 trueVar = true 84 85 User_NTAuthorityLocalService = "NT AUTHORITY\\Local Service" 86 User_NTAuthoritySystem = "NT AUTHORITY\\SYSTEM" 87 ) 88 89 var _ = sigDescribe(feature.WindowsHostProcessContainers, "[MinimumKubeletVersion:1.22] HostProcess containers", skipUnlessWindows(func() { 90 ginkgo.BeforeEach(func() { 91 e2eskipper.SkipUnlessNodeOSDistroIs("windows") 92 }) 93 94 f := framework.NewDefaultFramework("host-process-test-windows") 95 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged 96 97 ginkgo.It("should run as a process on the host/node", func(ctx context.Context) { 98 99 ginkgo.By("selecting a Windows node") 100 targetNode, err := findWindowsNode(ctx, f) 101 framework.ExpectNoError(err, "Error finding Windows node") 102 framework.Logf("Using node: %v", targetNode.Name) 103 104 ginkgo.By("scheduling a pod with a container that verifies %COMPUTERNAME% matches selected node name") 105 image := imageutils.GetConfig(imageutils.BusyBox) 106 podName := "host-process-test-pod" 107 // We're passing this to powershell.exe -Command. Inside, we must use apostrophes for strings. 108 command := fmt.Sprintf(`& {if ('%s' -ine "$(hostname)") { exit -1 }}`, targetNode.Name) 109 pod := &v1.Pod{ 110 ObjectMeta: metav1.ObjectMeta{ 111 Name: podName, 112 }, 113 Spec: v1.PodSpec{ 114 SecurityContext: &v1.PodSecurityContext{ 115 WindowsOptions: &v1.WindowsSecurityContextOptions{ 116 HostProcess: &trueVar, 117 RunAsUserName: &User_NTAuthoritySystem, 118 }, 119 }, 120 HostNetwork: true, 121 Containers: []v1.Container{ 122 { 123 Image: image.GetE2EImage(), 124 Name: "computer-name-test", 125 Command: []string{"powershell.exe", "-Command", command}, 126 }, 127 }, 128 RestartPolicy: v1.RestartPolicyNever, 129 NodeName: targetNode.Name, 130 }, 131 } 132 133 e2epod.NewPodClient(f).Create(ctx, pod) 134 135 ginkgo.By("Waiting for pod to run") 136 e2epod.NewPodClient(f).WaitForFinish(ctx, podName, 3*time.Minute) 137 138 ginkgo.By("Then ensuring pod finished running successfully") 139 p, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Get( 140 ctx, 141 podName, 142 metav1.GetOptions{}) 143 144 framework.ExpectNoError(err, "Error retrieving pod") 145 gomega.Expect(p.Status.Phase).To(gomega.Equal(v1.PodSucceeded)) 146 }) 147 148 ginkgo.It("should support init containers", func(ctx context.Context) { 149 ginkgo.By("scheduling a pod with a container that verifies init container can configure the node") 150 podName := "host-process-init-pods" 151 filename := fmt.Sprintf("/testfile%s.txt", string(uuid.NewUUID())) 152 pod := &v1.Pod{ 153 ObjectMeta: metav1.ObjectMeta{ 154 Name: podName, 155 }, 156 Spec: v1.PodSpec{ 157 SecurityContext: &v1.PodSecurityContext{ 158 WindowsOptions: &v1.WindowsSecurityContextOptions{ 159 HostProcess: &trueVar, 160 RunAsUserName: &User_NTAuthoritySystem, 161 }, 162 }, 163 HostNetwork: true, 164 InitContainers: []v1.Container{ 165 { 166 Image: imageutils.GetE2EImage(imageutils.BusyBox), 167 Name: "configure-node", 168 Command: []string{"powershell", "-c", "Set-content", "-Path", filename, "-V", "test"}, 169 }, 170 }, 171 Containers: []v1.Container{ 172 { 173 Image: imageutils.GetE2EImage(imageutils.BusyBox), 174 Name: "read-configuration", 175 Command: []string{"powershell", "-c", "ls", filename}, 176 }, 177 }, 178 RestartPolicy: v1.RestartPolicyNever, 179 NodeSelector: map[string]string{ 180 "kubernetes.io/os": "windows", 181 }, 182 }, 183 } 184 185 e2epod.NewPodClient(f).Create(ctx, pod) 186 187 ginkgo.By("Waiting for pod to run") 188 e2epod.NewPodClient(f).WaitForFinish(ctx, podName, 3*time.Minute) 189 190 ginkgo.By("Then ensuring pod finished running successfully") 191 p, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Get( 192 ctx, 193 podName, 194 metav1.GetOptions{}) 195 196 framework.ExpectNoError(err, "Error retrieving pod") 197 198 if p.Status.Phase != v1.PodSucceeded { 199 logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, podName, "read-configuration") 200 if err != nil { 201 framework.Logf("Error pulling logs: %v", err) 202 } 203 framework.Logf("Pod phase: %v\nlogs:\n%s", p.Status.Phase, logs) 204 } 205 gomega.Expect(p.Status.Phase).To(gomega.Equal(v1.PodSucceeded)) 206 }) 207 208 ginkgo.It("container command path validation", func(ctx context.Context) { 209 210 // The way hostprocess containers are created is being updated in container 211 // v1.7 to better support volume mounts and part of these changes include 212 // updates how the container's starting process is invoked. 213 // These test cases are only valid for containerd v1.6. 214 // See https://github.com/kubernetes/enhancements/blob/master/keps/sig-windows/1981-windows-privileged-container-support/README.md 215 // for more details. 216 ginkgo.By("Ensuring Windows nodes are running containerd v1.6.x") 217 windowsNode, err := findWindowsNode(ctx, f) 218 framework.ExpectNoError(err, "error finding Windows node") 219 r, v, err := getNodeContainerRuntimeAndVersion(windowsNode) 220 framework.ExpectNoError(err, "error getting node container runtime and version") 221 framework.Logf("Got runtime: %s, version %v, node: %s", r, v, windowsNode.Name) 222 223 if !strings.EqualFold(r, "containerd") { 224 e2eskipper.Skipf("container runtime is not containerd") 225 } 226 227 v1dot7 := semver.MustParse("1.7.0") 228 if v.GTE(v1dot7) { 229 e2eskipper.Skipf("container runtime is >= 1.7.0") 230 } 231 232 // The following test cases are broken into batches to speed up the test. 233 // Each batch will be scheduled as a single pod with a container for each test case. 234 // Pods will be scheduled sequentially since the start-up cost of containers is high 235 // on Windows and ginkgo may also schedule test cases in parallel. 236 tests := [][]struct { 237 command []string 238 args []string 239 workingDir string 240 }{ 241 { 242 { 243 command: []string{"cmd.exe", "/c", "ver"}, 244 }, 245 { 246 command: []string{"System32\\cmd.exe", "/c", "ver"}, 247 workingDir: "c:\\Windows", 248 }, 249 { 250 command: []string{"System32\\cmd.exe", "/c", "ver"}, 251 workingDir: "c:\\Windows\\", 252 }, 253 { 254 command: []string{"%CONTAINER_SANDBOX_MOUNT_POINT%\\bin\\uname.exe", "-o"}, 255 }, 256 }, 257 { 258 { 259 command: []string{"%CONTAINER_SANDBOX_MOUNT_POINT%/bin/uname.exe", "-o"}, 260 }, 261 { 262 command: []string{"%CONTAINER_SANDBOX_MOUNT_POINT%\\bin/uname.exe", "-o"}, 263 }, 264 { 265 command: []string{"bin/uname.exe", "-o"}, 266 }, 267 { 268 command: []string{"bin/uname.exe", "-o"}, 269 workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%", 270 }, 271 }, 272 { 273 { 274 command: []string{"bin\\uname.exe", "-o"}, 275 workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%", 276 }, 277 { 278 command: []string{"uname.exe", "-o"}, 279 workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%/bin", 280 }, 281 { 282 command: []string{"uname.exe", "-o"}, 283 workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%/bin/", 284 }, 285 { 286 command: []string{"uname.exe", "-o"}, 287 workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%\\bin\\", 288 }, 289 }, 290 { 291 { 292 command: []string{"powershell", "cmd.exe", "/ver"}, 293 }, 294 { 295 command: []string{"powershell", "c:/Windows/System32/cmd.exe", "/c", "ver"}, 296 }, 297 { 298 command: []string{"powershell", "c:\\Windows\\System32/cmd.exe", "/c", "ver"}, 299 }, 300 { 301 command: []string{"powershell", "%CONTAINER_SANDBOX_MOUNT_POINT%\\bin\\uname.exe", "-o"}, 302 }, 303 }, 304 { 305 { 306 command: []string{"powershell", "$env:CONTAINER_SANDBOX_MOUNT_POINT\\bin\\uname.exe", "-o"}, 307 }, 308 { 309 command: []string{"powershell", "%CONTAINER_SANDBOX_MOUNT_POINT%/bin/uname.exe", "-o"}, 310 }, 311 { 312 command: []string{"powershell", "$env:CONTAINER_SANDBOX_MOUNT_POINT/bin/uname.exe", "-o"}, 313 }, 314 { 315 command: []string{"powershell", "bin/uname.exe", "-o"}, 316 workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%", 317 }, 318 }, 319 { 320 { 321 command: []string{"powershell", "bin/uname.exe", "-o"}, 322 workingDir: "$env:CONTAINER_SANDBOX_MOUNT_POINT", 323 }, 324 { 325 command: []string{"powershell", "bin\\uname.exe", "-o"}, 326 workingDir: "$env:CONTAINER_SANDBOX_MOUNT_POINT", 327 }, 328 { 329 command: []string{"powershell", ".\\uname.exe", "-o"}, 330 workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%/bin", 331 }, 332 { 333 command: []string{"powershell", "./uname.exe", "-o"}, 334 workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%/bin", 335 }, 336 }, 337 { 338 { 339 command: []string{"powershell", "./uname.exe", "-o"}, 340 workingDir: "$env:CONTAINER_SANDBOX_MOUNT_POINT\\bin\\", 341 }, 342 { 343 command: []string{"%CONTAINER_SANDBOX_MOUNT_POINT%\\bin\\uname.exe"}, 344 args: []string{"-o"}, 345 }, 346 { 347 command: []string{"bin\\uname.exe"}, 348 args: []string{"-o"}, 349 workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%", 350 }, 351 { 352 command: []string{"uname.exe"}, 353 args: []string{"-o"}, 354 workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%\\bin", 355 }, 356 }, 357 { 358 { 359 command: []string{"cmd.exe"}, 360 args: []string{"/c", "dir", "%CONTAINER_SANDBOX_MOUNT_POINT%\\bin\\uname.exe"}, 361 }, 362 { 363 command: []string{"cmd.exe"}, 364 args: []string{"/c", "dir", "bin\\uname.exe"}, 365 workingDir: "%CONTAINER_SANDBOX_MOUNT_POINT%", 366 }, 367 { 368 command: []string{"powershell"}, 369 args: []string{"Get-ChildItem", "-Path", "$env:CONTAINER_SANDBOX_MOUNT_POINT\\bin\\uname.exe"}, 370 }, 371 { 372 command: []string{"powershell"}, 373 args: []string{"Get-ChildItem", "-Path", "bin\\uname.exe"}, 374 workingDir: "$env:CONTAINER_SANDBOX_MOUNT_POINT", 375 }, 376 }, 377 { 378 { 379 command: []string{"powershell"}, 380 args: []string{"Get-ChildItem", "-Path", "uname.exe"}, 381 workingDir: "$env:CONTAINER_SANDBOX_MOUNT_POINT/bin", 382 }, 383 }, 384 } 385 386 for podIndex, testCaseBatch := range tests { 387 image := imageutils.GetConfig(imageutils.BusyBox) 388 podName := fmt.Sprintf("host-process-command-%d", podIndex) 389 containers := []v1.Container{} 390 for containerIndex, testCase := range testCaseBatch { 391 containerName := fmt.Sprintf("host-process-command-%d-%d", podIndex, containerIndex) 392 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)) 393 394 container := v1.Container{ 395 Image: image.GetE2EImage(), 396 Name: containerName, 397 Command: testCase.command, 398 Args: testCase.args, 399 WorkingDir: testCase.workingDir, 400 } 401 containers = append(containers, container) 402 } 403 404 pod := &v1.Pod{ 405 ObjectMeta: metav1.ObjectMeta{ 406 Name: podName, 407 }, 408 Spec: v1.PodSpec{ 409 SecurityContext: &v1.PodSecurityContext{ 410 WindowsOptions: &v1.WindowsSecurityContextOptions{ 411 HostProcess: &trueVar, 412 RunAsUserName: &User_NTAuthorityLocalService, 413 }, 414 }, 415 HostNetwork: true, 416 Containers: containers, 417 RestartPolicy: v1.RestartPolicyNever, 418 NodeSelector: map[string]string{ 419 "kubernetes.io/os": "windows", 420 }, 421 }, 422 } 423 e2epod.NewPodClient(f).Create(ctx, pod) 424 425 ginkgo.By(fmt.Sprintf("Waiting for pod '%s' to run", podName)) 426 e2epod.NewPodClient(f).WaitForFinish(ctx, podName, 3*time.Minute) 427 428 ginkgo.By("Then ensuring pod finished running successfully") 429 p, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Get( 430 ctx, 431 podName, 432 metav1.GetOptions{}) 433 434 framework.ExpectNoError(err, "Error retrieving pod") 435 436 if p.Status.Phase != v1.PodSucceeded { 437 framework.Logf("Getting pod events") 438 options := metav1.ListOptions{ 439 FieldSelector: fields.Set{ 440 "involvedObject.kind": "Pod", 441 "involvedObject.name": podName, 442 "involvedObject.namespace": f.Namespace.Name, 443 }.AsSelector().String(), 444 } 445 events, err := f.ClientSet.CoreV1().Events(f.Namespace.Name).List(ctx, options) 446 framework.ExpectNoError(err, "Error getting events for failed pod") 447 for _, event := range events.Items { 448 framework.Logf("%s: %s", event.Reason, event.Message) 449 } 450 framework.Failf("Pod '%s' did failed.", p.Name) 451 } 452 } 453 454 }) 455 456 ginkgo.It("should support various volume mount types", func(ctx context.Context) { 457 ns := f.Namespace 458 459 ginkgo.By("Creating a configmap containing test data and a validation script") 460 configMap := &v1.ConfigMap{ 461 TypeMeta: metav1.TypeMeta{ 462 APIVersion: "v1", 463 Kind: "ConfigMap", 464 }, 465 ObjectMeta: metav1.ObjectMeta{ 466 Name: "sample-config-map", 467 }, 468 Data: map[string]string{ 469 "text": "Lorem ipsum dolor sit amet", 470 "validation-script": validation_script, 471 }, 472 } 473 _, err := f.ClientSet.CoreV1().ConfigMaps(ns.Name).Create(ctx, configMap, metav1.CreateOptions{}) 474 framework.ExpectNoError(err, "unable to create create configmap") 475 476 ginkgo.By("Creating a secret containing test data") 477 secret := &v1.Secret{ 478 TypeMeta: metav1.TypeMeta{ 479 APIVersion: "v1", 480 Kind: "Secret", 481 }, 482 ObjectMeta: metav1.ObjectMeta{ 483 Name: "sample-secret", 484 }, 485 Type: v1.SecretTypeOpaque, 486 Data: map[string][]byte{ 487 "foo": []byte("bar"), 488 }, 489 } 490 _, err = f.ClientSet.CoreV1().Secrets(ns.Name).Create(ctx, secret, metav1.CreateOptions{}) 491 framework.ExpectNoError(err, "unable to create secret") 492 493 ginkgo.By("Creating a pod with a HostProcess container that uses various types of volume mounts") 494 495 podAndContainerName := "host-process-volume-mounts" 496 pod := makeTestPodWithVolumeMounts(podAndContainerName) 497 498 e2epod.NewPodClient(f).Create(ctx, pod) 499 500 ginkgo.By("Waiting for pod to run") 501 e2epod.NewPodClient(f).WaitForFinish(ctx, podAndContainerName, 3*time.Minute) 502 503 logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, ns.Name, podAndContainerName, podAndContainerName) 504 framework.ExpectNoError(err, "Error getting pod logs") 505 framework.Logf("Container logs: %s", logs) 506 507 ginkgo.By("Then ensuring pod finished running successfully") 508 p, err := f.ClientSet.CoreV1().Pods(ns.Name).Get( 509 ctx, 510 podAndContainerName, 511 metav1.GetOptions{}) 512 513 framework.ExpectNoError(err, "Error retrieving pod") 514 gomega.Expect(p.Status.Phase).To(gomega.Equal(v1.PodSucceeded)) 515 }) 516 517 ginkgo.It("metrics should report count of started and failed to start HostProcess containers", func(ctx context.Context) { 518 ginkgo.By("Selecting a Windows node") 519 targetNode, err := findWindowsNode(ctx, f) 520 framework.ExpectNoError(err, "Error finding Windows node") 521 framework.Logf("Using node: %v", targetNode.Name) 522 523 ginkgo.By("Getting initial kubelet metrics values") 524 beforeMetrics, err := getCurrentHostProcessMetrics(ctx, f, targetNode.Name) 525 framework.ExpectNoError(err, "Error getting initial kubelet metrics for node") 526 framework.Logf("Initial HostProcess container metrics -- StartedContainers: %v, StartedContainersErrors: %v, StartedInitContainers: %v, StartedInitContainersErrors: %v", 527 beforeMetrics.StartedContainersCount, beforeMetrics.StartedContainersErrorCount, beforeMetrics.StartedInitContainersCount, beforeMetrics.StartedInitContainersErrorCount) 528 529 ginkgo.By("Scheduling a pod with a HostProcess init container that will fail") 530 531 badUserName := "bad-user-name" 532 533 podName := "host-process-metrics-pod-failing-init-container" 534 pod := &v1.Pod{ 535 ObjectMeta: metav1.ObjectMeta{ 536 Name: podName, 537 }, 538 Spec: v1.PodSpec{ 539 SecurityContext: &v1.PodSecurityContext{ 540 WindowsOptions: &v1.WindowsSecurityContextOptions{ 541 HostProcess: &trueVar, 542 RunAsUserName: &User_NTAuthoritySystem, 543 }, 544 }, 545 HostNetwork: true, 546 InitContainers: []v1.Container{ 547 { 548 SecurityContext: &v1.SecurityContext{ 549 WindowsOptions: &v1.WindowsSecurityContextOptions{ 550 RunAsUserName: &badUserName, 551 }, 552 }, 553 Image: imageutils.GetE2EImage(imageutils.BusyBox), 554 Name: "failing-init-container", 555 Command: []string{"foobar.exe"}, 556 }, 557 }, 558 Containers: []v1.Container{ 559 { 560 Image: imageutils.GetE2EImage(imageutils.BusyBox), 561 Name: "container", 562 Command: []string{"cmd.exe", "/c", "exit", "/b", "0"}, 563 }, 564 }, 565 RestartPolicy: v1.RestartPolicyNever, 566 NodeName: targetNode.Name, 567 }, 568 } 569 570 e2epod.NewPodClient(f).Create(ctx, pod) 571 e2epod.NewPodClient(f).WaitForFinish(ctx, podName, 3*time.Minute) 572 573 ginkgo.By("Scheduling a pod with a HostProcess container that will fail") 574 podName = "host-process-metrics-pod-failing-container" 575 pod = &v1.Pod{ 576 ObjectMeta: metav1.ObjectMeta{ 577 Name: podName, 578 }, 579 Spec: v1.PodSpec{ 580 SecurityContext: &v1.PodSecurityContext{ 581 WindowsOptions: &v1.WindowsSecurityContextOptions{ 582 HostProcess: &trueVar, 583 RunAsUserName: &User_NTAuthoritySystem, 584 }, 585 }, 586 HostNetwork: true, 587 Containers: []v1.Container{ 588 { 589 SecurityContext: &v1.SecurityContext{ 590 WindowsOptions: &v1.WindowsSecurityContextOptions{ 591 RunAsUserName: &badUserName, 592 }, 593 }, 594 Image: imageutils.GetE2EImage(imageutils.BusyBox), 595 Name: "failing-container", 596 Command: []string{"foobar.exe"}, 597 }, 598 }, 599 RestartPolicy: v1.RestartPolicyNever, 600 NodeName: targetNode.Name, 601 }, 602 } 603 604 e2epod.NewPodClient(f).Create(ctx, pod) 605 e2epod.NewPodClient(f).WaitForFinish(ctx, podName, 3*time.Minute) 606 607 ginkgo.By("Getting subsequent kubelet metrics values") 608 609 afterMetrics, err := getCurrentHostProcessMetrics(ctx, f, targetNode.Name) 610 framework.ExpectNoError(err, "Error getting subsequent kubelet metrics for node") 611 framework.Logf("Subsequent HostProcess container metrics -- StartedContainers: %v, StartedContainersErrors: %v, StartedInitContainers: %v, StartedInitContainersErrors: %v", 612 afterMetrics.StartedContainersCount, afterMetrics.StartedContainersErrorCount, afterMetrics.StartedInitContainersCount, afterMetrics.StartedInitContainersErrorCount) 613 614 // Note: This test performs relative comparisons to ensure metrics values were logged and does not validate specific values. 615 // This done so the test can be run in parallel with other tests which may start HostProcess containers on the same node. 616 ginkgo.By("Ensuring metrics were updated") 617 gomega.Expect(beforeMetrics.StartedContainersCount).To(gomega.BeNumerically("<", afterMetrics.StartedContainersCount), "Count of started HostProcess containers should increase") 618 gomega.Expect(beforeMetrics.StartedContainersErrorCount).To(gomega.BeNumerically("<", afterMetrics.StartedContainersErrorCount), "Count of started HostProcess errors containers should increase") 619 gomega.Expect(beforeMetrics.StartedInitContainersCount).To(gomega.BeNumerically("<", afterMetrics.StartedInitContainersCount), "Count of started HostProcess init containers should increase") 620 gomega.Expect(beforeMetrics.StartedInitContainersErrorCount).To(gomega.BeNumerically("<", afterMetrics.StartedInitContainersErrorCount), "Count of started HostProcess errors init containers should increase") 621 }) 622 623 ginkgo.It("container stats validation", func(ctx context.Context) { 624 ginkgo.By("selecting a Windows node") 625 targetNode, err := findWindowsNode(ctx, f) 626 framework.ExpectNoError(err, "Error finding Windows node") 627 framework.Logf("Using node: %v", targetNode.Name) 628 629 ginkgo.By("schedule a pod with a HostProcess container") 630 image := imageutils.GetConfig(imageutils.BusyBox) 631 podName := "host-process-stats-pod" 632 pod := &v1.Pod{ 633 ObjectMeta: metav1.ObjectMeta{ 634 Name: podName, 635 }, 636 Spec: v1.PodSpec{ 637 SecurityContext: &v1.PodSecurityContext{ 638 WindowsOptions: &v1.WindowsSecurityContextOptions{ 639 HostProcess: &trueVar, 640 RunAsUserName: &User_NTAuthorityLocalService, 641 }, 642 }, 643 HostNetwork: true, 644 Containers: []v1.Container{ 645 { 646 Image: image.GetE2EImage(), 647 Name: "host-process-stats", 648 Command: []string{"powershell.exe", "-Command", "Write-Host 'Hello'; sleep -Seconds 600"}, 649 }, 650 }, 651 RestartPolicy: v1.RestartPolicyNever, 652 NodeName: targetNode.Name, 653 }, 654 } 655 656 e2epod.NewPodClient(f).Create(ctx, pod) 657 658 ginkgo.By("Waiting for the pod to start running") 659 timeout := 3 * time.Minute 660 err = e2epod.WaitForPodsRunningReady(ctx, f.ClientSet, f.Namespace.Name, 1, timeout) 661 framework.ExpectNoError(err) 662 663 ginkgo.By("Getting container stats for pod") 664 statsChecked := false 665 666 wait.Poll(2*time.Second, timeout, func() (bool, error) { 667 return ensurePodsStatsOnNode(ctx, f.ClientSet, f.Namespace.Name, targetNode.Name, &statsChecked) 668 }) 669 670 if !statsChecked { 671 framework.Failf("Failed to get stats for pod %s/%s", f.Namespace.Name, podName) 672 } 673 }) 674 675 ginkgo.It("should support querying api-server using in-cluster config", func(ctx context.Context) { 676 // This functionality is only support on containerd v1.7+ 677 skipUnlessContainerdOneSevenOrGreater(ctx, f) 678 679 ginkgo.By("Scheduling a pod that runs agnhost inclusterclient") 680 podName := "host-process-agnhost-icc" 681 pod := &v1.Pod{ 682 ObjectMeta: metav1.ObjectMeta{ 683 Name: podName, 684 }, 685 Spec: v1.PodSpec{ 686 SecurityContext: &v1.PodSecurityContext{ 687 WindowsOptions: &v1.WindowsSecurityContextOptions{ 688 HostProcess: &trueVar, 689 RunAsUserName: &User_NTAuthoritySystem, 690 }, 691 }, 692 HostNetwork: true, 693 Containers: []v1.Container{ 694 { 695 Image: imageutils.GetE2EImage(imageutils.Agnhost), 696 Name: "hpc-agnhost", 697 // TODO: Figure out why we need to copy agnhost as agnhost.exe here 698 // Possibly related to https://github.com/microsoft/hcsshim/issues/1128 and 699 // https://github.com/microsoft/hcsshim/pull/1174 updating PATHEXT here doesn't 700 // seem address the issue. 701 Command: []string{"cmd", "/C", "copy", "c:\\hpc\\agnhost", "c:\\hpc\\agnhost.exe", "&&", "c:\\hpc\\agnhost.exe", "inclusterclient"}, 702 }, 703 }, 704 RestartPolicy: v1.RestartPolicyNever, 705 NodeSelector: map[string]string{ 706 "kubernetes.io/os": "windows", 707 }, 708 }, 709 } 710 711 pc := e2epod.NewPodClient(f) 712 pc.Create(ctx, pod) 713 714 ginkgo.By("Waiting for pod to run") 715 err := e2epod.WaitForPodsRunningReady(ctx, f.ClientSet, f.Namespace.Name, 1, 3*time.Minute) 716 framework.ExpectNoError(err) 717 718 ginkgo.By("Waiting for 60 seconds") 719 // We wait an additional 60 seconds after the pod is Running because the 720 // `InClusterClient` app in the agnhost image waits 30 seconds before 721 // starting to make queries - https://github.com/kubernetes/kubernetes/blob/ceee3783ed963497db669504241af2ca6a11610f/test/images/agnhost/inclusterclient/main.go#L51 722 time.Sleep(60 * time.Second) 723 724 ginkgo.By("Ensuring the test app was able to successfully query the api-server") 725 logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, podName, "hpc-agnhost") 726 framework.ExpectNoError(err, "Error getting pod logs") 727 728 framework.Logf("Logs: %s\n", logs) 729 730 gomega.Expect(logs).Should(gomega.ContainSubstring("calling /healthz"), "app logs should contain 'calling /healthz'") 731 732 gomega.Expect(logs).ShouldNot(gomega.ContainSubstring("status=failed"), "app logs should not contain 'status=failed'") 733 }) 734 735 ginkgo.It("should run as localgroup accounts", func(ctx context.Context) { 736 // This functionality is only supported on containerd v1.7+ 737 skipUnlessContainerdOneSevenOrGreater(ctx, f) 738 739 ginkgo.By("Scheduling a pod that creates a localgroup from an init container then starts a container using that group") 740 localGroupName := getRandomUserGrounName() 741 podName := "host-process-localgroup-pod" 742 pod := &v1.Pod{ 743 ObjectMeta: metav1.ObjectMeta{ 744 Name: podName, 745 }, 746 Spec: v1.PodSpec{ 747 SecurityContext: &v1.PodSecurityContext{ 748 WindowsOptions: &v1.WindowsSecurityContextOptions{ 749 HostProcess: &trueVar, 750 RunAsUserName: &User_NTAuthoritySystem, 751 }, 752 }, 753 HostNetwork: true, 754 InitContainers: []v1.Container{ 755 { 756 Image: imageutils.GetE2EImage(imageutils.BusyBox), 757 Name: "setup", 758 Command: []string{"cmd", "/C", "net", "localgroup", localGroupName, "/add"}, 759 }, 760 }, 761 Containers: []v1.Container{ 762 { 763 Image: imageutils.GetE2EImage(imageutils.BusyBox), 764 Name: "localgroup-container", 765 Command: []string{"cmd", "/C", "whoami"}, 766 SecurityContext: &v1.SecurityContext{ 767 WindowsOptions: &v1.WindowsSecurityContextOptions{ 768 RunAsUserName: &localGroupName, 769 }, 770 }, 771 }, 772 }, 773 RestartPolicy: v1.RestartPolicyNever, 774 NodeSelector: map[string]string{ 775 "kubernetes.io/os": "windows", 776 }, 777 }, 778 } 779 780 e2epod.NewPodClient(f).Create(ctx, pod) 781 782 ginkgo.By("Waiting for pod to run") 783 e2epod.NewPodClient(f).WaitForFinish(ctx, podName, 3*time.Minute) 784 785 ginkgo.By("Then ensuring pod finished running successfully") 786 p, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Get( 787 context.TODO(), 788 podName, 789 metav1.GetOptions{}) 790 791 framework.ExpectNoError(err, "error retrieving pod") 792 gomega.Expect(p.Status.Phase).To(gomega.Equal(v1.PodSucceeded)) 793 794 // whoami will output %COMPUTER_NAME%/{randomly generated username} here. 795 // It is sufficient to just check that the logs do not container `nt authority` 796 // because all of the 'built-in' accounts that can be used with HostProcess 797 // are prefixed with this. 798 ginkgo.By("Then ensuring pod was not running as a system account") 799 logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, podName, "localgroup-container") 800 framework.ExpectNoError(err, "error retrieving container logs") 801 framework.Logf("Pod logs: %s", logs) 802 gomega.Expect(strings.ToLower(logs)).ShouldNot(gomega.ContainSubstring("nt authority"), "Container runs 'whoami' and logs should not contain 'nt authority'") 803 }) 804 805 })) 806 807 func makeTestPodWithVolumeMounts(name string) *v1.Pod { 808 hostPathDirectoryOrCreate := v1.HostPathDirectoryOrCreate 809 return &v1.Pod{ 810 ObjectMeta: metav1.ObjectMeta{ 811 Name: name, 812 }, 813 Spec: v1.PodSpec{ 814 SecurityContext: &v1.PodSecurityContext{ 815 WindowsOptions: &v1.WindowsSecurityContextOptions{ 816 HostProcess: &trueVar, 817 RunAsUserName: &User_NTAuthoritySystem, 818 }, 819 }, 820 HostNetwork: true, 821 Containers: []v1.Container{ 822 { 823 Image: imageutils.GetE2EImage(imageutils.BusyBox), 824 Name: name, 825 Command: []string{"powershell.exe", "./etc/configmap/validationscript.ps1"}, 826 Env: []v1.EnvVar{ 827 { 828 Name: "NODE_NAME_TEST", 829 ValueFrom: &v1.EnvVarSource{ 830 FieldRef: &v1.ObjectFieldSelector{ 831 FieldPath: "spec.nodeName", 832 }, 833 }, 834 }, 835 }, 836 VolumeMounts: []v1.VolumeMount{ 837 { 838 Name: "emptydir-volume", 839 MountPath: "/etc/emptydir", 840 }, 841 { 842 Name: "configmap-volume", 843 MountPath: "/etc/configmap", 844 }, 845 { 846 Name: "hostpath-volume", 847 MountPath: "/etc/hostpath", 848 }, 849 { 850 Name: "downwardapi-volume", 851 MountPath: "/etc/downwardapi", 852 }, 853 { 854 Name: "secret-volume", 855 MountPath: "/etc/secret", 856 }, 857 }, 858 }, 859 }, 860 RestartPolicy: v1.RestartPolicyNever, 861 NodeSelector: map[string]string{ 862 "kubernetes.io/os": "windows", 863 }, 864 Volumes: []v1.Volume{ 865 { 866 Name: "emptydir-volume", 867 VolumeSource: v1.VolumeSource{ 868 EmptyDir: &v1.EmptyDirVolumeSource{ 869 Medium: v1.StorageMediumDefault, 870 }, 871 }, 872 }, 873 { 874 Name: "configmap-volume", 875 VolumeSource: v1.VolumeSource{ 876 ConfigMap: &v1.ConfigMapVolumeSource{ 877 LocalObjectReference: v1.LocalObjectReference{ 878 Name: "sample-config-map", 879 }, 880 Items: []v1.KeyToPath{ 881 { 882 Key: "text", 883 Path: "text.txt", 884 }, 885 { 886 Key: "validation-script", 887 Path: "validationscript.ps1", 888 }, 889 }, 890 }, 891 }, 892 }, 893 { 894 Name: "hostpath-volume", 895 VolumeSource: v1.VolumeSource{ 896 HostPath: &v1.HostPathVolumeSource{ 897 Path: "/var/hostpath", 898 Type: &hostPathDirectoryOrCreate, 899 }, 900 }, 901 }, 902 { 903 Name: "downwardapi-volume", 904 VolumeSource: v1.VolumeSource{ 905 DownwardAPI: &v1.DownwardAPIVolumeSource{ 906 Items: []v1.DownwardAPIVolumeFile{ 907 { 908 Path: "podname", 909 FieldRef: &v1.ObjectFieldSelector{ 910 FieldPath: "metadata.name", 911 }, 912 }, 913 }, 914 }, 915 }, 916 }, 917 { 918 Name: "secret-volume", 919 VolumeSource: v1.VolumeSource{ 920 Secret: &v1.SecretVolumeSource{ 921 SecretName: "sample-secret", 922 Items: []v1.KeyToPath{ 923 { 924 Key: "foo", 925 Path: "foo.txt", 926 }, 927 }, 928 }, 929 }, 930 }, 931 }, 932 }, 933 } 934 } 935 936 type HostProcessContainersMetrics struct { 937 StartedContainersCount int64 938 StartedContainersErrorCount int64 939 StartedInitContainersCount int64 940 StartedInitContainersErrorCount int64 941 } 942 943 // ensurePodsStatsOnNode returns a boolean after checking Pods statistics on a particular node 944 func ensurePodsStatsOnNode(ctx context.Context, c clientset.Interface, namespace, nodeName string, statsChecked *bool) (bool, error) { 945 nodeStats, err := e2ekubelet.GetStatsSummary(ctx, c, nodeName) 946 framework.ExpectNoError(err, "Error getting node stats") 947 948 for _, podStats := range nodeStats.Pods { 949 if podStats.PodRef.Namespace != namespace { 950 continue 951 } 952 953 // check various pod stats 954 if *podStats.CPU.UsageCoreNanoSeconds <= 0 { 955 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) 956 return false, nil 957 } 958 if *podStats.Memory.WorkingSetBytes <= 0 { 959 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) 960 return false, nil 961 } 962 963 for _, containerStats := range podStats.Containers { 964 *statsChecked = true 965 966 // check various container stats 967 if *containerStats.CPU.UsageCoreNanoSeconds <= 0 { 968 framework.Logf("Container %s stats report cpu usage equal to %v but should be greater than 0", containerStats.Name, *containerStats.CPU.UsageCoreNanoSeconds) 969 return false, nil 970 } 971 if *containerStats.Memory.WorkingSetBytes <= 0 { 972 framework.Logf("Container %s stats report memory usage equal to %v but should be greater than 0", containerStats.Name, *containerStats.Memory.WorkingSetBytes) 973 return false, nil 974 } 975 if *containerStats.Logs.UsedBytes <= 0 { 976 framework.Logf("Container %s stats log usage equal to %v but should be greater than 0", containerStats.Name, *containerStats.Logs.UsedBytes) 977 return false, nil 978 } 979 } 980 } 981 return true, nil 982 } 983 984 // getCurrentHostProcessMetrics returns a HostPRocessContainersMetrics object. Any metrics that do not have any 985 // values reported will be set to 0. 986 func getCurrentHostProcessMetrics(ctx context.Context, f *framework.Framework, nodeName string) (HostProcessContainersMetrics, error) { 987 var result HostProcessContainersMetrics 988 989 metrics, err := e2emetrics.GetKubeletMetrics(ctx, f.ClientSet, nodeName) 990 if err != nil { 991 return result, err 992 } 993 994 for _, sample := range metrics["started_host_process_containers_total"] { 995 switch sample.Metric["container_type"] { 996 case "container": 997 result.StartedContainersCount = int64(sample.Value) 998 case "init_container": 999 result.StartedInitContainersCount = int64(sample.Value) 1000 } 1001 } 1002 1003 // note: accumulate failures of all types (ErrImagePull, RunContainerError, etc) 1004 // for each container type here. 1005 for _, sample := range metrics["started_host_process_containers_errors_total"] { 1006 switch sample.Metric["container_type"] { 1007 case "container": 1008 result.StartedContainersErrorCount += int64(sample.Value) 1009 case "init_container": 1010 result.StartedInitContainersErrorCount += int64(sample.Value) 1011 } 1012 } 1013 1014 return result, nil 1015 }