cost-optimization/gke-shift-left-cost/api/types.go (298 lines of code) (raw):

// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package api import ( "fmt" "strings" "github.com/fernandorubbo/k8s-cost-estimator/util" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" coreV1 "k8s.io/api/core/v1" ) const ( // HPAKind just to avoid mispeling HPAKind = "HorizontalPodAutoscaler" // DeploymentKind is just to avoid mispeling DeploymentKind = "Deployment" // ReplicaSetKind is just to avoid mispeling ReplicaSetKind = "ReplicaSet" // StatefulSetKind is just to avoid mispeling StatefulSetKind = "StatefulSet" // DaemonSetKind is just to avoid mispeling DaemonSetKind = "DaemonSet" // VolumeClaimKind is just to avoid mispeling VolumeClaimKind = "PersistentVolumeClaim" ) // SupportedKinds groups all supported kinds var SupportedKinds = []string{HPAKind, DeploymentKind, ReplicaSetKind, StatefulSetKind, DaemonSetKind, VolumeClaimKind} // GroupVersionKind is the reprsentation of k8s type // This object is used to to avoid sprawl of dependent library (eg. apimachinary) across the code // This will allow easy migration to others library (eg. kyaml) in the future once the dependency is all encapulated into k8s_decoder.go type GroupVersionKind struct { Group string Version string Kind string } // HPA is the simplified reprsentation of k8s HPA // Client doesn't need to handle different version and the complexity of k8s.io package type HPA struct { APIVersionKindName string TargetRef string MinReplicas int32 MaxReplicas int32 TargetCPUPercentage int32 } // HorizontalScalableResource is a Horizontal Scalable Resource // Implemented by Deployment, ReplicaSet and StatefulSet type HorizontalScalableResource interface { getContainers() []Container getReplicas() int32 hasHPA() bool getHPA() HPA } // Deployment is the simplified reprsentation of k8s deployment // Client doesn't need to handle different version and the complexity of k8s.io package type Deployment struct { APIVersionKindName string Replicas int32 Containers []Container hpa HPA } func (d *Deployment) estimateCost(rp ResourcePrice) CostRange { return estimateCost(DeploymentKind, d, rp) } func (d *Deployment) getKindName() string { return buildKindName(d.APIVersionKindName) } func (d *Deployment) getContainers() []Container { return d.Containers } func (d *Deployment) getReplicas() int32 { return d.Replicas } func (d *Deployment) hasHPA() bool { return d.hpa.APIVersionKindName != "" } func (d *Deployment) getHPA() HPA { return d.hpa } // ReplicaSet is the simplified reprsentation of k8s replicaset // Client doesn't need to handle different version and the complexity of k8s.io package type ReplicaSet struct { APIVersionKindName string Replicas int32 Containers []Container hpa HPA } func (r *ReplicaSet) estimateCost(rp ResourcePrice) CostRange { return estimateCost(ReplicaSetKind, r, rp) } func (r *ReplicaSet) getKindName() string { return buildKindName(r.APIVersionKindName) } func (r *ReplicaSet) getContainers() []Container { return r.Containers } func (r *ReplicaSet) getReplicas() int32 { return r.Replicas } func (r *ReplicaSet) hasHPA() bool { return r.hpa.APIVersionKindName != "" } func (r *ReplicaSet) getHPA() HPA { return r.hpa } // StatefulSet is the simplified reprsentation of k8s StatefulSet // Client doesn't need to handle different version and the complexity of k8s.io package type StatefulSet struct { APIVersionKindName string Replicas int32 Containers []Container hpa HPA VolumeClaims []*VolumeClaim } func (s *StatefulSet) estimateCost(rp ResourcePrice) CostRange { return estimateCost(StatefulSetKind, s, rp) } func (s *StatefulSet) getKindName() string { return buildKindName(s.APIVersionKindName) } func (s *StatefulSet) getContainers() []Container { return s.Containers } func (s *StatefulSet) getReplicas() int32 { return s.Replicas } func (s *StatefulSet) hasHPA() bool { return s.hpa.APIVersionKindName != "" } func (s *StatefulSet) getHPA() HPA { return s.hpa } // DaemonSet is the simplified reprsentation of k8s DaemonSet // Client doesn't need to handle different version and the complexity of k8s.io package type DaemonSet struct { APIVersionKindName string NodesCount int32 Containers []Container } func (d *DaemonSet) estimateCost(rp ResourcePrice) CostRange { cost := CostRange{Kind: DaemonSetKind} cpuReq, cpuLim, memReq, memLim := totalContainers(d.Containers) var cpuMonthlyPrice = float64(rp.CPUMonthlyPrice()) var memoryMonthlyPrice = float64(rp.MemoryMonthlyPrice()) nodesCount := float64(d.NodesCount) cost.MinRequested = (nodesCount * cpuReq * cpuMonthlyPrice) + (nodesCount * memReq * memoryMonthlyPrice) cost.MaxRequested = cost.MinRequested cost.HPABuffer = cost.MinRequested cost.MinLimited = (nodesCount * cpuLim * cpuMonthlyPrice) + (nodesCount * memLim * memoryMonthlyPrice) cost.MaxLimited = cost.MinLimited return postProcessCost(cost) } // VolumeClaim is the simplified reprsentation of k8s VolumeClaim // Client doesn't need to handle different version and the complexity of k8s.io package type VolumeClaim struct { APIVersionKindName string StorageClass string Requests Resource Limits Resource } func (v *VolumeClaim) estimateCost(sp StoragePrice) CostRange { storageMonthlyPrice := float64(sp.PdStandardMonthlyPrice()) storageClass := v.StorageClass if storageClass != storageClassStandard { log.Infof("Estimation for StorageClass '%s' not implemented for PersistentVolumeClaim. Using standard (GCE Regional Persistent Disk) instead", storageClass) storageMonthlyPrice = float64(sp.PdStandardMonthlyPrice()) } cost := CostRange{Kind: VolumeClaimKind} cost.MinRequested = (float64(v.Requests.Storage) * storageMonthlyPrice) cost.MaxRequested = cost.MinRequested cost.HPABuffer = cost.MinRequested cost.MinLimited = (float64(v.Limits.Storage) * storageMonthlyPrice) cost.MaxLimited = cost.MinLimited return postProcessCost(cost) } // Container is the simplified representation of k8s Container // Client doesn't need to handle different version and the complexity of k8s.io package type Container struct { Requests Resource Limits Resource } // Resource is the simplified reprsentation of k8s Resource // Client doesn't need to handle different version and the complexity of k8s.io package type Resource struct { CPU int64 Memory int64 Storage int64 } // -------- Price Catalog --------- //ResourcePrice interface type ResourcePrice interface { CPUMonthlyPrice() float32 MemoryMonthlyPrice() float32 } //StoragePrice interface type StoragePrice interface { PdStandardMonthlyPrice() float32 } //GCPPriceCatalog implementation to make call to GCP CloudCatalog type GCPPriceCatalog struct { cpuPrice float32 memoryPrice float32 pdStandardPrice float32 } // CPUMonthlyPrice returns the GCP CPU price in USD func (pc *GCPPriceCatalog) CPUMonthlyPrice() float32 { return pc.cpuPrice } // MemoryMonthlyPrice returns the GCP Memory price in USD func (pc *GCPPriceCatalog) MemoryMonthlyPrice() float32 { return pc.memoryPrice } // PdStandardMonthlyPrice returns the GCP Storage PD price in USD func (pc *GCPPriceCatalog) PdStandardMonthlyPrice() float32 { return pc.pdStandardPrice } // --- utility functions --- func buildAPIVersionKindName(apiVersion, kind, ns, name string) string { namespace := "default" if ns != "" { namespace = ns } return fmt.Sprintf("%s|%s|%s|%s", apiVersion, kind, namespace, name) } func buildKindName(apiVersionKindName string) string { index := strings.Index(apiVersionKindName, "|") return apiVersionKindName[index:] } func estimateCost(kind string, r HorizontalScalableResource, rp ResourcePrice) CostRange { cost := CostRange{Kind: kind} cpuReq, cpuLim, memReq, memLim := totalContainers(r.getContainers()) var cpuMonthlyPrice = float64(rp.CPUMonthlyPrice()) var memoryMonthlyPrice = float64(rp.MemoryMonthlyPrice()) if r.hasHPA() { hpa := r.getHPA() targetCPUPercentage := hpa.TargetCPUPercentage minReplicas := float64(hpa.MinReplicas) maxReplicas := float64(hpa.MaxReplicas) cost.MinRequested = (minReplicas * cpuReq * cpuMonthlyPrice) + (minReplicas * memReq * memoryMonthlyPrice) cost.MaxRequested = (maxReplicas * cpuReq * cpuMonthlyPrice) + (maxReplicas * memReq * memoryMonthlyPrice) cpuBuffer := minReplicas if targetCPUPercentage > 0 { buff := float64(100-targetCPUPercentage) / 100 cpuBuffer = minReplicas + (buff * minReplicas) } cost.HPABuffer = (cpuBuffer * cpuReq * cpuMonthlyPrice) + (cpuBuffer * memReq * memoryMonthlyPrice) cost.MinLimited = (minReplicas * cpuLim * cpuMonthlyPrice) + (minReplicas * memLim * memoryMonthlyPrice) cost.MaxLimited = (maxReplicas * cpuLim * cpuMonthlyPrice) + (maxReplicas * memLim * memoryMonthlyPrice) } else { replicas := float64(r.getReplicas()) cost.MinRequested = (replicas * cpuReq * cpuMonthlyPrice) + (replicas * memReq * memoryMonthlyPrice) cost.MaxRequested = cost.MinRequested cost.HPABuffer = cost.MinRequested cost.MinLimited = (replicas * cpuLim * cpuMonthlyPrice) + (replicas * memLim * memoryMonthlyPrice) cost.MaxLimited = cost.MinLimited } return postProcessCost(cost) } func postProcessCost(cost CostRange) CostRange { // just to make sure limit will not be smaller than requested if cost.MinLimited < cost.MinRequested { cost.MinLimited = cost.MinRequested } if cost.MaxLimited < cost.MaxRequested { cost.MaxLimited = cost.MaxRequested } return cost } func buildContainers(cont []coreV1.Container, conf CostimatorConfig) []Container { containers := []Container{} for i := 0; i < len(cont); i++ { requests := cont[i].Resources.Requests requestsCPU := requests[coreV1.ResourceCPU] requestsMemory := requests[coreV1.ResourceMemory] limits := cont[i].Resources.Limits limitsCPU := limits[coreV1.ResourceCPU] limitsMemory := limits[coreV1.ResourceMemory] requestsCPUinMilli := requestsCPU.MilliValue() requestsMemoryinMilli := requestsMemory.Value() limitsCPUinMilli := limitsCPU.MilliValue() limitsMemoryinMilli := limitsMemory.Value() // If Requests is omitted for a container, it defaults to Limits if that is explicitly specified if requestsCPUinMilli == 0 { requestsCPUinMilli = limitsCPUinMilli } if requestsMemoryinMilli == 0 { requestsMemoryinMilli = limitsMemoryinMilli } // otherwise to an config-defined value. if requestsCPUinMilli == 0 { requestsCPUinMilli = conf.ResourceConf.DefaultCPUinMillis } if requestsMemoryinMilli == 0 { requestsMemoryinMilli = conf.ResourceConf.DefaultMemoryinBytes } // Give a percentage increase for umbounded resources if limitsCPUinMilli == 0 { limitsCPUinMilli = requestsCPUinMilli + (conf.ResourceConf.PercentageIncreaseForUnboundedRerouces * requestsCPUinMilli / 100) } if limitsMemoryinMilli == 0 { limitsMemoryinMilli = requestsMemoryinMilli + (conf.ResourceConf.PercentageIncreaseForUnboundedRerouces * requestsMemoryinMilli / 100) } container := Container{ Requests: Resource{ CPU: requestsCPUinMilli, Memory: requestsMemoryinMilli, }, Limits: Resource{ CPU: limitsCPUinMilli, Memory: limitsMemoryinMilli, }, } containers = append(containers, container) } return containers } func totalContainers(containers []Container) (cpuReq float64, cpuLim float64, memReq float64, memLim float64) { for _, container := range containers { cpuReq = cpuReq + float64(container.Requests.CPU) cpuLim = cpuLim + float64(container.Limits.CPU) memReq = memReq + float64(container.Requests.Memory) // bytes memLim = memLim + float64(container.Limits.Memory) // bytes } cpuReq = cpuReq / 1000 // from milis to # of cores cpuLim = cpuLim / 1000 // from milis to # of cores return } func isObjectSupported(data []byte) (string, bool) { ak := struct { APIVersion string `yaml:"apiVersion,omitempty"` Kind string `yaml:"kind,omitempty"` }{} err := yaml.Unmarshal(data, &ak) if err != nil { return fmt.Sprintf("%+v", ak), false } return fmt.Sprintf("%+v", ak), isKindSupported(ak.Kind) } func isKindSupported(kind string) bool { return util.Contains(SupportedKinds, kind) }