pkg/apis/common/v1alpha1/resources.go (222 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package v1alpha1
import (
"fmt"
"math"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/resource"
)
const (
// GiB - 1 GibiByte
GiB = int64(1024 * 1024 * 1024)
// GB - 1 Gigabyte
GB = int64(1000 * 1000 * 1000)
)
// NodeSetsResources models for all the nodeSets managed by a same autoscaling policy:
// * the desired resources quantities (cpu, memory, storage) expected in the nodeSet specifications
// * the individual number of nodes (count) in each nodeSet
// +kubebuilder:object:generate=false
type NodeSetsResources struct {
// Name is the name of the autoscaling policy to witch this resources belong to.
Name string `json:"name"`
// NodeSetNodeCount holds the number of nodes for each nodeSet.
NodeSetNodeCount NodeSetNodeCountList `json:"nodeSets"`
// NodeResources holds the resource values common to all the nodeSet managed by a same autoscaling policy.
NodeResources
}
// NewNodeSetsResources initialize an empty NodeSetsResources for a given set of NodeSets.
func NewNodeSetsResources(name string, nodeSetNames []string) NodeSetsResources {
return NodeSetsResources{
Name: name,
NodeSetNodeCount: newNodeSetNodeCountList(nodeSetNames),
}
}
// ClusterResources models the desired resources (CPU, memory, storage and number of nodes) for all the autoscaling policies in a cluster.
// +kubebuilder:object:generate=false
type ClusterResources []NodeSetsResources
// NodeResources holds the resources to be used by each node managed by an autoscaling policy.
// All the nodes managed by an autoscaling policy have the same resources, even if they are in different NodeSets.
type NodeResources struct {
Limits corev1.ResourceList `json:"limits,omitempty"`
Requests corev1.ResourceList `json:"requests,omitempty"`
}
// NodeSetNodeCount models the number of nodes expected in a given NodeSet.
type NodeSetNodeCount struct {
// Name of the Nodeset.
Name string `json:"name"`
// NodeCount is the number of nodes, as computed by the autoscaler, expected in this NodeSet.
NodeCount int32 `json:"nodeCount"`
}
type NodeSetNodeCountList []NodeSetNodeCount
// TotalNodeCount returns the total number of nodes.
func (n NodeSetNodeCountList) TotalNodeCount() int32 {
var totalNodeCount int32
for _, nodeSet := range n {
totalNodeCount += nodeSet.NodeCount
}
return totalNodeCount
}
func (n NodeSetNodeCountList) ByNodeSet() map[string]int32 {
byNodeSet := make(map[string]int32)
for _, nodeSet := range n {
byNodeSet[nodeSet.Name] = nodeSet.NodeCount
}
return byNodeSet
}
func newNodeSetNodeCountList(nodeSetNames []string) NodeSetNodeCountList {
nodeSetNodeCount := make([]NodeSetNodeCount, len(nodeSetNames))
for i := range nodeSetNames {
nodeSetNodeCount[i] = NodeSetNodeCount{Name: nodeSetNames[i]}
}
return nodeSetNodeCount
}
// ToContainerResourcesWith builds new ResourceRequirements for Memory and CPU, overriding existing values from the provided
// ResourceRequirements with the values from the NodeResources.
// If there is no recommendations for a given resource, then its current value remains unchanged.
// Values for extended resources (e.g. GPU), are left untouched.
// This function has no side effect and does not modify the original ResourceRequirements.
func (nr *NodeResources) ToContainerResourcesWith(sourceRequirements corev1.ResourceRequirements) corev1.ResourceRequirements {
mergedResources := sourceRequirements.DeepCopy()
// Update requests
if nr.Requests != nil && mergedResources.Requests == nil {
mergedResources.Requests = corev1.ResourceList{}
}
for _, resourceName := range []corev1.ResourceName{corev1.ResourceCPU, corev1.ResourceMemory} {
if nr.HasRequest(resourceName) {
mergedResources.Requests[resourceName] = nr.GetRequest(resourceName)
}
}
// Update Limits
if nr.Limits != nil && mergedResources.Limits == nil {
mergedResources.Limits = corev1.ResourceList{}
}
for _, resourceName := range []corev1.ResourceName{corev1.ResourceCPU, corev1.ResourceMemory} {
if nr.HasLimit(resourceName) {
mergedResources.Limits[resourceName] = nr.GetLimit(resourceName)
}
}
return *mergedResources
}
// MaxMerge merges the specified resource into the NodeResources only if its quantity is greater than the existing one.
func (nr *NodeResources) MaxMerge(
other corev1.ResourceRequirements,
resourceName corev1.ResourceName,
) {
// Requests
otherResourceRequestValue, otherHasResourceRequest := other.Requests[resourceName]
if otherHasResourceRequest {
if nr.Requests == nil {
nr.Requests = make(corev1.ResourceList)
}
receiverValue, receiverHasResource := nr.Requests[resourceName]
if !receiverHasResource {
nr.Requests[resourceName] = otherResourceRequestValue
} else if otherResourceRequestValue.Cmp(receiverValue) > 0 {
nr.Requests[resourceName] = otherResourceRequestValue
}
}
// Limits
otherResourceLimitValue, otherHasResourceLimit := other.Limits[resourceName]
if otherHasResourceLimit {
if nr.Limits == nil {
nr.Limits = make(corev1.ResourceList)
}
receiverValue, receiverHasResource := nr.Limits[resourceName]
if !receiverHasResource {
nr.Limits[resourceName] = otherResourceLimitValue
} else if otherResourceLimitValue.Cmp(receiverValue) > 0 {
nr.Limits[resourceName] = otherResourceLimitValue
}
}
}
func (nr *NodeResources) SetRequest(resourceName corev1.ResourceName, quantity resource.Quantity) {
if nr.Requests == nil {
nr.Requests = make(corev1.ResourceList)
}
nr.Requests[resourceName] = quantity
}
func (nr *NodeResources) SetLimit(resourceName corev1.ResourceName, quantity resource.Quantity) {
if nr.Limits == nil {
nr.Limits = make(corev1.ResourceList)
}
nr.Limits[resourceName] = quantity
}
func (nr *NodeResources) HasRequest(resourceName corev1.ResourceName) bool {
if nr.Requests == nil {
return false
}
_, hasRequest := nr.Requests[resourceName]
return hasRequest
}
func (nr *NodeResources) GetRequest(resourceName corev1.ResourceName) resource.Quantity {
return nr.Requests[resourceName]
}
func (nr *NodeResources) HasLimit(resourceName corev1.ResourceName) bool {
if nr.Limits == nil {
return false
}
_, hasLimit := nr.Limits[resourceName]
return hasLimit
}
func (nr *NodeResources) GetLimit(resourceName corev1.ResourceName) resource.Quantity {
return nr.Limits[resourceName]
}
// UpdateLimits updates the limits in nodesets resources according to the resource requests and the ratio set by the user.
func (nr NodeResources) UpdateLimits(autoscalingResources AutoscalingResources) NodeResources {
if nr.HasRequest(corev1.ResourceMemory) {
// Update Memory limit
if autoscalingResources.MemoryRequestsToLimitsRatio() > 0 {
request := nr.GetRequest(corev1.ResourceMemory)
limit := int64(math.Ceil(float64(request.Value()) * autoscalingResources.MemoryRequestsToLimitsRatio()))
nr.SetLimit(corev1.ResourceMemory, ResourceToQuantity(limit))
}
}
if nr.HasRequest(corev1.ResourceCPU) {
// Update CPU limit
if autoscalingResources.CPURequestsToLimitsRatio() > 0 {
request := nr.GetRequest(corev1.ResourceCPU)
limit := int64(math.Ceil(float64(request.Value()) * autoscalingResources.CPURequestsToLimitsRatio()))
nr.SetLimit(corev1.ResourceCPU, ResourceToQuantity(limit))
}
}
return nr
}
// ResourceToQuantity attempts to convert a raw integer value into a human readable quantity.
func ResourceToQuantity(nodeResource int64) resource.Quantity {
switch {
case nodeResource >= GiB && nodeResource%GiB == 0:
// When it's possible we may want to express the memory with a "human readable unit" like the Gi unit
return resource.MustParse(fmt.Sprintf("%dGi", nodeResource/GiB))
case nodeResource >= GB && nodeResource%GB == 0:
// Same for gigabytes unit
return resource.MustParse(fmt.Sprintf("%dG", nodeResource/GB))
}
return resource.NewQuantity(nodeResource, resource.DecimalSI).DeepCopy()
}
// ResourceListInt64 is a set of (resource name, quantity) pairs.
type ResourceListInt64 map[corev1.ResourceName]int64
// NodeResourcesInt64 is mostly use in logs to print comparable values which can be used in dashboards.
type NodeResourcesInt64 struct {
Requests ResourceListInt64 `json:"requests,omitempty"`
Limits ResourceListInt64 `json:"limits,omitempty"`
}
// ToInt64 converts all the resource quantities to int64, mostly to be logged and to build dashboards.
func (nr NodeResources) ToInt64() NodeResourcesInt64 {
rs64 := NodeResourcesInt64{
Requests: make(ResourceListInt64),
Limits: make(ResourceListInt64),
}
for res, value := range nr.Requests {
switch res {
case corev1.ResourceCPU:
rs64.Requests[res] = value.MilliValue()
default:
rs64.Requests[res] = value.Value()
}
}
for res, value := range nr.Limits {
switch res {
case corev1.ResourceCPU:
rs64.Limits[res] = value.MilliValue()
default:
rs64.Limits[res] = value.Value()
}
}
return rs64
}
// +kubebuilder:object:generate=false
type NodeSetResources struct {
NodeCount int32
*NodeSetsResources
}
// SameResources compares the resources allocated to 2 set of node sets in an autoscaling policy and returns true
// if they are equal.
func (ntr NodeSetsResources) SameResources(other NodeSetsResources) bool {
thisByName := ntr.NodeSetNodeCount.ByNodeSet()
otherByName := other.NodeSetNodeCount.ByNodeSet()
if len(thisByName) != len(otherByName) {
return false
}
for nodeSet, nodeCount := range thisByName {
otherNodeCount, ok := otherByName[nodeSet]
if !ok || nodeCount != otherNodeCount {
return false
}
}
return equality.Semantic.DeepEqual(ntr.NodeResources, other.NodeResources)
}
func (cr ClusterResources) ByNodeSet() map[string]NodeSetResources {
byNodeSet := make(map[string]NodeSetResources)
for i := range cr {
nodeSetsResource := cr[i]
for j := range nodeSetsResource.NodeSetNodeCount {
nodeSetNodeCount := nodeSetsResource.NodeSetNodeCount[j]
nodeSetResources := NodeSetResources{
NodeCount: nodeSetNodeCount.NodeCount,
NodeSetsResources: &nodeSetsResource,
}
byNodeSet[nodeSetNodeCount.Name] = nodeSetResources
}
}
return byNodeSet
}