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 }