k8s.io/kubernetes@v1.29.3/pkg/api/v1/resource/helpers.go (about)

     1  /*
     2  Copyright 2014 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 resource
    18  
    19  import (
    20  	"fmt"
    21  	"math"
    22  	"strconv"
    23  	"strings"
    24  
    25  	v1 "k8s.io/api/core/v1"
    26  	"k8s.io/apimachinery/pkg/api/resource"
    27  
    28  	podutil "k8s.io/kubernetes/pkg/api/v1/pod"
    29  )
    30  
    31  // PodResourcesOptions controls the behavior of PodRequests and PodLimits.
    32  type PodResourcesOptions struct {
    33  	// Reuse, if provided will be reused to accumulate resources and returned by the PodRequests or PodLimits
    34  	// functions. All existing values in Reuse will be lost.
    35  	Reuse v1.ResourceList
    36  	// InPlacePodVerticalScalingEnabled indicates that the in-place pod vertical scaling feature gate is enabled.
    37  	InPlacePodVerticalScalingEnabled bool
    38  	// ExcludeOverhead controls if pod overhead is excluded from the calculation.
    39  	ExcludeOverhead bool
    40  	// ContainerFn is called with the effective resources required for each container within the pod.
    41  	ContainerFn func(res v1.ResourceList, containerType podutil.ContainerType)
    42  	// NonMissingContainerRequests if provided will replace any missing container level requests for the specified resources
    43  	// with the given values.  If the requests for those resources are explicitly set, even if zero, they will not be modified.
    44  	NonMissingContainerRequests v1.ResourceList
    45  }
    46  
    47  // PodRequests computes the pod requests per the PodResourcesOptions supplied. If PodResourcesOptions is nil, then
    48  // the requests are returned including pod overhead. The computation is part of the API and must be reviewed
    49  // as an API change.
    50  func PodRequests(pod *v1.Pod, opts PodResourcesOptions) v1.ResourceList {
    51  	// attempt to reuse the maps if passed, or allocate otherwise
    52  	reqs := reuseOrClearResourceList(opts.Reuse)
    53  
    54  	var containerStatuses map[string]*v1.ContainerStatus
    55  	if opts.InPlacePodVerticalScalingEnabled {
    56  		containerStatuses = make(map[string]*v1.ContainerStatus, len(pod.Status.ContainerStatuses))
    57  		for i := range pod.Status.ContainerStatuses {
    58  			containerStatuses[pod.Status.ContainerStatuses[i].Name] = &pod.Status.ContainerStatuses[i]
    59  		}
    60  	}
    61  
    62  	for _, container := range pod.Spec.Containers {
    63  		containerReqs := container.Resources.Requests
    64  		if opts.InPlacePodVerticalScalingEnabled {
    65  			cs, found := containerStatuses[container.Name]
    66  			if found {
    67  				if pod.Status.Resize == v1.PodResizeStatusInfeasible {
    68  					containerReqs = cs.AllocatedResources.DeepCopy()
    69  				} else {
    70  					containerReqs = max(container.Resources.Requests, cs.AllocatedResources)
    71  				}
    72  			}
    73  		}
    74  
    75  		if len(opts.NonMissingContainerRequests) > 0 {
    76  			containerReqs = applyNonMissing(containerReqs, opts.NonMissingContainerRequests)
    77  		}
    78  
    79  		if opts.ContainerFn != nil {
    80  			opts.ContainerFn(containerReqs, podutil.Containers)
    81  		}
    82  
    83  		addResourceList(reqs, containerReqs)
    84  	}
    85  
    86  	restartableInitContainerReqs := v1.ResourceList{}
    87  	initContainerReqs := v1.ResourceList{}
    88  	// init containers define the minimum of any resource
    89  	// Note: In-place resize is not allowed for InitContainers, so no need to check for ResizeStatus value
    90  	//
    91  	// Let's say `InitContainerUse(i)` is the resource requirements when the i-th
    92  	// init container is initializing, then
    93  	// `InitContainerUse(i) = sum(Resources of restartable init containers with index < i) + Resources of i-th init container`.
    94  	//
    95  	// See https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/753-sidecar-containers#exposing-pod-resource-requirements for the detail.
    96  	for _, container := range pod.Spec.InitContainers {
    97  		containerReqs := container.Resources.Requests
    98  		if len(opts.NonMissingContainerRequests) > 0 {
    99  			containerReqs = applyNonMissing(containerReqs, opts.NonMissingContainerRequests)
   100  		}
   101  
   102  		if container.RestartPolicy != nil && *container.RestartPolicy == v1.ContainerRestartPolicyAlways {
   103  			// and add them to the resulting cumulative container requests
   104  			addResourceList(reqs, containerReqs)
   105  
   106  			// track our cumulative restartable init container resources
   107  			addResourceList(restartableInitContainerReqs, containerReqs)
   108  			containerReqs = restartableInitContainerReqs
   109  		} else {
   110  			tmp := v1.ResourceList{}
   111  			addResourceList(tmp, containerReqs)
   112  			addResourceList(tmp, restartableInitContainerReqs)
   113  			containerReqs = tmp
   114  		}
   115  
   116  		if opts.ContainerFn != nil {
   117  			opts.ContainerFn(containerReqs, podutil.InitContainers)
   118  		}
   119  		maxResourceList(initContainerReqs, containerReqs)
   120  	}
   121  
   122  	maxResourceList(reqs, initContainerReqs)
   123  
   124  	// Add overhead for running a pod to the sum of requests if requested:
   125  	if !opts.ExcludeOverhead && pod.Spec.Overhead != nil {
   126  		addResourceList(reqs, pod.Spec.Overhead)
   127  	}
   128  
   129  	return reqs
   130  }
   131  
   132  // applyNonMissing will return a copy of the given resource list with any missing values replaced by the nonMissing values
   133  func applyNonMissing(reqs v1.ResourceList, nonMissing v1.ResourceList) v1.ResourceList {
   134  	cp := v1.ResourceList{}
   135  	for k, v := range reqs {
   136  		cp[k] = v.DeepCopy()
   137  	}
   138  
   139  	for k, v := range nonMissing {
   140  		if _, found := reqs[k]; !found {
   141  			rk := cp[k]
   142  			rk.Add(v)
   143  			cp[k] = rk
   144  		}
   145  	}
   146  	return cp
   147  }
   148  
   149  // PodLimits computes the pod limits per the PodResourcesOptions supplied. If PodResourcesOptions is nil, then
   150  // the limits are returned including pod overhead for any non-zero limits. The computation is part of the API and must be reviewed
   151  // as an API change.
   152  func PodLimits(pod *v1.Pod, opts PodResourcesOptions) v1.ResourceList {
   153  	// attempt to reuse the maps if passed, or allocate otherwise
   154  	limits := reuseOrClearResourceList(opts.Reuse)
   155  
   156  	for _, container := range pod.Spec.Containers {
   157  		if opts.ContainerFn != nil {
   158  			opts.ContainerFn(container.Resources.Limits, podutil.Containers)
   159  		}
   160  		addResourceList(limits, container.Resources.Limits)
   161  	}
   162  
   163  	restartableInitContainerLimits := v1.ResourceList{}
   164  	initContainerLimits := v1.ResourceList{}
   165  	// init containers define the minimum of any resource
   166  	//
   167  	// Let's say `InitContainerUse(i)` is the resource requirements when the i-th
   168  	// init container is initializing, then
   169  	// `InitContainerUse(i) = sum(Resources of restartable init containers with index < i) + Resources of i-th init container`.
   170  	//
   171  	// See https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/753-sidecar-containers#exposing-pod-resource-requirements for the detail.
   172  	for _, container := range pod.Spec.InitContainers {
   173  		containerLimits := container.Resources.Limits
   174  		// Is the init container marked as a restartable init container?
   175  		if container.RestartPolicy != nil && *container.RestartPolicy == v1.ContainerRestartPolicyAlways {
   176  			addResourceList(limits, containerLimits)
   177  
   178  			// track our cumulative restartable init container resources
   179  			addResourceList(restartableInitContainerLimits, containerLimits)
   180  			containerLimits = restartableInitContainerLimits
   181  		} else {
   182  			tmp := v1.ResourceList{}
   183  			addResourceList(tmp, containerLimits)
   184  			addResourceList(tmp, restartableInitContainerLimits)
   185  			containerLimits = tmp
   186  		}
   187  
   188  		if opts.ContainerFn != nil {
   189  			opts.ContainerFn(containerLimits, podutil.InitContainers)
   190  		}
   191  		maxResourceList(initContainerLimits, containerLimits)
   192  	}
   193  
   194  	maxResourceList(limits, initContainerLimits)
   195  
   196  	// Add overhead to non-zero limits if requested:
   197  	if !opts.ExcludeOverhead && pod.Spec.Overhead != nil {
   198  		for name, quantity := range pod.Spec.Overhead {
   199  			if value, ok := limits[name]; ok && !value.IsZero() {
   200  				value.Add(quantity)
   201  				limits[name] = value
   202  			}
   203  		}
   204  	}
   205  
   206  	return limits
   207  }
   208  
   209  // addResourceList adds the resources in newList to list.
   210  func addResourceList(list, newList v1.ResourceList) {
   211  	for name, quantity := range newList {
   212  		if value, ok := list[name]; !ok {
   213  			list[name] = quantity.DeepCopy()
   214  		} else {
   215  			value.Add(quantity)
   216  			list[name] = value
   217  		}
   218  	}
   219  }
   220  
   221  // maxResourceList sets list to the greater of list/newList for every resource in newList
   222  func maxResourceList(list, newList v1.ResourceList) {
   223  	for name, quantity := range newList {
   224  		if value, ok := list[name]; !ok || quantity.Cmp(value) > 0 {
   225  			list[name] = quantity.DeepCopy()
   226  		}
   227  	}
   228  }
   229  
   230  // max returns the result of max(a, b) for each named resource and is only used if we can't
   231  // accumulate into an existing resource list
   232  func max(a v1.ResourceList, b v1.ResourceList) v1.ResourceList {
   233  	result := v1.ResourceList{}
   234  	for key, value := range a {
   235  		if other, found := b[key]; found {
   236  			if value.Cmp(other) <= 0 {
   237  				result[key] = other.DeepCopy()
   238  				continue
   239  			}
   240  		}
   241  		result[key] = value.DeepCopy()
   242  	}
   243  	for key, value := range b {
   244  		if _, found := result[key]; !found {
   245  			result[key] = value.DeepCopy()
   246  		}
   247  	}
   248  	return result
   249  }
   250  
   251  // reuseOrClearResourceList is a helper for avoiding excessive allocations of
   252  // resource lists within the inner loop of resource calculations.
   253  func reuseOrClearResourceList(reuse v1.ResourceList) v1.ResourceList {
   254  	if reuse == nil {
   255  		return make(v1.ResourceList, 4)
   256  	}
   257  	for k := range reuse {
   258  		delete(reuse, k)
   259  	}
   260  	return reuse
   261  }
   262  
   263  // GetResourceRequestQuantity finds and returns the request quantity for a specific resource.
   264  func GetResourceRequestQuantity(pod *v1.Pod, resourceName v1.ResourceName) resource.Quantity {
   265  	requestQuantity := resource.Quantity{}
   266  
   267  	switch resourceName {
   268  	case v1.ResourceCPU:
   269  		requestQuantity = resource.Quantity{Format: resource.DecimalSI}
   270  	case v1.ResourceMemory, v1.ResourceStorage, v1.ResourceEphemeralStorage:
   271  		requestQuantity = resource.Quantity{Format: resource.BinarySI}
   272  	default:
   273  		requestQuantity = resource.Quantity{Format: resource.DecimalSI}
   274  	}
   275  
   276  	for _, container := range pod.Spec.Containers {
   277  		if rQuantity, ok := container.Resources.Requests[resourceName]; ok {
   278  			requestQuantity.Add(rQuantity)
   279  		}
   280  	}
   281  
   282  	for _, container := range pod.Spec.InitContainers {
   283  		if rQuantity, ok := container.Resources.Requests[resourceName]; ok {
   284  			if requestQuantity.Cmp(rQuantity) < 0 {
   285  				requestQuantity = rQuantity.DeepCopy()
   286  			}
   287  		}
   288  	}
   289  
   290  	// Add overhead for running a pod
   291  	// to the total requests if the resource total is non-zero
   292  	if pod.Spec.Overhead != nil {
   293  		if podOverhead, ok := pod.Spec.Overhead[resourceName]; ok && !requestQuantity.IsZero() {
   294  			requestQuantity.Add(podOverhead)
   295  		}
   296  	}
   297  
   298  	return requestQuantity
   299  }
   300  
   301  // GetResourceRequest finds and returns the request value for a specific resource.
   302  func GetResourceRequest(pod *v1.Pod, resource v1.ResourceName) int64 {
   303  	if resource == v1.ResourcePods {
   304  		return 1
   305  	}
   306  
   307  	requestQuantity := GetResourceRequestQuantity(pod, resource)
   308  
   309  	if resource == v1.ResourceCPU {
   310  		return requestQuantity.MilliValue()
   311  	}
   312  
   313  	return requestQuantity.Value()
   314  }
   315  
   316  // ExtractResourceValueByContainerName extracts the value of a resource
   317  // by providing container name
   318  func ExtractResourceValueByContainerName(fs *v1.ResourceFieldSelector, pod *v1.Pod, containerName string) (string, error) {
   319  	container, err := findContainerInPod(pod, containerName)
   320  	if err != nil {
   321  		return "", err
   322  	}
   323  	return ExtractContainerResourceValue(fs, container)
   324  }
   325  
   326  // ExtractResourceValueByContainerNameAndNodeAllocatable extracts the value of a resource
   327  // by providing container name and node allocatable
   328  func ExtractResourceValueByContainerNameAndNodeAllocatable(fs *v1.ResourceFieldSelector, pod *v1.Pod, containerName string, nodeAllocatable v1.ResourceList) (string, error) {
   329  	realContainer, err := findContainerInPod(pod, containerName)
   330  	if err != nil {
   331  		return "", err
   332  	}
   333  
   334  	container := realContainer.DeepCopy()
   335  
   336  	MergeContainerResourceLimits(container, nodeAllocatable)
   337  
   338  	return ExtractContainerResourceValue(fs, container)
   339  }
   340  
   341  // ExtractContainerResourceValue extracts the value of a resource
   342  // in an already known container
   343  func ExtractContainerResourceValue(fs *v1.ResourceFieldSelector, container *v1.Container) (string, error) {
   344  	divisor := resource.Quantity{}
   345  	if divisor.Cmp(fs.Divisor) == 0 {
   346  		divisor = resource.MustParse("1")
   347  	} else {
   348  		divisor = fs.Divisor
   349  	}
   350  
   351  	switch fs.Resource {
   352  	case "limits.cpu":
   353  		return convertResourceCPUToString(container.Resources.Limits.Cpu(), divisor)
   354  	case "limits.memory":
   355  		return convertResourceMemoryToString(container.Resources.Limits.Memory(), divisor)
   356  	case "limits.ephemeral-storage":
   357  		return convertResourceEphemeralStorageToString(container.Resources.Limits.StorageEphemeral(), divisor)
   358  	case "requests.cpu":
   359  		return convertResourceCPUToString(container.Resources.Requests.Cpu(), divisor)
   360  	case "requests.memory":
   361  		return convertResourceMemoryToString(container.Resources.Requests.Memory(), divisor)
   362  	case "requests.ephemeral-storage":
   363  		return convertResourceEphemeralStorageToString(container.Resources.Requests.StorageEphemeral(), divisor)
   364  	}
   365  	// handle extended standard resources with dynamic names
   366  	// example: requests.hugepages-<pageSize> or limits.hugepages-<pageSize>
   367  	if strings.HasPrefix(fs.Resource, "requests.") {
   368  		resourceName := v1.ResourceName(strings.TrimPrefix(fs.Resource, "requests."))
   369  		if IsHugePageResourceName(resourceName) {
   370  			return convertResourceHugePagesToString(container.Resources.Requests.Name(resourceName, resource.BinarySI), divisor)
   371  		}
   372  	}
   373  	if strings.HasPrefix(fs.Resource, "limits.") {
   374  		resourceName := v1.ResourceName(strings.TrimPrefix(fs.Resource, "limits."))
   375  		if IsHugePageResourceName(resourceName) {
   376  			return convertResourceHugePagesToString(container.Resources.Limits.Name(resourceName, resource.BinarySI), divisor)
   377  		}
   378  	}
   379  	return "", fmt.Errorf("unsupported container resource : %v", fs.Resource)
   380  }
   381  
   382  // convertResourceCPUToString converts cpu value to the format of divisor and returns
   383  // ceiling of the value.
   384  func convertResourceCPUToString(cpu *resource.Quantity, divisor resource.Quantity) (string, error) {
   385  	c := int64(math.Ceil(float64(cpu.MilliValue()) / float64(divisor.MilliValue())))
   386  	return strconv.FormatInt(c, 10), nil
   387  }
   388  
   389  // convertResourceMemoryToString converts memory value to the format of divisor and returns
   390  // ceiling of the value.
   391  func convertResourceMemoryToString(memory *resource.Quantity, divisor resource.Quantity) (string, error) {
   392  	m := int64(math.Ceil(float64(memory.Value()) / float64(divisor.Value())))
   393  	return strconv.FormatInt(m, 10), nil
   394  }
   395  
   396  // convertResourceHugePagesToString converts hugepages value to the format of divisor and returns
   397  // ceiling of the value.
   398  func convertResourceHugePagesToString(hugePages *resource.Quantity, divisor resource.Quantity) (string, error) {
   399  	m := int64(math.Ceil(float64(hugePages.Value()) / float64(divisor.Value())))
   400  	return strconv.FormatInt(m, 10), nil
   401  }
   402  
   403  // convertResourceEphemeralStorageToString converts ephemeral storage value to the format of divisor and returns
   404  // ceiling of the value.
   405  func convertResourceEphemeralStorageToString(ephemeralStorage *resource.Quantity, divisor resource.Quantity) (string, error) {
   406  	m := int64(math.Ceil(float64(ephemeralStorage.Value()) / float64(divisor.Value())))
   407  	return strconv.FormatInt(m, 10), nil
   408  }
   409  
   410  // findContainerInPod finds a container by its name in the provided pod
   411  func findContainerInPod(pod *v1.Pod, containerName string) (*v1.Container, error) {
   412  	for _, container := range pod.Spec.Containers {
   413  		if container.Name == containerName {
   414  			return &container, nil
   415  		}
   416  	}
   417  	for _, container := range pod.Spec.InitContainers {
   418  		if container.Name == containerName {
   419  			return &container, nil
   420  		}
   421  	}
   422  	return nil, fmt.Errorf("container %s not found", containerName)
   423  }
   424  
   425  // MergeContainerResourceLimits checks if a limit is applied for
   426  // the container, and if not, it sets the limit to the passed resource list.
   427  func MergeContainerResourceLimits(container *v1.Container,
   428  	allocatable v1.ResourceList) {
   429  	if container.Resources.Limits == nil {
   430  		container.Resources.Limits = make(v1.ResourceList)
   431  	}
   432  	// NOTE: we exclude hugepages-* resources because hugepages are never overcommitted.
   433  	// This means that the container always has a limit specified.
   434  	for _, resource := range []v1.ResourceName{v1.ResourceCPU, v1.ResourceMemory, v1.ResourceEphemeralStorage} {
   435  		if quantity, exists := container.Resources.Limits[resource]; !exists || quantity.IsZero() {
   436  			if cap, exists := allocatable[resource]; exists {
   437  				container.Resources.Limits[resource] = cap.DeepCopy()
   438  			}
   439  		}
   440  	}
   441  }
   442  
   443  // IsHugePageResourceName returns true if the resource name has the huge page
   444  // resource prefix.
   445  func IsHugePageResourceName(name v1.ResourceName) bool {
   446  	return strings.HasPrefix(string(name), v1.ResourceHugePagesPrefix)
   447  }