k8s.io/kubernetes@v1.29.3/test/e2e/framework/pod/utils.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 pod
    18  
    19  import (
    20  	"flag"
    21  	"fmt"
    22  
    23  	"github.com/onsi/ginkgo/v2"
    24  	"github.com/onsi/gomega"
    25  
    26  	v1 "k8s.io/api/core/v1"
    27  	imageutils "k8s.io/kubernetes/test/utils/image"
    28  	psaapi "k8s.io/pod-security-admission/api"
    29  	psapolicy "k8s.io/pod-security-admission/policy"
    30  	"k8s.io/utils/pointer"
    31  )
    32  
    33  // NodeOSDistroIs returns true if the distro is the same as `--node-os-distro`
    34  // the package framework/pod can't import the framework package (see #81245)
    35  // we need to check if the --node-os-distro=windows is set and the framework package
    36  // is the one that's parsing the flags, as a workaround this method is looking for the same flag again
    37  // TODO: replace with `framework.NodeOSDistroIs` when #81245 is complete
    38  func NodeOSDistroIs(distro string) bool {
    39  	var nodeOsDistro *flag.Flag = flag.Lookup("node-os-distro")
    40  	if nodeOsDistro != nil && nodeOsDistro.Value.String() == distro {
    41  		return true
    42  	}
    43  	return false
    44  }
    45  
    46  // GenerateScriptCmd generates the corresponding command lines to execute a command.
    47  func GenerateScriptCmd(command string) []string {
    48  	var commands []string
    49  	commands = []string{"/bin/sh", "-c", command}
    50  	return commands
    51  }
    52  
    53  // GetDefaultTestImage returns the default test image based on OS.
    54  // If the node OS is windows, currently we return Agnhost image for Windows node
    55  // due to the issue of #https://github.com/kubernetes-sigs/windows-testing/pull/35.
    56  // If the node OS is linux, return busybox image
    57  func GetDefaultTestImage() string {
    58  	return imageutils.GetE2EImage(GetDefaultTestImageID())
    59  }
    60  
    61  // GetDefaultTestImageID returns the default test image id based on OS.
    62  // If the node OS is windows, currently we return Agnhost image for Windows node
    63  // due to the issue of #https://github.com/kubernetes-sigs/windows-testing/pull/35.
    64  // If the node OS is linux, return busybox image
    65  func GetDefaultTestImageID() imageutils.ImageID {
    66  	return GetTestImageID(imageutils.BusyBox)
    67  }
    68  
    69  // GetTestImage returns the image name with the given input
    70  // If the Node OS is windows, currently we return Agnhost image for Windows node
    71  // due to the issue of #https://github.com/kubernetes-sigs/windows-testing/pull/35.
    72  func GetTestImage(id imageutils.ImageID) string {
    73  	if NodeOSDistroIs("windows") {
    74  		return imageutils.GetE2EImage(imageutils.Agnhost)
    75  	}
    76  	return imageutils.GetE2EImage(id)
    77  }
    78  
    79  // GetTestImageID returns the image id with the given input
    80  // If the Node OS is windows, currently we return Agnhost image for Windows node
    81  // due to the issue of #https://github.com/kubernetes-sigs/windows-testing/pull/35.
    82  func GetTestImageID(id imageutils.ImageID) imageutils.ImageID {
    83  	if NodeOSDistroIs("windows") {
    84  		return imageutils.Agnhost
    85  	}
    86  	return id
    87  }
    88  
    89  // GetDefaultNonRootUser returns default non root user
    90  // If the Node OS is windows, we return nill due to issue with invalid permissions set on projected volumes
    91  // https://github.com/kubernetes/kubernetes/issues/102849
    92  func GetDefaultNonRootUser() *int64 {
    93  	if NodeOSDistroIs("windows") {
    94  		return nil
    95  	}
    96  	return pointer.Int64(DefaultNonRootUser)
    97  }
    98  
    99  // GeneratePodSecurityContext generates the corresponding pod security context with the given inputs
   100  // If the Node OS is windows, currently we will ignore the inputs and return nil.
   101  // TODO: Will modify it after windows has its own security context
   102  func GeneratePodSecurityContext(fsGroup *int64, seLinuxOptions *v1.SELinuxOptions) *v1.PodSecurityContext {
   103  	if NodeOSDistroIs("windows") {
   104  		return nil
   105  	}
   106  	return &v1.PodSecurityContext{
   107  		FSGroup:        fsGroup,
   108  		SELinuxOptions: seLinuxOptions,
   109  	}
   110  }
   111  
   112  // GenerateContainerSecurityContext generates the corresponding container security context with the given inputs
   113  // If the Node OS is windows, currently we will ignore the inputs and return nil.
   114  // TODO: Will modify it after windows has its own security context
   115  func GenerateContainerSecurityContext(level psaapi.Level) *v1.SecurityContext {
   116  	if NodeOSDistroIs("windows") {
   117  		return nil
   118  	}
   119  
   120  	switch level {
   121  	case psaapi.LevelBaseline:
   122  		return &v1.SecurityContext{
   123  			Privileged: pointer.Bool(false),
   124  		}
   125  	case psaapi.LevelPrivileged:
   126  		return &v1.SecurityContext{
   127  			Privileged: pointer.Bool(true),
   128  		}
   129  	case psaapi.LevelRestricted:
   130  		return GetRestrictedContainerSecurityContext()
   131  	default:
   132  		ginkgo.Fail(fmt.Sprintf("unknown k8s.io/pod-security-admission/policy.Level %q", level))
   133  		panic("not reached")
   134  	}
   135  }
   136  
   137  // GetLinuxLabel returns the default SELinuxLabel based on OS.
   138  // If the node OS is windows, it will return nil
   139  func GetLinuxLabel() *v1.SELinuxOptions {
   140  	if NodeOSDistroIs("windows") {
   141  		return nil
   142  	}
   143  	return &v1.SELinuxOptions{
   144  		Level: "s0:c0,c1"}
   145  }
   146  
   147  // DefaultNonRootUser is the default user ID used for running restricted (non-root) containers.
   148  const DefaultNonRootUser = 1000
   149  
   150  // DefaultNonRootUserName is the default username in Windows used for running restricted (non-root) containers
   151  const DefaultNonRootUserName = "ContainerUser"
   152  
   153  // GetRestrictedPodSecurityContext returns a restricted pod security context.
   154  // This includes setting RunAsUser for convenience, to pass the RunAsNonRoot check.
   155  // Tests that require a specific user ID should override this.
   156  func GetRestrictedPodSecurityContext() *v1.PodSecurityContext {
   157  	psc := &v1.PodSecurityContext{
   158  		RunAsNonRoot:   pointer.Bool(true),
   159  		RunAsUser:      GetDefaultNonRootUser(),
   160  		SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault},
   161  	}
   162  
   163  	if NodeOSDistroIs("windows") {
   164  		psc.WindowsOptions = &v1.WindowsSecurityContextOptions{}
   165  		psc.WindowsOptions.RunAsUserName = pointer.String(DefaultNonRootUserName)
   166  	}
   167  
   168  	return psc
   169  }
   170  
   171  // GetRestrictedContainerSecurityContext returns a minimal restricted container security context.
   172  func GetRestrictedContainerSecurityContext() *v1.SecurityContext {
   173  	return &v1.SecurityContext{
   174  		AllowPrivilegeEscalation: pointer.Bool(false),
   175  		Capabilities:             &v1.Capabilities{Drop: []v1.Capability{"ALL"}},
   176  	}
   177  }
   178  
   179  var psaEvaluator, _ = psapolicy.NewEvaluator(psapolicy.DefaultChecks())
   180  
   181  // MustMixinRestrictedPodSecurity makes the given pod compliant with the restricted pod security level.
   182  // If doing so would overwrite existing non-conformant configuration, a test failure is triggered.
   183  func MustMixinRestrictedPodSecurity(pod *v1.Pod) *v1.Pod {
   184  	err := MixinRestrictedPodSecurity(pod)
   185  	gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred())
   186  	return pod
   187  }
   188  
   189  // MixinRestrictedPodSecurity makes the given pod compliant with the restricted pod security level.
   190  // If doing so would overwrite existing non-conformant configuration, an error is returned.
   191  // Note that this sets a default RunAsUser. See GetRestrictedPodSecurityContext.
   192  // TODO(#105919): Handle PodOS for windows pods.
   193  func MixinRestrictedPodSecurity(pod *v1.Pod) error {
   194  	if pod.Spec.SecurityContext == nil {
   195  		pod.Spec.SecurityContext = GetRestrictedPodSecurityContext()
   196  	} else {
   197  		if pod.Spec.SecurityContext.RunAsNonRoot == nil {
   198  			pod.Spec.SecurityContext.RunAsNonRoot = pointer.Bool(true)
   199  		}
   200  		if pod.Spec.SecurityContext.RunAsUser == nil {
   201  			pod.Spec.SecurityContext.RunAsUser = GetDefaultNonRootUser()
   202  		}
   203  		if pod.Spec.SecurityContext.SeccompProfile == nil {
   204  			pod.Spec.SecurityContext.SeccompProfile = &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}
   205  		}
   206  		if NodeOSDistroIs("windows") && pod.Spec.SecurityContext.WindowsOptions == nil {
   207  			pod.Spec.SecurityContext.WindowsOptions = &v1.WindowsSecurityContextOptions{}
   208  			pod.Spec.SecurityContext.WindowsOptions.RunAsUserName = pointer.String(DefaultNonRootUserName)
   209  		}
   210  	}
   211  	for i := range pod.Spec.Containers {
   212  		mixinRestrictedContainerSecurityContext(&pod.Spec.Containers[i])
   213  	}
   214  	for i := range pod.Spec.InitContainers {
   215  		mixinRestrictedContainerSecurityContext(&pod.Spec.InitContainers[i])
   216  	}
   217  
   218  	// Validate the resulting pod against the restricted profile.
   219  	restricted := psaapi.LevelVersion{
   220  		Level:   psaapi.LevelRestricted,
   221  		Version: psaapi.LatestVersion(),
   222  	}
   223  	if agg := psapolicy.AggregateCheckResults(psaEvaluator.EvaluatePod(restricted, &pod.ObjectMeta, &pod.Spec)); !agg.Allowed {
   224  		return fmt.Errorf("failed to make pod %s restricted: %s", pod.Name, agg.ForbiddenDetail())
   225  	}
   226  
   227  	return nil
   228  }
   229  
   230  // mixinRestrictedContainerSecurityContext adds the required container security context options to
   231  // be compliant with the restricted pod security level. Non-conformance checking is handled by the
   232  // caller.
   233  func mixinRestrictedContainerSecurityContext(container *v1.Container) {
   234  	if container.SecurityContext == nil {
   235  		container.SecurityContext = GetRestrictedContainerSecurityContext()
   236  	} else {
   237  		if container.SecurityContext.AllowPrivilegeEscalation == nil {
   238  			container.SecurityContext.AllowPrivilegeEscalation = pointer.Bool(false)
   239  		}
   240  		if container.SecurityContext.Capabilities == nil {
   241  			container.SecurityContext.Capabilities = &v1.Capabilities{}
   242  		}
   243  		if len(container.SecurityContext.Capabilities.Drop) == 0 {
   244  			container.SecurityContext.Capabilities.Drop = []v1.Capability{"ALL"}
   245  		}
   246  	}
   247  }
   248  
   249  // FindPodConditionByType loops through all pod conditions in pod status and returns the specified condition.
   250  func FindPodConditionByType(podStatus *v1.PodStatus, conditionType v1.PodConditionType) *v1.PodCondition {
   251  	for _, cond := range podStatus.Conditions {
   252  		if cond.Type == conditionType {
   253  			return &cond
   254  		}
   255  	}
   256  	return nil
   257  }
   258  
   259  // FindContainerStatusInPod finds a container status by its name in the provided pod
   260  func FindContainerStatusInPod(pod *v1.Pod, containerName string) *v1.ContainerStatus {
   261  	for _, containerStatus := range pod.Status.InitContainerStatuses {
   262  		if containerStatus.Name == containerName {
   263  			return &containerStatus
   264  		}
   265  	}
   266  	for _, containerStatus := range pod.Status.ContainerStatuses {
   267  		if containerStatus.Name == containerName {
   268  			return &containerStatus
   269  		}
   270  	}
   271  	for _, containerStatus := range pod.Status.EphemeralContainerStatuses {
   272  		if containerStatus.Name == containerName {
   273  			return &containerStatus
   274  		}
   275  	}
   276  	return nil
   277  }