allocation.go 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110
  1. package costmodel
  2. import (
  3. "fmt"
  4. "math"
  5. "strconv"
  6. "strings"
  7. "time"
  8. "github.com/kubecost/cost-model/pkg/util/timeutil"
  9. "github.com/kubecost/cost-model/pkg/cloud"
  10. "github.com/kubecost/cost-model/pkg/env"
  11. "github.com/kubecost/cost-model/pkg/kubecost"
  12. "github.com/kubecost/cost-model/pkg/log"
  13. "github.com/kubecost/cost-model/pkg/prom"
  14. "k8s.io/apimachinery/pkg/labels"
  15. )
  16. const (
  17. queryFmtPods = `avg(kube_pod_container_status_running{}) by (pod, namespace, %s)[%s:%s]%s`
  18. queryFmtRAMBytesAllocated = `avg(avg_over_time(container_memory_allocation_bytes{container!="", container!="POD", node!=""}[%s]%s)) by (container, pod, namespace, node, %s, provider_id)`
  19. queryFmtRAMRequests = `avg(avg_over_time(kube_pod_container_resource_requests{resource="memory", unit="byte", container!="", container!="POD", node!=""}[%s]%s)) by (container, pod, namespace, node, %s)`
  20. queryFmtRAMUsageAvg = `avg(avg_over_time(container_memory_working_set_bytes{container!="", container_name!="POD", container!="POD"}[%s]%s)) by (container_name, container, pod_name, pod, namespace, instance, %s)`
  21. queryFmtRAMUsageMax = `max(max_over_time(container_memory_working_set_bytes{container!="", container_name!="POD", container!="POD"}[%s]%s)) by (container_name, container, pod_name, pod, namespace, instance, %s)`
  22. queryFmtCPUCoresAllocated = `avg(avg_over_time(container_cpu_allocation{container!="", container!="POD", node!=""}[%s]%s)) by (container, pod, namespace, node, %s)`
  23. queryFmtCPURequests = `avg(avg_over_time(kube_pod_container_resource_requests{resource="cpu", unit="core", container!="", container!="POD", node!=""}[%s]%s)) by (container, pod, namespace, node, %s)`
  24. queryFmtCPUUsageAvg = `avg(rate(container_cpu_usage_seconds_total{container!="", container_name!="POD", container!="POD"}[%s]%s)) by (container_name, container, pod_name, pod, namespace, instance, %s)`
  25. // This query could be written without the recording rule
  26. // "kubecost_savings_container_cpu_usage_seconds", but we should
  27. // only do that when we're ready to incur the performance tradeoffs
  28. // with subqueries which would probably be in the world of hourly
  29. // ETL.
  30. //
  31. // See PromQL subquery documentation for a rate example:
  32. // https://prometheus.io/blog/2019/01/28/subquery-support/#examples
  33. queryFmtCPUUsageMax = `max(max_over_time(kubecost_savings_container_cpu_usage_seconds[%s]%s)) by (container_name, pod_name, namespace, instance, %s)`
  34. queryFmtGPUsRequested = `avg(avg_over_time(kube_pod_container_resource_requests{resource="nvidia_com_gpu", container!="",container!="POD", node!=""}[%s]%s)) by (container, pod, namespace, node, %s)`
  35. queryFmtGPUsAllocated = `avg(avg_over_time(container_gpu_allocation{container!="", container!="POD", node!=""}[%s]%s)) by (container, pod, namespace, node, %s)`
  36. queryFmtNodeCostPerCPUHr = `avg(avg_over_time(node_cpu_hourly_cost[%s]%s)) by (node, %s, instance_type, provider_id)`
  37. queryFmtNodeCostPerRAMGiBHr = `avg(avg_over_time(node_ram_hourly_cost[%s]%s)) by (node, %s, instance_type, provider_id)`
  38. queryFmtNodeCostPerGPUHr = `avg(avg_over_time(node_gpu_hourly_cost[%s]%s)) by (node, %s, instance_type, provider_id)`
  39. queryFmtNodeIsSpot = `avg_over_time(kubecost_node_is_spot[%s]%s)`
  40. queryFmtPVCInfo = `avg(kube_persistentvolumeclaim_info{volumename != ""}) by (persistentvolumeclaim, storageclass, volumename, namespace, %s)[%s:%s]%s`
  41. queryFmtPVBytes = `avg(avg_over_time(kube_persistentvolume_capacity_bytes[%s]%s)) by (persistentvolume, %s)`
  42. queryFmtPodPVCAllocation = `avg(avg_over_time(pod_pvc_allocation[%s]%s)) by (persistentvolume, persistentvolumeclaim, pod, namespace, %s)`
  43. queryFmtPVCBytesRequested = `avg(avg_over_time(kube_persistentvolumeclaim_resource_requests_storage_bytes{}[%s]%s)) by (persistentvolumeclaim, namespace, %s)`
  44. queryFmtPVCostPerGiBHour = `avg(avg_over_time(pv_hourly_cost[%s]%s)) by (volumename, %s)`
  45. queryFmtNetZoneGiB = `sum(increase(kubecost_pod_network_egress_bytes_total{internet="false", sameZone="false", sameRegion="true"}[%s]%s)) by (pod_name, namespace, %s) / 1024 / 1024 / 1024`
  46. queryFmtNetZoneCostPerGiB = `avg(avg_over_time(kubecost_network_zone_egress_cost{}[%s]%s)) by (%s)`
  47. queryFmtNetRegionGiB = `sum(increase(kubecost_pod_network_egress_bytes_total{internet="false", sameZone="false", sameRegion="false"}[%s]%s)) by (pod_name, namespace, %s) / 1024 / 1024 / 1024`
  48. queryFmtNetRegionCostPerGiB = `avg(avg_over_time(kubecost_network_region_egress_cost{}[%s]%s)) by (%s)`
  49. queryFmtNetInternetGiB = `sum(increase(kubecost_pod_network_egress_bytes_total{internet="true"}[%s]%s)) by (pod_name, namespace, %s) / 1024 / 1024 / 1024`
  50. queryFmtNetInternetCostPerGiB = `avg(avg_over_time(kubecost_network_internet_egress_cost{}[%s]%s)) by (%s)`
  51. queryFmtNetReceiveBytes = `sum(increase(container_network_receive_bytes_total{pod_name!=""}[%s]%s)) by (pod_name, namespace, %s)`
  52. queryFmtNetTransferBytes = `sum(increase(container_network_transmit_bytes_total{pod_name!=""}[%s]%s)) by (pod_name, namespace, %s)`
  53. queryFmtNamespaceLabels = `avg_over_time(kube_namespace_labels[%s]%s)`
  54. queryFmtNamespaceAnnotations = `avg_over_time(kube_namespace_annotations[%s]%s)`
  55. queryFmtPodLabels = `avg_over_time(kube_pod_labels[%s]%s)`
  56. queryFmtPodAnnotations = `avg_over_time(kube_pod_annotations[%s]%s)`
  57. queryFmtServiceLabels = `avg_over_time(service_selector_labels[%s]%s)`
  58. queryFmtDeploymentLabels = `avg_over_time(deployment_match_labels[%s]%s)`
  59. queryFmtStatefulSetLabels = `avg_over_time(statefulSet_match_labels[%s]%s)`
  60. queryFmtDaemonSetLabels = `sum(avg_over_time(kube_pod_owner{owner_kind="DaemonSet"}[%s]%s)) by (pod, owner_name, namespace, %s)`
  61. queryFmtJobLabels = `sum(avg_over_time(kube_pod_owner{owner_kind="Job"}[%s]%s)) by (pod, owner_name, namespace ,%s)`
  62. queryFmtLBCostPerHr = `avg(avg_over_time(kubecost_load_balancer_cost[%s]%s)) by (namespace, service_name, %s)`
  63. queryFmtLBActiveMins = `count(kubecost_load_balancer_cost) by (namespace, service_name, %s)[%s:%s]%s`
  64. )
  65. // CanCompute should return true if CostModel can act as a valid source for the
  66. // given time range. In the case of CostModel we want to attempt to compute as
  67. // long as the range starts in the past. If the CostModel ends up not having
  68. // data to match, that's okay, and should be communicated with an error
  69. // response from ComputeAllocation.
  70. func (cm *CostModel) CanCompute(start, end time.Time) bool {
  71. return start.Before(time.Now())
  72. }
  73. // Name returns the name of the Source
  74. func (cm *CostModel) Name() string {
  75. return "CostModel"
  76. }
  77. // ComputeAllocation uses the CostModel instance to compute an AllocationSet
  78. // for the window defined by the given start and end times. The Allocations
  79. // returned are unaggregated (i.e. down to the container level).
  80. func (cm *CostModel) ComputeAllocation(start, end time.Time, resolution time.Duration) (*kubecost.AllocationSet, error) {
  81. // 1. Build out Pod map from resolution-tuned, batched Pod start/end query
  82. // 2. Run and apply the results of the remaining queries to
  83. // 3. Build out AllocationSet from completed Pod map
  84. // Create a window spanning the requested query
  85. window := kubecost.NewWindow(&start, &end)
  86. // Create an empty AllocationSet. For safety, in the case of an error, we
  87. // should prefer to return this empty set with the error. (In the case of
  88. // no error, of course we populate the set and return it.)
  89. allocSet := kubecost.NewAllocationSet(start, end)
  90. // (1) Build out Pod map
  91. // Build out a map of Allocations as a mapping from pod-to-container-to-
  92. // underlying-Allocation instance, starting with (start, end) so that we
  93. // begin with minutes, from which we compute resource allocation and cost
  94. // totals from measured rate data.
  95. podMap := map[podKey]*Pod{}
  96. // clusterStarts and clusterEnds record the earliest start and latest end
  97. // times, respectively, on a cluster-basis. These are used for unmounted
  98. // PVs and other "virtual" Allocations so that minutes are maximally
  99. // accurate during start-up or spin-down of a cluster
  100. clusterStart := map[string]time.Time{}
  101. clusterEnd := map[string]time.Time{}
  102. cm.buildPodMap(window, resolution, env.GetETLMaxBatchDuration(), podMap, clusterStart, clusterEnd)
  103. // (2) Run and apply remaining queries
  104. // Convert window (start, end) to (duration, offset) for querying Prometheus,
  105. // including handling Thanos offset
  106. durStr, offStr, err := window.DurationOffsetForPrometheus()
  107. if err != nil {
  108. // Negative duration, so return empty set
  109. return allocSet, nil
  110. }
  111. // Convert resolution duration to a query-ready string
  112. resStr := timeutil.DurationString(resolution)
  113. ctx := prom.NewContext(cm.PrometheusClient)
  114. queryRAMBytesAllocated := fmt.Sprintf(queryFmtRAMBytesAllocated, durStr, offStr, env.GetPromClusterLabel())
  115. resChRAMBytesAllocated := ctx.Query(queryRAMBytesAllocated)
  116. queryRAMRequests := fmt.Sprintf(queryFmtRAMRequests, durStr, offStr, env.GetPromClusterLabel())
  117. resChRAMRequests := ctx.Query(queryRAMRequests)
  118. queryRAMUsageAvg := fmt.Sprintf(queryFmtRAMUsageAvg, durStr, offStr, env.GetPromClusterLabel())
  119. resChRAMUsageAvg := ctx.Query(queryRAMUsageAvg)
  120. queryRAMUsageMax := fmt.Sprintf(queryFmtRAMUsageMax, durStr, offStr, env.GetPromClusterLabel())
  121. resChRAMUsageMax := ctx.Query(queryRAMUsageMax)
  122. queryCPUCoresAllocated := fmt.Sprintf(queryFmtCPUCoresAllocated, durStr, offStr, env.GetPromClusterLabel())
  123. resChCPUCoresAllocated := ctx.Query(queryCPUCoresAllocated)
  124. queryCPURequests := fmt.Sprintf(queryFmtCPURequests, durStr, offStr, env.GetPromClusterLabel())
  125. resChCPURequests := ctx.Query(queryCPURequests)
  126. queryCPUUsageAvg := fmt.Sprintf(queryFmtCPUUsageAvg, durStr, offStr, env.GetPromClusterLabel())
  127. resChCPUUsageAvg := ctx.Query(queryCPUUsageAvg)
  128. queryCPUUsageMax := fmt.Sprintf(queryFmtCPUUsageMax, durStr, offStr, env.GetPromClusterLabel())
  129. resChCPUUsageMax := ctx.Query(queryCPUUsageMax)
  130. queryGPUsRequested := fmt.Sprintf(queryFmtGPUsRequested, durStr, offStr, env.GetPromClusterLabel())
  131. resChGPUsRequested := ctx.Query(queryGPUsRequested)
  132. queryGPUsAllocated := fmt.Sprintf(queryFmtGPUsAllocated, durStr, offStr, env.GetPromClusterLabel())
  133. resChGPUsAllocated := ctx.Query(queryGPUsAllocated)
  134. queryNodeCostPerCPUHr := fmt.Sprintf(queryFmtNodeCostPerCPUHr, durStr, offStr, env.GetPromClusterLabel())
  135. resChNodeCostPerCPUHr := ctx.Query(queryNodeCostPerCPUHr)
  136. queryNodeCostPerRAMGiBHr := fmt.Sprintf(queryFmtNodeCostPerRAMGiBHr, durStr, offStr, env.GetPromClusterLabel())
  137. resChNodeCostPerRAMGiBHr := ctx.Query(queryNodeCostPerRAMGiBHr)
  138. queryNodeCostPerGPUHr := fmt.Sprintf(queryFmtNodeCostPerGPUHr, durStr, offStr, env.GetPromClusterLabel())
  139. resChNodeCostPerGPUHr := ctx.Query(queryNodeCostPerGPUHr)
  140. queryNodeIsSpot := fmt.Sprintf(queryFmtNodeIsSpot, durStr, offStr)
  141. resChNodeIsSpot := ctx.Query(queryNodeIsSpot)
  142. queryPVCInfo := fmt.Sprintf(queryFmtPVCInfo, env.GetPromClusterLabel(), durStr, resStr, offStr)
  143. resChPVCInfo := ctx.Query(queryPVCInfo)
  144. queryPVBytes := fmt.Sprintf(queryFmtPVBytes, durStr, offStr, env.GetPromClusterLabel())
  145. resChPVBytes := ctx.Query(queryPVBytes)
  146. queryPodPVCAllocation := fmt.Sprintf(queryFmtPodPVCAllocation, durStr, offStr, env.GetPromClusterLabel())
  147. resChPodPVCAllocation := ctx.Query(queryPodPVCAllocation)
  148. queryPVCBytesRequested := fmt.Sprintf(queryFmtPVCBytesRequested, durStr, offStr, env.GetPromClusterLabel())
  149. resChPVCBytesRequested := ctx.Query(queryPVCBytesRequested)
  150. queryPVCostPerGiBHour := fmt.Sprintf(queryFmtPVCostPerGiBHour, durStr, offStr, env.GetPromClusterLabel())
  151. resChPVCostPerGiBHour := ctx.Query(queryPVCostPerGiBHour)
  152. queryNetTransferBytes := fmt.Sprintf(queryFmtNetTransferBytes, durStr, offStr, env.GetPromClusterLabel())
  153. resChNetTransferBytes := ctx.Query(queryNetTransferBytes)
  154. queryNetReceiveBytes := fmt.Sprintf(queryFmtNetReceiveBytes, durStr, offStr, env.GetPromClusterLabel())
  155. resChNetReceiveBytes := ctx.Query(queryNetReceiveBytes)
  156. queryNetZoneGiB := fmt.Sprintf(queryFmtNetZoneGiB, durStr, offStr, env.GetPromClusterLabel())
  157. resChNetZoneGiB := ctx.Query(queryNetZoneGiB)
  158. queryNetZoneCostPerGiB := fmt.Sprintf(queryFmtNetZoneCostPerGiB, durStr, offStr, env.GetPromClusterLabel())
  159. resChNetZoneCostPerGiB := ctx.Query(queryNetZoneCostPerGiB)
  160. queryNetRegionGiB := fmt.Sprintf(queryFmtNetRegionGiB, durStr, offStr, env.GetPromClusterLabel())
  161. resChNetRegionGiB := ctx.Query(queryNetRegionGiB)
  162. queryNetRegionCostPerGiB := fmt.Sprintf(queryFmtNetRegionCostPerGiB, durStr, offStr, env.GetPromClusterLabel())
  163. resChNetRegionCostPerGiB := ctx.Query(queryNetRegionCostPerGiB)
  164. queryNetInternetGiB := fmt.Sprintf(queryFmtNetInternetGiB, durStr, offStr, env.GetPromClusterLabel())
  165. resChNetInternetGiB := ctx.Query(queryNetInternetGiB)
  166. queryNetInternetCostPerGiB := fmt.Sprintf(queryFmtNetInternetCostPerGiB, durStr, offStr, env.GetPromClusterLabel())
  167. resChNetInternetCostPerGiB := ctx.Query(queryNetInternetCostPerGiB)
  168. queryNamespaceLabels := fmt.Sprintf(queryFmtNamespaceLabels, durStr, offStr)
  169. resChNamespaceLabels := ctx.Query(queryNamespaceLabels)
  170. queryNamespaceAnnotations := fmt.Sprintf(queryFmtNamespaceAnnotations, durStr, offStr)
  171. resChNamespaceAnnotations := ctx.Query(queryNamespaceAnnotations)
  172. queryPodLabels := fmt.Sprintf(queryFmtPodLabels, durStr, offStr)
  173. resChPodLabels := ctx.Query(queryPodLabels)
  174. queryPodAnnotations := fmt.Sprintf(queryFmtPodAnnotations, durStr, offStr)
  175. resChPodAnnotations := ctx.Query(queryPodAnnotations)
  176. queryServiceLabels := fmt.Sprintf(queryFmtServiceLabels, durStr, offStr)
  177. resChServiceLabels := ctx.Query(queryServiceLabels)
  178. queryDeploymentLabels := fmt.Sprintf(queryFmtDeploymentLabels, durStr, offStr)
  179. resChDeploymentLabels := ctx.Query(queryDeploymentLabels)
  180. queryStatefulSetLabels := fmt.Sprintf(queryFmtStatefulSetLabels, durStr, offStr)
  181. resChStatefulSetLabels := ctx.Query(queryStatefulSetLabels)
  182. queryDaemonSetLabels := fmt.Sprintf(queryFmtDaemonSetLabels, durStr, offStr, env.GetPromClusterLabel())
  183. resChDaemonSetLabels := ctx.Query(queryDaemonSetLabels)
  184. queryJobLabels := fmt.Sprintf(queryFmtJobLabels, durStr, offStr, env.GetPromClusterLabel())
  185. resChJobLabels := ctx.Query(queryJobLabels)
  186. queryLBCostPerHr := fmt.Sprintf(queryFmtLBCostPerHr, durStr, offStr, env.GetPromClusterLabel())
  187. resChLBCostPerHr := ctx.Query(queryLBCostPerHr)
  188. queryLBActiveMins := fmt.Sprintf(queryFmtLBActiveMins, env.GetPromClusterLabel(), durStr, resStr, offStr)
  189. resChLBActiveMins := ctx.Query(queryLBActiveMins)
  190. resCPUCoresAllocated, _ := resChCPUCoresAllocated.Await()
  191. resCPURequests, _ := resChCPURequests.Await()
  192. resCPUUsageAvg, _ := resChCPUUsageAvg.Await()
  193. resCPUUsageMax, _ := resChCPUUsageMax.Await()
  194. resRAMBytesAllocated, _ := resChRAMBytesAllocated.Await()
  195. resRAMRequests, _ := resChRAMRequests.Await()
  196. resRAMUsageAvg, _ := resChRAMUsageAvg.Await()
  197. resRAMUsageMax, _ := resChRAMUsageMax.Await()
  198. resGPUsRequested, _ := resChGPUsRequested.Await()
  199. resGPUsAllocated, _ := resChGPUsAllocated.Await()
  200. resNodeCostPerCPUHr, _ := resChNodeCostPerCPUHr.Await()
  201. resNodeCostPerRAMGiBHr, _ := resChNodeCostPerRAMGiBHr.Await()
  202. resNodeCostPerGPUHr, _ := resChNodeCostPerGPUHr.Await()
  203. resNodeIsSpot, _ := resChNodeIsSpot.Await()
  204. resPVBytes, _ := resChPVBytes.Await()
  205. resPVCostPerGiBHour, _ := resChPVCostPerGiBHour.Await()
  206. resPVCInfo, _ := resChPVCInfo.Await()
  207. resPVCBytesRequested, _ := resChPVCBytesRequested.Await()
  208. resPodPVCAllocation, _ := resChPodPVCAllocation.Await()
  209. resNetTransferBytes, _ := resChNetTransferBytes.Await()
  210. resNetReceiveBytes, _ := resChNetReceiveBytes.Await()
  211. resNetZoneGiB, _ := resChNetZoneGiB.Await()
  212. resNetZoneCostPerGiB, _ := resChNetZoneCostPerGiB.Await()
  213. resNetRegionGiB, _ := resChNetRegionGiB.Await()
  214. resNetRegionCostPerGiB, _ := resChNetRegionCostPerGiB.Await()
  215. resNetInternetGiB, _ := resChNetInternetGiB.Await()
  216. resNetInternetCostPerGiB, _ := resChNetInternetCostPerGiB.Await()
  217. resNamespaceLabels, _ := resChNamespaceLabels.Await()
  218. resNamespaceAnnotations, _ := resChNamespaceAnnotations.Await()
  219. resPodLabels, _ := resChPodLabels.Await()
  220. resPodAnnotations, _ := resChPodAnnotations.Await()
  221. resServiceLabels, _ := resChServiceLabels.Await()
  222. resDeploymentLabels, _ := resChDeploymentLabels.Await()
  223. resStatefulSetLabels, _ := resChStatefulSetLabels.Await()
  224. resDaemonSetLabels, _ := resChDaemonSetLabels.Await()
  225. resJobLabels, _ := resChJobLabels.Await()
  226. resLBCostPerHr, _ := resChLBCostPerHr.Await()
  227. resLBActiveMins, _ := resChLBActiveMins.Await()
  228. if ctx.HasErrors() {
  229. for _, err := range ctx.Errors() {
  230. log.Errorf("CostModel.ComputeAllocation: %s", err)
  231. }
  232. return allocSet, ctx.ErrorCollection()
  233. }
  234. // We choose to apply allocation before requests in the cases of RAM and
  235. // CPU so that we can assert that allocation should always be greater than
  236. // or equal to request.
  237. applyCPUCoresAllocated(podMap, resCPUCoresAllocated)
  238. applyCPUCoresRequested(podMap, resCPURequests)
  239. applyCPUCoresUsedAvg(podMap, resCPUUsageAvg)
  240. applyCPUCoresUsedMax(podMap, resCPUUsageMax)
  241. applyRAMBytesAllocated(podMap, resRAMBytesAllocated)
  242. applyRAMBytesRequested(podMap, resRAMRequests)
  243. applyRAMBytesUsedAvg(podMap, resRAMUsageAvg)
  244. applyRAMBytesUsedMax(podMap, resRAMUsageMax)
  245. applyGPUsAllocated(podMap, resGPUsRequested, resGPUsAllocated)
  246. applyNetworkTotals(podMap, resNetTransferBytes, resNetReceiveBytes)
  247. applyNetworkAllocation(podMap, resNetZoneGiB, resNetZoneCostPerGiB)
  248. applyNetworkAllocation(podMap, resNetRegionGiB, resNetRegionCostPerGiB)
  249. applyNetworkAllocation(podMap, resNetInternetGiB, resNetInternetCostPerGiB)
  250. namespaceLabels := resToNamespaceLabels(resNamespaceLabels)
  251. podLabels := resToPodLabels(resPodLabels)
  252. namespaceAnnotations := resToNamespaceAnnotations(resNamespaceAnnotations)
  253. podAnnotations := resToPodAnnotations(resPodAnnotations)
  254. applyLabels(podMap, namespaceLabels, podLabels)
  255. applyAnnotations(podMap, namespaceAnnotations, podAnnotations)
  256. serviceLabels := getServiceLabels(resServiceLabels)
  257. allocsByService := map[serviceKey][]*kubecost.Allocation{}
  258. applyServicesToPods(podMap, podLabels, allocsByService, serviceLabels)
  259. podDeploymentMap := labelsToPodControllerMap(podLabels, resToDeploymentLabels(resDeploymentLabels))
  260. podStatefulSetMap := labelsToPodControllerMap(podLabels, resToStatefulSetLabels(resStatefulSetLabels))
  261. podDaemonSetMap := resToPodDaemonSetMap(resDaemonSetLabels)
  262. podJobMap := resToPodJobMap(resJobLabels)
  263. applyControllersToPods(podMap, podDeploymentMap)
  264. applyControllersToPods(podMap, podStatefulSetMap)
  265. applyControllersToPods(podMap, podDaemonSetMap)
  266. applyControllersToPods(podMap, podJobMap)
  267. // TODO breakdown network costs?
  268. // Build out a map of Nodes with resource costs, discounts, and node types
  269. // for converting resource allocation data to cumulative costs.
  270. nodeMap := map[nodeKey]*NodePricing{}
  271. applyNodeCostPerCPUHr(nodeMap, resNodeCostPerCPUHr)
  272. applyNodeCostPerRAMGiBHr(nodeMap, resNodeCostPerRAMGiBHr)
  273. applyNodeCostPerGPUHr(nodeMap, resNodeCostPerGPUHr)
  274. applyNodeSpot(nodeMap, resNodeIsSpot)
  275. applyNodeDiscount(nodeMap, cm)
  276. // Build out the map of all PVs with class, size and cost-per-hour.
  277. // Note: this does not record time running, which we may want to
  278. // include later for increased PV precision. (As long as the PV has
  279. // a PVC, we get time running there, so this is only inaccurate
  280. // for short-lived, unmounted PVs.)
  281. pvMap := map[pvKey]*PV{}
  282. buildPVMap(pvMap, resPVCostPerGiBHour)
  283. applyPVBytes(pvMap, resPVBytes)
  284. // Build out the map of all PVCs with time running, bytes requested,
  285. // and connect to the correct PV from pvMap. (If no PV exists, that
  286. // is noted, but does not result in any allocation/cost.)
  287. pvcMap := map[pvcKey]*PVC{}
  288. buildPVCMap(window, pvcMap, pvMap, resPVCInfo)
  289. applyPVCBytesRequested(pvcMap, resPVCBytesRequested)
  290. // Build out the relationships of pods to their PVCs. This step
  291. // populates the PVC.Count field so that PVC allocation can be
  292. // split appropriately among each pod's container allocation.
  293. podPVCMap := map[podKey][]*PVC{}
  294. buildPodPVCMap(podPVCMap, pvMap, pvcMap, podMap, resPodPVCAllocation)
  295. // Identify unmounted PVs (PVs without PVCs) and add one Allocation per
  296. // cluster representing each cluster's unmounted PVs (if necessary).
  297. applyUnmountedPVs(window, podMap, pvMap, pvcMap)
  298. lbMap := getLoadBalancerCosts(resLBCostPerHr, resLBActiveMins, resolution)
  299. applyLoadBalancersToPods(lbMap, allocsByService)
  300. // (3) Build out AllocationSet from Pod map
  301. for _, pod := range podMap {
  302. for _, alloc := range pod.Allocations {
  303. cluster := alloc.Properties.Cluster
  304. nodeName := alloc.Properties.Node
  305. namespace := alloc.Properties.Namespace
  306. pod := alloc.Properties.Pod
  307. container := alloc.Properties.Container
  308. podKey := newPodKey(cluster, namespace, pod)
  309. nodeKey := newNodeKey(cluster, nodeName)
  310. node := cm.getNodePricing(nodeMap, nodeKey)
  311. alloc.Properties.ProviderID = node.ProviderID
  312. alloc.CPUCost = alloc.CPUCoreHours * node.CostPerCPUHr
  313. alloc.RAMCost = (alloc.RAMByteHours / 1024 / 1024 / 1024) * node.CostPerRAMGiBHr
  314. alloc.GPUCost = alloc.GPUHours * node.CostPerGPUHr
  315. if pvcs, ok := podPVCMap[podKey]; ok {
  316. for _, pvc := range pvcs {
  317. // Determine the (start, end) of the relationship between the
  318. // given PVC and the associated Allocation so that a precise
  319. // number of hours can be used to compute cumulative cost.
  320. s, e := alloc.Start, alloc.End
  321. if pvc.Start.After(alloc.Start) {
  322. s = pvc.Start
  323. }
  324. if pvc.End.Before(alloc.End) {
  325. e = pvc.End
  326. }
  327. minutes := e.Sub(s).Minutes()
  328. hrs := minutes / 60.0
  329. count := float64(pvc.Count)
  330. if pvc.Count < 1 {
  331. count = 1
  332. }
  333. gib := pvc.Bytes / 1024 / 1024 / 1024
  334. cost := pvc.Volume.CostPerGiBHour * gib * hrs
  335. // Apply the size and cost of the PV to the allocation, each
  336. // weighted by count (i.e. the number of containers in the pod)
  337. // record the amount of total PVBytes Hours attributable to a given PV
  338. if alloc.PVs == nil {
  339. alloc.PVs = kubecost.PVAllocations{}
  340. }
  341. pvKey := kubecost.PVKey{
  342. Cluster: pvc.Cluster,
  343. Name: pvc.Volume.Name,
  344. }
  345. alloc.PVs[pvKey] = &kubecost.PVAllocation{
  346. ByteHours: pvc.Bytes * hrs / count,
  347. Cost: cost / count,
  348. }
  349. }
  350. }
  351. // Make sure that the name is correct (node may not be present at this
  352. // point due to it missing from queryMinutes) then insert.
  353. alloc.Name = fmt.Sprintf("%s/%s/%s/%s/%s", cluster, nodeName, namespace, pod, container)
  354. allocSet.Set(alloc)
  355. }
  356. }
  357. return allocSet, nil
  358. }
  359. func (cm *CostModel) buildPodMap(window kubecost.Window, resolution, maxBatchSize time.Duration, podMap map[podKey]*Pod, clusterStart, clusterEnd map[string]time.Time) error {
  360. // Assumes that window is positive and closed
  361. start, end := *window.Start(), *window.End()
  362. // Convert resolution duration to a query-ready string
  363. resStr := timeutil.DurationString(resolution)
  364. ctx := prom.NewContext(cm.PrometheusClient)
  365. // Query for (start, end) by (pod, namespace, cluster) over the given
  366. // window, using the given resolution, and if necessary in batches no
  367. // larger than the given maximum batch size. If working in batches, track
  368. // overall progress by starting with (window.start, window.start) and
  369. // querying in batches no larger than maxBatchSize from start-to-end,
  370. // folding each result set into podMap as the results come back.
  371. coverage := kubecost.NewWindow(&start, &start)
  372. numQuery := 1
  373. for coverage.End().Before(end) {
  374. // Determine the (start, end) of the current batch
  375. batchStart := *coverage.End()
  376. batchEnd := coverage.End().Add(maxBatchSize)
  377. if batchEnd.After(end) {
  378. batchEnd = end
  379. }
  380. batchWindow := kubecost.NewWindow(&batchStart, &batchEnd)
  381. var resPods []*prom.QueryResult
  382. var err error
  383. maxTries := 3
  384. numTries := 0
  385. for resPods == nil && numTries < maxTries {
  386. numTries++
  387. // Convert window (start, end) to (duration, offset) for querying Prometheus,
  388. // including handling Thanos offset
  389. durStr, offStr, err := batchWindow.DurationOffsetForPrometheus()
  390. if err != nil || durStr == "" {
  391. // Negative duration, so set empty results and don't query
  392. resPods = []*prom.QueryResult{}
  393. err = nil
  394. break
  395. }
  396. // Submit and profile query
  397. queryPods := fmt.Sprintf(queryFmtPods, env.GetPromClusterLabel(), durStr, resStr, offStr)
  398. queryProfile := time.Now()
  399. resPods, err = ctx.Query(queryPods).Await()
  400. if err != nil {
  401. log.Profile(queryProfile, fmt.Sprintf("CostModel.ComputeAllocation: pod query %d try %d failed: %s", numQuery, numTries, queryPods))
  402. resPods = nil
  403. }
  404. }
  405. if err != nil {
  406. return err
  407. }
  408. applyPodResults(window, resolution, podMap, clusterStart, clusterEnd, resPods)
  409. coverage = coverage.ExpandEnd(batchEnd)
  410. numQuery++
  411. }
  412. return nil
  413. }
  414. func applyPodResults(window kubecost.Window, resolution time.Duration, podMap map[podKey]*Pod, clusterStart, clusterEnd map[string]time.Time, resPods []*prom.QueryResult) {
  415. for _, res := range resPods {
  416. if len(res.Values) == 0 {
  417. log.Warningf("CostModel.ComputeAllocation: empty minutes result")
  418. continue
  419. }
  420. cluster, err := res.GetString(env.GetPromClusterLabel())
  421. if err != nil {
  422. cluster = env.GetClusterID()
  423. }
  424. labels, err := res.GetStrings("namespace", "pod")
  425. if err != nil {
  426. log.Warningf("CostModel.ComputeAllocation: minutes query result missing field: %s", err)
  427. continue
  428. }
  429. namespace := labels["namespace"]
  430. pod := labels["pod"]
  431. key := newPodKey(cluster, namespace, pod)
  432. // allocStart and allocEnd are the timestamps of the first and last
  433. // minutes the pod was running, respectively. We subtract one resolution
  434. // from allocStart because this point will actually represent the end
  435. // of the first minute. We don't subtract from allocEnd because it
  436. // already represents the end of the last minute.
  437. var allocStart, allocEnd time.Time
  438. startAdjustmentCoeff, endAdjustmentCoeff := 1.0, 1.0
  439. for _, datum := range res.Values {
  440. t := time.Unix(int64(datum.Timestamp), 0)
  441. if allocStart.IsZero() && datum.Value > 0 && window.Contains(t) {
  442. // Set the start timestamp to the earliest non-zero timestamp
  443. allocStart = t
  444. // Record adjustment coefficient, i.e. the portion of the start
  445. // timestamp to "ignore". That is, sometimes the value will be
  446. // 0.5, meaning that we should discount the time running by
  447. // half of the resolution the timestamp stands for.
  448. startAdjustmentCoeff = (1.0 - datum.Value)
  449. }
  450. if datum.Value > 0 && window.Contains(t) {
  451. // Set the end timestamp to the latest non-zero timestamp
  452. allocEnd = t
  453. // Record adjustment coefficient, i.e. the portion of the end
  454. // timestamp to "ignore". (See explanation above for start.)
  455. endAdjustmentCoeff = (1.0 - datum.Value)
  456. }
  457. }
  458. if allocStart.IsZero() || allocEnd.IsZero() {
  459. continue
  460. }
  461. // Adjust timestamps according to the resolution and the adjustment
  462. // coefficients, as described above. That is, count the start timestamp
  463. // from the beginning of the resolution, not the end. Then "reduce" the
  464. // start and end by the correct amount, in the case that the "running"
  465. // value of the first or last timestamp was not a full 1.0.
  466. allocStart = allocStart.Add(-resolution)
  467. // Note: the *100 and /100 are necessary because Duration is an int, so
  468. // 0.5, for instance, will be truncated, resulting in no adjustment.
  469. allocStart = allocStart.Add(time.Duration(startAdjustmentCoeff*100) * resolution / time.Duration(100))
  470. allocEnd = allocEnd.Add(-time.Duration(endAdjustmentCoeff*100) * resolution / time.Duration(100))
  471. // If there is only one point with a value <= 0.5 that the start and
  472. // end timestamps both share, then we will enter this case because at
  473. // least half of a resolution will be subtracted from both the start
  474. // and the end. If that is the case, then add back half of each side
  475. // so that the pod is said to run for half a resolution total.
  476. // e.g. For resolution 1m and a value of 0.5 at one timestamp, we'll
  477. // end up with allocEnd == allocStart and each coeff == 0.5. In
  478. // that case, add 0.25m to each side, resulting in 0.5m duration.
  479. if !allocEnd.After(allocStart) {
  480. allocStart = allocStart.Add(-time.Duration(50*startAdjustmentCoeff) * resolution / time.Duration(100))
  481. allocEnd = allocEnd.Add(time.Duration(50*endAdjustmentCoeff) * resolution / time.Duration(100))
  482. }
  483. // Set start if unset or this datum's start time is earlier than the
  484. // current earliest time.
  485. if _, ok := clusterStart[cluster]; !ok || allocStart.Before(clusterStart[cluster]) {
  486. clusterStart[cluster] = allocStart
  487. }
  488. // Set end if unset or this datum's end time is later than the
  489. // current latest time.
  490. if _, ok := clusterEnd[cluster]; !ok || allocEnd.After(clusterEnd[cluster]) {
  491. clusterEnd[cluster] = allocEnd
  492. }
  493. if pod, ok := podMap[key]; ok {
  494. // Pod has already been recorded, so update it accordingly
  495. if allocStart.Before(pod.Start) {
  496. pod.Start = allocStart
  497. }
  498. if allocEnd.After(pod.End) {
  499. pod.End = allocEnd
  500. }
  501. } else {
  502. // Pod has not been recorded yet, so insert it
  503. podMap[key] = &Pod{
  504. Window: window.Clone(),
  505. Start: allocStart,
  506. End: allocEnd,
  507. Key: key,
  508. Allocations: map[string]*kubecost.Allocation{},
  509. }
  510. }
  511. }
  512. }
  513. func applyCPUCoresAllocated(podMap map[podKey]*Pod, resCPUCoresAllocated []*prom.QueryResult) {
  514. for _, res := range resCPUCoresAllocated {
  515. key, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  516. if err != nil {
  517. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU allocation result missing field: %s", err)
  518. continue
  519. }
  520. pod, ok := podMap[key]
  521. if !ok {
  522. continue
  523. }
  524. container, err := res.GetString("container")
  525. if err != nil {
  526. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU allocation query result missing 'container': %s", key)
  527. continue
  528. }
  529. if _, ok := pod.Allocations[container]; !ok {
  530. pod.AppendContainer(container)
  531. }
  532. cpuCores := res.Values[0].Value
  533. hours := pod.Allocations[container].Minutes() / 60.0
  534. pod.Allocations[container].CPUCoreHours = cpuCores * hours
  535. node, err := res.GetString("node")
  536. if err != nil {
  537. log.Warningf("CostModel.ComputeAllocation: CPU allocation query result missing 'node': %s", key)
  538. continue
  539. }
  540. pod.Allocations[container].Properties.Node = node
  541. }
  542. }
  543. func applyCPUCoresRequested(podMap map[podKey]*Pod, resCPUCoresRequested []*prom.QueryResult) {
  544. for _, res := range resCPUCoresRequested {
  545. key, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  546. if err != nil {
  547. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU request result missing field: %s", err)
  548. continue
  549. }
  550. pod, ok := podMap[key]
  551. if !ok {
  552. continue
  553. }
  554. container, err := res.GetString("container")
  555. if err != nil {
  556. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU request query result missing 'container': %s", key)
  557. continue
  558. }
  559. if _, ok := pod.Allocations[container]; !ok {
  560. pod.AppendContainer(container)
  561. }
  562. pod.Allocations[container].CPUCoreRequestAverage = res.Values[0].Value
  563. // If CPU allocation is less than requests, set CPUCoreHours to
  564. // request level.
  565. if pod.Allocations[container].CPUCores() < res.Values[0].Value {
  566. pod.Allocations[container].CPUCoreHours = res.Values[0].Value * (pod.Allocations[container].Minutes() / 60.0)
  567. }
  568. node, err := res.GetString("node")
  569. if err != nil {
  570. log.Warningf("CostModel.ComputeAllocation: CPU request query result missing 'node': %s", key)
  571. continue
  572. }
  573. pod.Allocations[container].Properties.Node = node
  574. }
  575. }
  576. func applyCPUCoresUsedAvg(podMap map[podKey]*Pod, resCPUCoresUsedAvg []*prom.QueryResult) {
  577. for _, res := range resCPUCoresUsedAvg {
  578. key, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  579. if err != nil {
  580. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU usage avg result missing field: %s", err)
  581. continue
  582. }
  583. pod, ok := podMap[key]
  584. if !ok {
  585. continue
  586. }
  587. container, err := res.GetString("container")
  588. if container == "" || err != nil {
  589. container, err = res.GetString("container_name")
  590. if err != nil {
  591. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU usage avg query result missing 'container': %s", key)
  592. continue
  593. }
  594. }
  595. if _, ok := pod.Allocations[container]; !ok {
  596. pod.AppendContainer(container)
  597. }
  598. pod.Allocations[container].CPUCoreUsageAverage = res.Values[0].Value
  599. }
  600. }
  601. func applyCPUCoresUsedMax(podMap map[podKey]*Pod, resCPUCoresUsedMax []*prom.QueryResult) {
  602. for _, res := range resCPUCoresUsedMax {
  603. key, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  604. if err != nil {
  605. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU usage max result missing field: %s", err)
  606. continue
  607. }
  608. pod, ok := podMap[key]
  609. if !ok {
  610. continue
  611. }
  612. container, err := res.GetString("container_name")
  613. if err != nil {
  614. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU usage max query result missing 'container': %s", key)
  615. continue
  616. }
  617. if _, ok := pod.Allocations[container]; !ok {
  618. pod.AppendContainer(container)
  619. }
  620. if pod.Allocations[container].RawAllocationOnly == nil {
  621. pod.Allocations[container].RawAllocationOnly = &kubecost.RawAllocationOnlyData{
  622. CPUCoreUsageMax: res.Values[0].Value,
  623. }
  624. } else {
  625. pod.Allocations[container].RawAllocationOnly.CPUCoreUsageMax = res.Values[0].Value
  626. }
  627. }
  628. }
  629. func applyRAMBytesAllocated(podMap map[podKey]*Pod, resRAMBytesAllocated []*prom.QueryResult) {
  630. for _, res := range resRAMBytesAllocated {
  631. key, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  632. if err != nil {
  633. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM allocation result missing field: %s", err)
  634. continue
  635. }
  636. pod, ok := podMap[key]
  637. if !ok {
  638. continue
  639. }
  640. container, err := res.GetString("container")
  641. if err != nil {
  642. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM allocation query result missing 'container': %s", key)
  643. continue
  644. }
  645. if _, ok := pod.Allocations[container]; !ok {
  646. pod.AppendContainer(container)
  647. }
  648. ramBytes := res.Values[0].Value
  649. hours := pod.Allocations[container].Minutes() / 60.0
  650. pod.Allocations[container].RAMByteHours = ramBytes * hours
  651. node, err := res.GetString("node")
  652. if err != nil {
  653. log.Warningf("CostModel.ComputeAllocation: RAM allocation query result missing 'node': %s", key)
  654. continue
  655. }
  656. pod.Allocations[container].Properties.Node = node
  657. }
  658. }
  659. func applyRAMBytesRequested(podMap map[podKey]*Pod, resRAMBytesRequested []*prom.QueryResult) {
  660. for _, res := range resRAMBytesRequested {
  661. key, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  662. if err != nil {
  663. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM request result missing field: %s", err)
  664. continue
  665. }
  666. pod, ok := podMap[key]
  667. if !ok {
  668. continue
  669. }
  670. container, err := res.GetString("container")
  671. if err != nil {
  672. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM request query result missing 'container': %s", key)
  673. continue
  674. }
  675. if _, ok := pod.Allocations[container]; !ok {
  676. pod.AppendContainer(container)
  677. }
  678. pod.Allocations[container].RAMBytesRequestAverage = res.Values[0].Value
  679. // If RAM allocation is less than requests, set RAMByteHours to
  680. // request level.
  681. if pod.Allocations[container].RAMBytes() < res.Values[0].Value {
  682. pod.Allocations[container].RAMByteHours = res.Values[0].Value * (pod.Allocations[container].Minutes() / 60.0)
  683. }
  684. node, err := res.GetString("node")
  685. if err != nil {
  686. log.Warningf("CostModel.ComputeAllocation: RAM request query result missing 'node': %s", key)
  687. continue
  688. }
  689. pod.Allocations[container].Properties.Node = node
  690. }
  691. }
  692. func applyRAMBytesUsedAvg(podMap map[podKey]*Pod, resRAMBytesUsedAvg []*prom.QueryResult) {
  693. for _, res := range resRAMBytesUsedAvg {
  694. key, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  695. if err != nil {
  696. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM avg usage result missing field: %s", err)
  697. continue
  698. }
  699. pod, ok := podMap[key]
  700. if !ok {
  701. continue
  702. }
  703. container, err := res.GetString("container")
  704. if container == "" || err != nil {
  705. container, err = res.GetString("container_name")
  706. if err != nil {
  707. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU usage avg query result missing 'container': %s", key)
  708. continue
  709. }
  710. }
  711. if _, ok := pod.Allocations[container]; !ok {
  712. pod.AppendContainer(container)
  713. }
  714. pod.Allocations[container].RAMBytesUsageAverage = res.Values[0].Value
  715. }
  716. }
  717. func applyRAMBytesUsedMax(podMap map[podKey]*Pod, resRAMBytesUsedMax []*prom.QueryResult) {
  718. for _, res := range resRAMBytesUsedMax {
  719. key, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  720. if err != nil {
  721. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM usage max result missing field: %s", err)
  722. continue
  723. }
  724. pod, ok := podMap[key]
  725. if !ok {
  726. continue
  727. }
  728. container, err := res.GetString("container_name")
  729. if err != nil {
  730. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM usage max query result missing 'container': %s", key)
  731. continue
  732. }
  733. if _, ok := pod.Allocations[container]; !ok {
  734. pod.AppendContainer(container)
  735. }
  736. if pod.Allocations[container].RawAllocationOnly == nil {
  737. pod.Allocations[container].RawAllocationOnly = &kubecost.RawAllocationOnlyData{
  738. RAMBytesUsageMax: res.Values[0].Value,
  739. }
  740. } else {
  741. pod.Allocations[container].RawAllocationOnly.RAMBytesUsageMax = res.Values[0].Value
  742. }
  743. }
  744. }
  745. func applyGPUsAllocated(podMap map[podKey]*Pod, resGPUsRequested []*prom.QueryResult, resGPUsAllocated []*prom.QueryResult) {
  746. if len(resGPUsAllocated) > 0 { // Use the new query, when it's become available in a window
  747. resGPUsRequested = resGPUsAllocated
  748. }
  749. for _, res := range resGPUsRequested {
  750. key, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  751. if err != nil {
  752. log.DedupedWarningf(10, "CostModel.ComputeAllocation: GPU request result missing field: %s", err)
  753. continue
  754. }
  755. pod, ok := podMap[key]
  756. if !ok {
  757. continue
  758. }
  759. container, err := res.GetString("container")
  760. if err != nil {
  761. log.DedupedWarningf(10, "CostModel.ComputeAllocation: GPU request query result missing 'container': %s", key)
  762. continue
  763. }
  764. if _, ok := pod.Allocations[container]; !ok {
  765. pod.AppendContainer(container)
  766. }
  767. hrs := pod.Allocations[container].Minutes() / 60.0
  768. pod.Allocations[container].GPUHours = res.Values[0].Value * hrs
  769. }
  770. }
  771. func applyNetworkTotals(podMap map[podKey]*Pod, resNetworkTransferBytes []*prom.QueryResult, resNetworkReceiveBytes []*prom.QueryResult) {
  772. for _, res := range resNetworkTransferBytes {
  773. podKey, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  774. if err != nil {
  775. log.DedupedWarningf(10, "CostModel.ComputeAllocation: Network Transfer Bytes query result missing field: %s", err)
  776. continue
  777. }
  778. pod, ok := podMap[podKey]
  779. if !ok {
  780. continue
  781. }
  782. for _, alloc := range pod.Allocations {
  783. alloc.NetworkTransferBytes = res.Values[0].Value / float64(len(pod.Allocations))
  784. }
  785. }
  786. for _, res := range resNetworkReceiveBytes {
  787. podKey, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  788. if err != nil {
  789. log.DedupedWarningf(10, "CostModel.ComputeAllocation: Network Receive Bytes query result missing field: %s", err)
  790. continue
  791. }
  792. pod, ok := podMap[podKey]
  793. if !ok {
  794. continue
  795. }
  796. for _, alloc := range pod.Allocations {
  797. alloc.NetworkReceiveBytes = res.Values[0].Value / float64(len(pod.Allocations))
  798. }
  799. }
  800. }
  801. func applyNetworkAllocation(podMap map[podKey]*Pod, resNetworkGiB []*prom.QueryResult, resNetworkCostPerGiB []*prom.QueryResult) {
  802. costPerGiBByCluster := map[string]float64{}
  803. for _, res := range resNetworkCostPerGiB {
  804. cluster, err := res.GetString(env.GetPromClusterLabel())
  805. if err != nil {
  806. cluster = env.GetClusterID()
  807. }
  808. costPerGiBByCluster[cluster] = res.Values[0].Value
  809. }
  810. for _, res := range resNetworkGiB {
  811. podKey, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  812. if err != nil {
  813. log.DedupedWarningf(10, "CostModel.ComputeAllocation: Network allocation query result missing field: %s", err)
  814. continue
  815. }
  816. pod, ok := podMap[podKey]
  817. if !ok {
  818. continue
  819. }
  820. for _, alloc := range pod.Allocations {
  821. gib := res.Values[0].Value / float64(len(pod.Allocations))
  822. costPerGiB := costPerGiBByCluster[podKey.Cluster]
  823. alloc.NetworkCost = gib * costPerGiB
  824. }
  825. }
  826. }
  827. func resToNamespaceLabels(resNamespaceLabels []*prom.QueryResult) map[namespaceKey]map[string]string {
  828. namespaceLabels := map[namespaceKey]map[string]string{}
  829. for _, res := range resNamespaceLabels {
  830. nsKey, err := resultNamespaceKey(res, env.GetPromClusterLabel(), "namespace")
  831. if err != nil {
  832. continue
  833. }
  834. if _, ok := namespaceLabels[nsKey]; !ok {
  835. namespaceLabels[nsKey] = map[string]string{}
  836. }
  837. for k, l := range res.GetLabels() {
  838. namespaceLabels[nsKey][k] = l
  839. }
  840. }
  841. return namespaceLabels
  842. }
  843. func resToPodLabels(resPodLabels []*prom.QueryResult) map[podKey]map[string]string {
  844. podLabels := map[podKey]map[string]string{}
  845. for _, res := range resPodLabels {
  846. podKey, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  847. if err != nil {
  848. continue
  849. }
  850. if _, ok := podLabels[podKey]; !ok {
  851. podLabels[podKey] = map[string]string{}
  852. }
  853. for k, l := range res.GetLabels() {
  854. podLabels[podKey][k] = l
  855. }
  856. }
  857. return podLabels
  858. }
  859. func resToNamespaceAnnotations(resNamespaceAnnotations []*prom.QueryResult) map[string]map[string]string {
  860. namespaceAnnotations := map[string]map[string]string{}
  861. for _, res := range resNamespaceAnnotations {
  862. namespace, err := res.GetString("namespace")
  863. if err != nil {
  864. continue
  865. }
  866. if _, ok := namespaceAnnotations[namespace]; !ok {
  867. namespaceAnnotations[namespace] = map[string]string{}
  868. }
  869. for k, l := range res.GetAnnotations() {
  870. namespaceAnnotations[namespace][k] = l
  871. }
  872. }
  873. return namespaceAnnotations
  874. }
  875. func resToPodAnnotations(resPodAnnotations []*prom.QueryResult) map[podKey]map[string]string {
  876. podAnnotations := map[podKey]map[string]string{}
  877. for _, res := range resPodAnnotations {
  878. podKey, err := resultPodKey(res, env.GetPromClusterLabel(), "namespace")
  879. if err != nil {
  880. continue
  881. }
  882. if _, ok := podAnnotations[podKey]; !ok {
  883. podAnnotations[podKey] = map[string]string{}
  884. }
  885. for k, l := range res.GetAnnotations() {
  886. podAnnotations[podKey][k] = l
  887. }
  888. }
  889. return podAnnotations
  890. }
  891. func applyLabels(podMap map[podKey]*Pod, namespaceLabels map[namespaceKey]map[string]string, podLabels map[podKey]map[string]string) {
  892. for podKey, pod := range podMap {
  893. for _, alloc := range pod.Allocations {
  894. allocLabels := alloc.Properties.Labels
  895. if allocLabels == nil {
  896. allocLabels = make(map[string]string)
  897. }
  898. // Apply namespace labels first, then pod labels so that pod labels
  899. // overwrite namespace labels.
  900. nsKey := newNamespaceKey(podKey.Cluster, podKey.Namespace)
  901. if labels, ok := namespaceLabels[nsKey]; ok {
  902. for k, v := range labels {
  903. allocLabels[k] = v
  904. }
  905. }
  906. if labels, ok := podLabels[podKey]; ok {
  907. for k, v := range labels {
  908. allocLabels[k] = v
  909. }
  910. }
  911. alloc.Properties.Labels = allocLabels
  912. }
  913. }
  914. }
  915. func applyAnnotations(podMap map[podKey]*Pod, namespaceAnnotations map[string]map[string]string, podAnnotations map[podKey]map[string]string) {
  916. for key, pod := range podMap {
  917. for _, alloc := range pod.Allocations {
  918. allocAnnotations := alloc.Properties.Annotations
  919. if allocAnnotations == nil {
  920. allocAnnotations = make(map[string]string)
  921. }
  922. // Apply namespace annotations first, then pod annotations so that
  923. // pod labels overwrite namespace labels.
  924. if labels, ok := namespaceAnnotations[key.Namespace]; ok {
  925. for k, v := range labels {
  926. allocAnnotations[k] = v
  927. }
  928. }
  929. if labels, ok := podAnnotations[key]; ok {
  930. for k, v := range labels {
  931. allocAnnotations[k] = v
  932. }
  933. }
  934. alloc.Properties.Annotations = allocAnnotations
  935. }
  936. }
  937. }
  938. func getServiceLabels(resServiceLabels []*prom.QueryResult) map[serviceKey]map[string]string {
  939. serviceLabels := map[serviceKey]map[string]string{}
  940. for _, res := range resServiceLabels {
  941. serviceKey, err := resultServiceKey(res, env.GetPromClusterLabel(), "namespace", "service")
  942. if err != nil {
  943. continue
  944. }
  945. if _, ok := serviceLabels[serviceKey]; !ok {
  946. serviceLabels[serviceKey] = map[string]string{}
  947. }
  948. for k, l := range res.GetLabels() {
  949. serviceLabels[serviceKey][k] = l
  950. }
  951. }
  952. // Prune duplicate services. That is, if the same service exists with
  953. // hyphens instead of underscores, keep the one that uses hyphens.
  954. for key := range serviceLabels {
  955. if strings.Contains(key.Service, "_") {
  956. duplicateService := strings.Replace(key.Service, "_", "-", -1)
  957. duplicateKey := newServiceKey(key.Cluster, key.Namespace, duplicateService)
  958. if _, ok := serviceLabels[duplicateKey]; ok {
  959. delete(serviceLabels, key)
  960. }
  961. }
  962. }
  963. return serviceLabels
  964. }
  965. func resToDeploymentLabels(resDeploymentLabels []*prom.QueryResult) map[controllerKey]map[string]string {
  966. deploymentLabels := map[controllerKey]map[string]string{}
  967. for _, res := range resDeploymentLabels {
  968. controllerKey, err := resultDeploymentKey(res, env.GetPromClusterLabel(), "namespace", "deployment")
  969. if err != nil {
  970. continue
  971. }
  972. if _, ok := deploymentLabels[controllerKey]; !ok {
  973. deploymentLabels[controllerKey] = map[string]string{}
  974. }
  975. for k, l := range res.GetLabels() {
  976. deploymentLabels[controllerKey][k] = l
  977. }
  978. }
  979. // Prune duplicate deployments. That is, if the same deployment exists with
  980. // hyphens instead of underscores, keep the one that uses hyphens.
  981. for key := range deploymentLabels {
  982. if strings.Contains(key.Controller, "_") {
  983. duplicateController := strings.Replace(key.Controller, "_", "-", -1)
  984. duplicateKey := newControllerKey(key.Cluster, key.Namespace, key.ControllerKind, duplicateController)
  985. if _, ok := deploymentLabels[duplicateKey]; ok {
  986. delete(deploymentLabels, key)
  987. }
  988. }
  989. }
  990. return deploymentLabels
  991. }
  992. func resToStatefulSetLabels(resStatefulSetLabels []*prom.QueryResult) map[controllerKey]map[string]string {
  993. statefulSetLabels := map[controllerKey]map[string]string{}
  994. for _, res := range resStatefulSetLabels {
  995. controllerKey, err := resultStatefulSetKey(res, env.GetPromClusterLabel(), "namespace", "statefulSet")
  996. if err != nil {
  997. continue
  998. }
  999. if _, ok := statefulSetLabels[controllerKey]; !ok {
  1000. statefulSetLabels[controllerKey] = map[string]string{}
  1001. }
  1002. for k, l := range res.GetLabels() {
  1003. statefulSetLabels[controllerKey][k] = l
  1004. }
  1005. }
  1006. // Prune duplicate stateful sets. That is, if the same stateful set exists
  1007. // with hyphens instead of underscores, keep the one that uses hyphens.
  1008. for key := range statefulSetLabels {
  1009. if strings.Contains(key.Controller, "_") {
  1010. duplicateController := strings.Replace(key.Controller, "_", "-", -1)
  1011. duplicateKey := newControllerKey(key.Cluster, key.Namespace, key.ControllerKind, duplicateController)
  1012. if _, ok := statefulSetLabels[duplicateKey]; ok {
  1013. delete(statefulSetLabels, key)
  1014. }
  1015. }
  1016. }
  1017. return statefulSetLabels
  1018. }
  1019. func labelsToPodControllerMap(podLabels map[podKey]map[string]string, controllerLabels map[controllerKey]map[string]string) map[podKey]controllerKey {
  1020. podControllerMap := map[podKey]controllerKey{}
  1021. // For each controller, turn the labels into a selector and attempt to
  1022. // match it with each set of pod labels. A match indicates that the pod
  1023. // belongs to the controller.
  1024. for cKey, cLabels := range controllerLabels {
  1025. selector := labels.Set(cLabels).AsSelectorPreValidated()
  1026. for pKey, pLabels := range podLabels {
  1027. // If the pod is in a different cluster or namespace, there is
  1028. // no need to compare the labels.
  1029. if cKey.Cluster != pKey.Cluster || cKey.Namespace != pKey.Namespace {
  1030. continue
  1031. }
  1032. podLabelSet := labels.Set(pLabels)
  1033. if selector.Matches(podLabelSet) {
  1034. if _, ok := podControllerMap[pKey]; ok {
  1035. log.DedupedWarningf(5, "CostModel.ComputeAllocation: PodControllerMap match already exists: %s matches %s and %s", pKey, podControllerMap[pKey], cKey)
  1036. }
  1037. podControllerMap[pKey] = cKey
  1038. }
  1039. }
  1040. }
  1041. return podControllerMap
  1042. }
  1043. func resToPodDaemonSetMap(resDaemonSetLabels []*prom.QueryResult) map[podKey]controllerKey {
  1044. daemonSetLabels := map[podKey]controllerKey{}
  1045. for _, res := range resDaemonSetLabels {
  1046. controllerKey, err := resultDaemonSetKey(res, env.GetPromClusterLabel(), "namespace", "owner_name")
  1047. if err != nil {
  1048. continue
  1049. }
  1050. pod, err := res.GetString("pod")
  1051. if err != nil {
  1052. log.Warningf("CostModel.ComputeAllocation: DaemonSetLabel result without pod: %s", controllerKey)
  1053. }
  1054. podKey := newPodKey(controllerKey.Cluster, controllerKey.Namespace, pod)
  1055. daemonSetLabels[podKey] = controllerKey
  1056. }
  1057. return daemonSetLabels
  1058. }
  1059. func resToPodJobMap(resJobLabels []*prom.QueryResult) map[podKey]controllerKey {
  1060. jobLabels := map[podKey]controllerKey{}
  1061. for _, res := range resJobLabels {
  1062. controllerKey, err := resultJobKey(res, env.GetPromClusterLabel(), "namespace", "owner_name")
  1063. if err != nil {
  1064. continue
  1065. }
  1066. // Convert the name of Jobs generated by CronJobs to the name of the
  1067. // CronJob by stripping the timestamp off the end.
  1068. match := isCron.FindStringSubmatch(controllerKey.Controller)
  1069. if match != nil {
  1070. controllerKey.Controller = match[1]
  1071. }
  1072. pod, err := res.GetString("pod")
  1073. if err != nil {
  1074. log.Warningf("CostModel.ComputeAllocation: JobLabel result without pod: %s", controllerKey)
  1075. }
  1076. podKey := newPodKey(controllerKey.Cluster, controllerKey.Namespace, pod)
  1077. jobLabels[podKey] = controllerKey
  1078. }
  1079. return jobLabels
  1080. }
  1081. func applyServicesToPods(podMap map[podKey]*Pod, podLabels map[podKey]map[string]string, allocsByService map[serviceKey][]*kubecost.Allocation, serviceLabels map[serviceKey]map[string]string) {
  1082. podServicesMap := map[podKey][]serviceKey{}
  1083. // For each service, turn the labels into a selector and attempt to
  1084. // match it with each set of pod labels. A match indicates that the pod
  1085. // belongs to the service.
  1086. for sKey, sLabels := range serviceLabels {
  1087. selector := labels.Set(sLabels).AsSelectorPreValidated()
  1088. for pKey, pLabels := range podLabels {
  1089. // If the pod is in a different cluster or namespace, there is
  1090. // no need to compare the labels.
  1091. if sKey.Cluster != pKey.Cluster || sKey.Namespace != pKey.Namespace {
  1092. continue
  1093. }
  1094. podLabelSet := labels.Set(pLabels)
  1095. if selector.Matches(podLabelSet) {
  1096. if _, ok := podServicesMap[pKey]; !ok {
  1097. podServicesMap[pKey] = []serviceKey{}
  1098. }
  1099. podServicesMap[pKey] = append(podServicesMap[pKey], sKey)
  1100. }
  1101. }
  1102. }
  1103. // For each allocation in each pod, attempt to find and apply the list of
  1104. // services associated with the allocation's pod.
  1105. for key, pod := range podMap {
  1106. for _, alloc := range pod.Allocations {
  1107. if sKeys, ok := podServicesMap[key]; ok {
  1108. services := []string{}
  1109. for _, sKey := range sKeys {
  1110. services = append(services, sKey.Service)
  1111. allocsByService[sKey] = append(allocsByService[sKey], alloc)
  1112. }
  1113. alloc.Properties.Services = services
  1114. }
  1115. }
  1116. }
  1117. }
  1118. func applyControllersToPods(podMap map[podKey]*Pod, podControllerMap map[podKey]controllerKey) {
  1119. for key, pod := range podMap {
  1120. for _, alloc := range pod.Allocations {
  1121. if controllerKey, ok := podControllerMap[key]; ok {
  1122. alloc.Properties.ControllerKind = controllerKey.ControllerKind
  1123. alloc.Properties.Controller = controllerKey.Controller
  1124. }
  1125. }
  1126. }
  1127. }
  1128. func applyNodeCostPerCPUHr(nodeMap map[nodeKey]*NodePricing, resNodeCostPerCPUHr []*prom.QueryResult) {
  1129. for _, res := range resNodeCostPerCPUHr {
  1130. cluster, err := res.GetString(env.GetPromClusterLabel())
  1131. if err != nil {
  1132. cluster = env.GetClusterID()
  1133. }
  1134. node, err := res.GetString("node")
  1135. if err != nil {
  1136. log.Warningf("CostModel.ComputeAllocation: Node CPU cost query result missing field: %s", err)
  1137. continue
  1138. }
  1139. instanceType, err := res.GetString("instance_type")
  1140. if err != nil {
  1141. log.Warningf("CostModel.ComputeAllocation: Node CPU cost query result missing field: %s", err)
  1142. continue
  1143. }
  1144. providerID, err := res.GetString("provider_id")
  1145. if err != nil {
  1146. log.Warningf("CostModel.ComputeAllocation: Node CPU cost query result missing field: %s", err)
  1147. continue
  1148. }
  1149. key := newNodeKey(cluster, node)
  1150. if _, ok := nodeMap[key]; !ok {
  1151. nodeMap[key] = &NodePricing{
  1152. Name: node,
  1153. NodeType: instanceType,
  1154. ProviderID: cloud.ParseID(providerID),
  1155. }
  1156. }
  1157. nodeMap[key].CostPerCPUHr = res.Values[0].Value
  1158. }
  1159. }
  1160. func applyNodeCostPerRAMGiBHr(nodeMap map[nodeKey]*NodePricing, resNodeCostPerRAMGiBHr []*prom.QueryResult) {
  1161. for _, res := range resNodeCostPerRAMGiBHr {
  1162. cluster, err := res.GetString(env.GetPromClusterLabel())
  1163. if err != nil {
  1164. cluster = env.GetClusterID()
  1165. }
  1166. node, err := res.GetString("node")
  1167. if err != nil {
  1168. log.Warningf("CostModel.ComputeAllocation: Node RAM cost query result missing field: %s", err)
  1169. continue
  1170. }
  1171. instanceType, err := res.GetString("instance_type")
  1172. if err != nil {
  1173. log.Warningf("CostModel.ComputeAllocation: Node RAM cost query result missing field: %s", err)
  1174. continue
  1175. }
  1176. providerID, err := res.GetString("provider_id")
  1177. if err != nil {
  1178. log.Warningf("CostModel.ComputeAllocation: Node RAM cost query result missing field: %s", err)
  1179. continue
  1180. }
  1181. key := newNodeKey(cluster, node)
  1182. if _, ok := nodeMap[key]; !ok {
  1183. nodeMap[key] = &NodePricing{
  1184. Name: node,
  1185. NodeType: instanceType,
  1186. ProviderID: cloud.ParseID(providerID),
  1187. }
  1188. }
  1189. nodeMap[key].CostPerRAMGiBHr = res.Values[0].Value
  1190. }
  1191. }
  1192. func applyNodeCostPerGPUHr(nodeMap map[nodeKey]*NodePricing, resNodeCostPerGPUHr []*prom.QueryResult) {
  1193. for _, res := range resNodeCostPerGPUHr {
  1194. cluster, err := res.GetString(env.GetPromClusterLabel())
  1195. if err != nil {
  1196. cluster = env.GetClusterID()
  1197. }
  1198. node, err := res.GetString("node")
  1199. if err != nil {
  1200. log.Warningf("CostModel.ComputeAllocation: Node GPU cost query result missing field: %s", err)
  1201. continue
  1202. }
  1203. instanceType, err := res.GetString("instance_type")
  1204. if err != nil {
  1205. log.Warningf("CostModel.ComputeAllocation: Node GPU cost query result missing field: %s", err)
  1206. continue
  1207. }
  1208. providerID, err := res.GetString("provider_id")
  1209. if err != nil {
  1210. log.Warningf("CostModel.ComputeAllocation: Node GPU cost query result missing field: %s", err)
  1211. continue
  1212. }
  1213. key := newNodeKey(cluster, node)
  1214. if _, ok := nodeMap[key]; !ok {
  1215. nodeMap[key] = &NodePricing{
  1216. Name: node,
  1217. NodeType: instanceType,
  1218. ProviderID: cloud.ParseID(providerID),
  1219. }
  1220. }
  1221. nodeMap[key].CostPerGPUHr = res.Values[0].Value
  1222. }
  1223. }
  1224. func applyNodeSpot(nodeMap map[nodeKey]*NodePricing, resNodeIsSpot []*prom.QueryResult) {
  1225. for _, res := range resNodeIsSpot {
  1226. cluster, err := res.GetString(env.GetPromClusterLabel())
  1227. if err != nil {
  1228. cluster = env.GetClusterID()
  1229. }
  1230. node, err := res.GetString("node")
  1231. if err != nil {
  1232. log.Warningf("CostModel.ComputeAllocation: Node spot query result missing field: %s", err)
  1233. continue
  1234. }
  1235. key := newNodeKey(cluster, node)
  1236. if _, ok := nodeMap[key]; !ok {
  1237. log.Warningf("CostModel.ComputeAllocation: Node spot query result for missing node: %s", key)
  1238. continue
  1239. }
  1240. nodeMap[key].Preemptible = res.Values[0].Value > 0
  1241. }
  1242. }
  1243. func applyNodeDiscount(nodeMap map[nodeKey]*NodePricing, cm *CostModel) {
  1244. if cm == nil {
  1245. return
  1246. }
  1247. c, err := cm.Provider.GetConfig()
  1248. if err != nil {
  1249. log.Errorf("CostModel.ComputeAllocation: applyNodeDiscount: %s", err)
  1250. return
  1251. }
  1252. discount, err := ParsePercentString(c.Discount)
  1253. if err != nil {
  1254. log.Errorf("CostModel.ComputeAllocation: applyNodeDiscount: %s", err)
  1255. return
  1256. }
  1257. negotiatedDiscount, err := ParsePercentString(c.NegotiatedDiscount)
  1258. if err != nil {
  1259. log.Errorf("CostModel.ComputeAllocation: applyNodeDiscount: %s", err)
  1260. return
  1261. }
  1262. for _, node := range nodeMap {
  1263. // TODO GKE Reserved Instances into account
  1264. node.Discount = cm.Provider.CombinedDiscountForNode(node.NodeType, node.Preemptible, discount, negotiatedDiscount)
  1265. node.CostPerCPUHr *= (1.0 - node.Discount)
  1266. node.CostPerRAMGiBHr *= (1.0 - node.Discount)
  1267. }
  1268. }
  1269. func buildPVMap(pvMap map[pvKey]*PV, resPVCostPerGiBHour []*prom.QueryResult) {
  1270. for _, res := range resPVCostPerGiBHour {
  1271. cluster, err := res.GetString(env.GetPromClusterLabel())
  1272. if err != nil {
  1273. cluster = env.GetClusterID()
  1274. }
  1275. name, err := res.GetString("volumename")
  1276. if err != nil {
  1277. log.Warningf("CostModel.ComputeAllocation: PV cost without volumename")
  1278. continue
  1279. }
  1280. key := newPVKey(cluster, name)
  1281. pvMap[key] = &PV{
  1282. Cluster: cluster,
  1283. Name: name,
  1284. CostPerGiBHour: res.Values[0].Value,
  1285. }
  1286. }
  1287. }
  1288. func applyPVBytes(pvMap map[pvKey]*PV, resPVBytes []*prom.QueryResult) {
  1289. for _, res := range resPVBytes {
  1290. key, err := resultPVKey(res, env.GetPromClusterLabel(), "persistentvolume")
  1291. if err != nil {
  1292. log.Warningf("CostModel.ComputeAllocation: PV bytes query result missing field: %s", err)
  1293. continue
  1294. }
  1295. if _, ok := pvMap[key]; !ok {
  1296. log.Warningf("CostModel.ComputeAllocation: PV bytes result for missing PV: %s", err)
  1297. continue
  1298. }
  1299. pvMap[key].Bytes = res.Values[0].Value
  1300. }
  1301. }
  1302. func buildPVCMap(window kubecost.Window, pvcMap map[pvcKey]*PVC, pvMap map[pvKey]*PV, resPVCInfo []*prom.QueryResult) {
  1303. for _, res := range resPVCInfo {
  1304. cluster, err := res.GetString(env.GetPromClusterLabel())
  1305. if err != nil {
  1306. cluster = env.GetClusterID()
  1307. }
  1308. values, err := res.GetStrings("persistentvolumeclaim", "storageclass", "volumename", "namespace")
  1309. if err != nil {
  1310. log.DedupedWarningf(10, "CostModel.ComputeAllocation: PVC info query result missing field: %s", err)
  1311. continue
  1312. }
  1313. namespace := values["namespace"]
  1314. name := values["persistentvolumeclaim"]
  1315. volume := values["volumename"]
  1316. storageClass := values["storageclass"]
  1317. pvKey := newPVKey(cluster, volume)
  1318. pvcKey := newPVCKey(cluster, namespace, name)
  1319. // pvcStart and pvcEnd are the timestamps of the first and last minutes
  1320. // the PVC was running, respectively. We subtract 1m from pvcStart
  1321. // because this point will actually represent the end of the first
  1322. // minute. We don't subtract from pvcEnd because it already represents
  1323. // the end of the last minute.
  1324. var pvcStart, pvcEnd time.Time
  1325. for _, datum := range res.Values {
  1326. t := time.Unix(int64(datum.Timestamp), 0)
  1327. if pvcStart.IsZero() && datum.Value > 0 && window.Contains(t) {
  1328. pvcStart = t
  1329. }
  1330. if datum.Value > 0 && window.Contains(t) {
  1331. pvcEnd = t
  1332. }
  1333. }
  1334. if pvcStart.IsZero() || pvcEnd.IsZero() {
  1335. log.Warningf("CostModel.ComputeAllocation: PVC %s has no running time", pvcKey)
  1336. }
  1337. pvcStart = pvcStart.Add(-time.Minute)
  1338. if _, ok := pvMap[pvKey]; !ok {
  1339. continue
  1340. }
  1341. pvMap[pvKey].StorageClass = storageClass
  1342. if _, ok := pvcMap[pvcKey]; !ok {
  1343. pvcMap[pvcKey] = &PVC{}
  1344. }
  1345. pvcMap[pvcKey].Name = name
  1346. pvcMap[pvcKey].Namespace = namespace
  1347. pvcMap[pvcKey].Volume = pvMap[pvKey]
  1348. pvcMap[pvcKey].Start = pvcStart
  1349. pvcMap[pvcKey].End = pvcEnd
  1350. }
  1351. }
  1352. func applyPVCBytesRequested(pvcMap map[pvcKey]*PVC, resPVCBytesRequested []*prom.QueryResult) {
  1353. for _, res := range resPVCBytesRequested {
  1354. key, err := resultPVCKey(res, env.GetPromClusterLabel(), "namespace", "persistentvolumeclaim")
  1355. if err != nil {
  1356. continue
  1357. }
  1358. if _, ok := pvcMap[key]; !ok {
  1359. continue
  1360. }
  1361. pvcMap[key].Bytes = res.Values[0].Value
  1362. }
  1363. }
  1364. func buildPodPVCMap(podPVCMap map[podKey][]*PVC, pvMap map[pvKey]*PV, pvcMap map[pvcKey]*PVC, podMap map[podKey]*Pod, resPodPVCAllocation []*prom.QueryResult) {
  1365. for _, res := range resPodPVCAllocation {
  1366. cluster, err := res.GetString(env.GetPromClusterLabel())
  1367. if err != nil {
  1368. cluster = env.GetClusterID()
  1369. }
  1370. values, err := res.GetStrings("persistentvolume", "persistentvolumeclaim", "pod", "namespace")
  1371. if err != nil {
  1372. log.DedupedWarningf(5, "CostModel.ComputeAllocation: PVC allocation query result missing field: %s", err)
  1373. continue
  1374. }
  1375. namespace := values["namespace"]
  1376. pod := values["pod"]
  1377. name := values["persistentvolumeclaim"]
  1378. volume := values["persistentvolume"]
  1379. podKey := newPodKey(cluster, namespace, pod)
  1380. pvKey := newPVKey(cluster, volume)
  1381. pvcKey := newPVCKey(cluster, namespace, name)
  1382. if _, ok := pvMap[pvKey]; !ok {
  1383. log.DedupedWarningf(5, "CostModel.ComputeAllocation: PV missing for PVC allocation query result: %s", pvKey)
  1384. continue
  1385. }
  1386. if _, ok := podPVCMap[podKey]; !ok {
  1387. podPVCMap[podKey] = []*PVC{}
  1388. }
  1389. pvc, ok := pvcMap[pvcKey]
  1390. if !ok {
  1391. log.DedupedWarningf(5, "CostModel.ComputeAllocation: PVC missing for PVC allocation query: %s", pvcKey)
  1392. continue
  1393. }
  1394. count := 1
  1395. if pod, ok := podMap[podKey]; ok && len(pod.Allocations) > 0 {
  1396. count = len(pod.Allocations)
  1397. } else {
  1398. log.DedupedWarningf(10, "CostModel.ComputeAllocation: PVC %s for missing pod %s", pvcKey, podKey)
  1399. }
  1400. pvc.Count = count
  1401. pvc.Mounted = true
  1402. podPVCMap[podKey] = append(podPVCMap[podKey], pvc)
  1403. }
  1404. }
  1405. func applyUnmountedPVs(window kubecost.Window, podMap map[podKey]*Pod, pvMap map[pvKey]*PV, pvcMap map[pvcKey]*PVC) {
  1406. unmountedPVBytes := map[string]float64{}
  1407. unmountedPVCost := map[string]float64{}
  1408. for _, pv := range pvMap {
  1409. mounted := false
  1410. for _, pvc := range pvcMap {
  1411. if pvc.Volume == nil {
  1412. continue
  1413. }
  1414. if pvc.Volume == pv {
  1415. mounted = true
  1416. break
  1417. }
  1418. }
  1419. if !mounted {
  1420. gib := pv.Bytes / 1024 / 1024 / 1024
  1421. hrs := window.Minutes() / 60.0 // TODO improve with PV hours, not window hours
  1422. cost := pv.CostPerGiBHour * gib * hrs
  1423. unmountedPVCost[pv.Cluster] += cost
  1424. unmountedPVBytes[pv.Cluster] += pv.Bytes
  1425. }
  1426. }
  1427. for cluster, amount := range unmountedPVCost {
  1428. container := kubecost.UnmountedSuffix
  1429. pod := kubecost.UnmountedSuffix
  1430. namespace := kubecost.UnmountedSuffix
  1431. node := ""
  1432. key := newPodKey(cluster, namespace, pod)
  1433. podMap[key] = &Pod{
  1434. Window: window.Clone(),
  1435. Start: *window.Start(),
  1436. End: *window.End(),
  1437. Key: key,
  1438. Allocations: map[string]*kubecost.Allocation{},
  1439. }
  1440. podMap[key].AppendContainer(container)
  1441. podMap[key].Allocations[container].Properties.Cluster = cluster
  1442. podMap[key].Allocations[container].Properties.Node = node
  1443. podMap[key].Allocations[container].Properties.Namespace = namespace
  1444. podMap[key].Allocations[container].Properties.Pod = pod
  1445. podMap[key].Allocations[container].Properties.Container = container
  1446. pvKey := kubecost.PVKey{
  1447. Cluster: cluster,
  1448. Name: kubecost.UnmountedSuffix,
  1449. }
  1450. unmountedPVs := kubecost.PVAllocations{
  1451. pvKey: {
  1452. ByteHours: unmountedPVBytes[cluster] * window.Minutes() / 60.0,
  1453. Cost: amount,
  1454. },
  1455. }
  1456. podMap[key].Allocations[container].PVs = podMap[key].Allocations[container].PVs.Add(unmountedPVs)
  1457. }
  1458. }
  1459. func applyUnmountedPVCs(window kubecost.Window, podMap map[podKey]*Pod, pvcMap map[pvcKey]*PVC) {
  1460. unmountedPVCBytes := map[namespaceKey]float64{}
  1461. unmountedPVCCost := map[namespaceKey]float64{}
  1462. for _, pvc := range pvcMap {
  1463. if !pvc.Mounted && pvc.Volume != nil {
  1464. key := newNamespaceKey(pvc.Cluster, pvc.Namespace)
  1465. gib := pvc.Volume.Bytes / 1024 / 1024 / 1024
  1466. hrs := pvc.Minutes() / 60.0
  1467. cost := pvc.Volume.CostPerGiBHour * gib * hrs
  1468. unmountedPVCCost[key] += cost
  1469. unmountedPVCBytes[key] += pvc.Volume.Bytes
  1470. }
  1471. }
  1472. for key, amount := range unmountedPVCCost {
  1473. container := kubecost.UnmountedSuffix
  1474. pod := kubecost.UnmountedSuffix
  1475. namespace := key.Namespace
  1476. node := ""
  1477. cluster := key.Cluster
  1478. podKey := newPodKey(cluster, namespace, pod)
  1479. podMap[podKey] = &Pod{
  1480. Window: window.Clone(),
  1481. Start: *window.Start(),
  1482. End: *window.End(),
  1483. Key: podKey,
  1484. Allocations: map[string]*kubecost.Allocation{},
  1485. }
  1486. podMap[podKey].AppendContainer(container)
  1487. podMap[podKey].Allocations[container].Properties.Cluster = cluster
  1488. podMap[podKey].Allocations[container].Properties.Node = node
  1489. podMap[podKey].Allocations[container].Properties.Namespace = namespace
  1490. podMap[podKey].Allocations[container].Properties.Pod = pod
  1491. podMap[podKey].Allocations[container].Properties.Container = container
  1492. pvKey := kubecost.PVKey{
  1493. Cluster: cluster,
  1494. Name: kubecost.UnmountedSuffix,
  1495. }
  1496. unmountedPVs := kubecost.PVAllocations{
  1497. pvKey: {
  1498. ByteHours: unmountedPVCBytes[key] * window.Minutes() / 60.0,
  1499. Cost: amount,
  1500. },
  1501. }
  1502. podMap[podKey].Allocations[container].PVs = podMap[podKey].Allocations[container].PVs.Add(unmountedPVs)
  1503. }
  1504. }
  1505. // LB describes the start and end time of a Load Balancer along with cost
  1506. type LB struct {
  1507. TotalCost float64
  1508. Start time.Time
  1509. End time.Time
  1510. }
  1511. func getLoadBalancerCosts(resLBCost, resLBActiveMins []*prom.QueryResult, resolution time.Duration) map[serviceKey]*LB {
  1512. lbMap := make(map[serviceKey]*LB)
  1513. lbHourlyCosts := make(map[serviceKey]float64)
  1514. for _, res := range resLBCost {
  1515. serviceKey, err := resultServiceKey(res, env.GetPromClusterLabel(), "namespace", "service_name")
  1516. if err != nil {
  1517. continue
  1518. }
  1519. lbHourlyCosts[serviceKey] = res.Values[0].Value
  1520. }
  1521. for _, res := range resLBActiveMins {
  1522. serviceKey, err := resultServiceKey(res, env.GetPromClusterLabel(), "namespace", "service_name")
  1523. if err != nil || len(res.Values) == 0 {
  1524. continue
  1525. }
  1526. if _, ok := lbHourlyCosts[serviceKey]; !ok {
  1527. log.Warningf("CostModel: failed to find hourly cost for Load Balancer: %v", serviceKey)
  1528. continue
  1529. }
  1530. s := time.Unix(int64(res.Values[0].Timestamp), 0)
  1531. // subtract resolution from start time to cover full time period
  1532. s = s.Add(-resolution)
  1533. e := time.Unix(int64(res.Values[len(res.Values)-1].Timestamp), 0)
  1534. hours := e.Sub(s).Hours()
  1535. lbMap[serviceKey] = &LB{
  1536. TotalCost: lbHourlyCosts[serviceKey] * hours,
  1537. Start: s,
  1538. End: e,
  1539. }
  1540. }
  1541. return lbMap
  1542. }
  1543. func applyLoadBalancersToPods(lbMap map[serviceKey]*LB, allocsByService map[serviceKey][]*kubecost.Allocation) {
  1544. for sKey, lb := range lbMap {
  1545. totalHours := 0.0
  1546. allocHours := make(map[*kubecost.Allocation]float64)
  1547. // Add portion of load balancing cost to each allocation
  1548. // proportional to the total number of hours allocations used the load balancer
  1549. for _, alloc := range allocsByService[sKey] {
  1550. // Determine the (start, end) of the relationship between the
  1551. // given LB and the associated Allocation so that a precise
  1552. // number of hours can be used to compute cumulative cost.
  1553. s, e := alloc.Start, alloc.End
  1554. if lb.Start.After(alloc.Start) {
  1555. s = lb.Start
  1556. }
  1557. if lb.End.Before(alloc.End) {
  1558. e = lb.End
  1559. }
  1560. hours := e.Sub(s).Hours()
  1561. // A negative number of hours signifies no overlap between the windows
  1562. if hours > 0 {
  1563. totalHours += hours
  1564. allocHours[alloc] = hours
  1565. }
  1566. }
  1567. // Distribute cost of service once total hours is calculated
  1568. for alloc, hours := range allocHours {
  1569. alloc.LoadBalancerCost += lb.TotalCost * hours / totalHours
  1570. }
  1571. }
  1572. }
  1573. // getNodePricing determines node pricing, given a key and a mapping from keys
  1574. // to their NodePricing instances, as well as the custom pricing configuration
  1575. // inherent to the CostModel instance. If custom pricing is set, use that. If
  1576. // not, use the pricing defined by the given key. If that doesn't exist, fall
  1577. // back on custom pricing as a default.
  1578. func (cm *CostModel) getNodePricing(nodeMap map[nodeKey]*NodePricing, nodeKey nodeKey) *NodePricing {
  1579. // Find the relevant NodePricing, if it exists. If not, substitute the
  1580. // custom NodePricing as a default.
  1581. node, ok := nodeMap[nodeKey]
  1582. if !ok || node == nil {
  1583. if nodeKey.Node != "" {
  1584. log.DedupedWarningf(5, "CostModel: failed to find node for %s", nodeKey)
  1585. }
  1586. return cm.getCustomNodePricing(false)
  1587. }
  1588. // If custom pricing is enabled and can be retrieved, override detected
  1589. // node pricing with the custom values.
  1590. customPricingConfig, err := cm.Provider.GetConfig()
  1591. if err != nil {
  1592. log.Warningf("CostModel: failed to load custom pricing: %s", err)
  1593. }
  1594. if cloud.CustomPricesEnabled(cm.Provider) && customPricingConfig != nil {
  1595. return cm.getCustomNodePricing(node.Preemptible)
  1596. }
  1597. node.Source = "prometheus"
  1598. // If any of the values are NaN or zero, replace them with the custom
  1599. // values as default.
  1600. // TODO:CLEANUP can't we parse these custom prices once? why do we store
  1601. // them as strings like this?
  1602. if node.CostPerCPUHr == 0 || math.IsNaN(node.CostPerCPUHr) {
  1603. log.Warningf("CostModel: node pricing has illegal CostPerCPUHr; replacing with custom pricing: %s", nodeKey)
  1604. cpuCostStr := customPricingConfig.CPU
  1605. if node.Preemptible {
  1606. cpuCostStr = customPricingConfig.SpotCPU
  1607. }
  1608. costPerCPUHr, err := strconv.ParseFloat(cpuCostStr, 64)
  1609. if err != nil {
  1610. log.Warningf("CostModel: custom pricing has illegal CPU cost: %s", cpuCostStr)
  1611. }
  1612. node.CostPerCPUHr = costPerCPUHr
  1613. node.Source += "/customCPU"
  1614. }
  1615. if math.IsNaN(node.CostPerGPUHr) {
  1616. log.Warningf("CostModel: node pricing has illegal CostPerGPUHr; replacing with custom pricing: %s", nodeKey)
  1617. gpuCostStr := customPricingConfig.GPU
  1618. if node.Preemptible {
  1619. gpuCostStr = customPricingConfig.SpotGPU
  1620. }
  1621. costPerGPUHr, err := strconv.ParseFloat(gpuCostStr, 64)
  1622. if err != nil {
  1623. log.Warningf("CostModel: custom pricing has illegal GPU cost: %s", gpuCostStr)
  1624. }
  1625. node.CostPerGPUHr = costPerGPUHr
  1626. node.Source += "/customGPU"
  1627. }
  1628. if node.CostPerRAMGiBHr == 0 || math.IsNaN(node.CostPerRAMGiBHr) {
  1629. log.Warningf("CostModel: node pricing has illegal CostPerRAMHr; replacing with custom pricing: %s", nodeKey)
  1630. ramCostStr := customPricingConfig.RAM
  1631. if node.Preemptible {
  1632. ramCostStr = customPricingConfig.SpotRAM
  1633. }
  1634. costPerRAMHr, err := strconv.ParseFloat(ramCostStr, 64)
  1635. if err != nil {
  1636. log.Warningf("CostModel: custom pricing has illegal RAM cost: %s", ramCostStr)
  1637. }
  1638. node.CostPerRAMGiBHr = costPerRAMHr
  1639. node.Source += "/customRAM"
  1640. }
  1641. return node
  1642. }
  1643. // getCustomNodePricing converts the CostModel's configured custom pricing
  1644. // values into a NodePricing instance.
  1645. func (cm *CostModel) getCustomNodePricing(spot bool) *NodePricing {
  1646. customPricingConfig, err := cm.Provider.GetConfig()
  1647. if err != nil {
  1648. return nil
  1649. }
  1650. cpuCostStr := customPricingConfig.CPU
  1651. gpuCostStr := customPricingConfig.GPU
  1652. ramCostStr := customPricingConfig.RAM
  1653. if spot {
  1654. cpuCostStr = customPricingConfig.SpotCPU
  1655. gpuCostStr = customPricingConfig.SpotGPU
  1656. ramCostStr = customPricingConfig.SpotRAM
  1657. }
  1658. node := &NodePricing{Source: "custom"}
  1659. costPerCPUHr, err := strconv.ParseFloat(cpuCostStr, 64)
  1660. if err != nil {
  1661. log.Warningf("CostModel: custom pricing has illegal CPU cost: %s", cpuCostStr)
  1662. }
  1663. node.CostPerCPUHr = costPerCPUHr
  1664. costPerGPUHr, err := strconv.ParseFloat(gpuCostStr, 64)
  1665. if err != nil {
  1666. log.Warningf("CostModel: custom pricing has illegal GPU cost: %s", gpuCostStr)
  1667. }
  1668. node.CostPerGPUHr = costPerGPUHr
  1669. costPerRAMHr, err := strconv.ParseFloat(ramCostStr, 64)
  1670. if err != nil {
  1671. log.Warningf("CostModel: custom pricing has illegal RAM cost: %s", ramCostStr)
  1672. }
  1673. node.CostPerRAMGiBHr = costPerRAMHr
  1674. return node
  1675. }
  1676. // NodePricing describes the resource costs associated with a given node, as
  1677. // well as the source of the information (e.g. prometheus, custom)
  1678. type NodePricing struct {
  1679. Name string
  1680. NodeType string
  1681. ProviderID string
  1682. Preemptible bool
  1683. CostPerCPUHr float64
  1684. CostPerRAMGiBHr float64
  1685. CostPerGPUHr float64
  1686. Discount float64
  1687. Source string
  1688. }
  1689. // Pod describes a running pod's start and end time within a Window and
  1690. // all the Allocations (i.e. containers) contained within it.
  1691. type Pod struct {
  1692. Window kubecost.Window
  1693. Start time.Time
  1694. End time.Time
  1695. Key podKey
  1696. Allocations map[string]*kubecost.Allocation
  1697. }
  1698. // AppendContainer adds an entry for the given container name to the Pod.
  1699. func (p Pod) AppendContainer(container string) {
  1700. name := fmt.Sprintf("%s/%s/%s/%s", p.Key.Cluster, p.Key.Namespace, p.Key.Pod, container)
  1701. alloc := &kubecost.Allocation{
  1702. Name: name,
  1703. Properties: &kubecost.AllocationProperties{},
  1704. Window: p.Window.Clone(),
  1705. Start: p.Start,
  1706. End: p.End,
  1707. }
  1708. alloc.Properties.Container = container
  1709. alloc.Properties.Pod = p.Key.Pod
  1710. alloc.Properties.Namespace = p.Key.Namespace
  1711. alloc.Properties.Cluster = p.Key.Cluster
  1712. p.Allocations[container] = alloc
  1713. }
  1714. // PVC describes a PersistentVolumeClaim
  1715. // TODO:CLEANUP move to pkg/kubecost?
  1716. // TODO:CLEANUP add PersistentVolumeClaims field to type Allocation?
  1717. type PVC struct {
  1718. Bytes float64 `json:"bytes"`
  1719. Count int `json:"count"`
  1720. Name string `json:"name"`
  1721. Cluster string `json:"cluster"`
  1722. Namespace string `json:"namespace"`
  1723. Volume *PV `json:"persistentVolume"`
  1724. Mounted bool `json:"mounted"`
  1725. Start time.Time `json:"start"`
  1726. End time.Time `json:"end"`
  1727. }
  1728. // Cost computes the cumulative cost of the PVC
  1729. func (pvc *PVC) Cost() float64 {
  1730. if pvc == nil || pvc.Volume == nil {
  1731. return 0.0
  1732. }
  1733. gib := pvc.Bytes / 1024 / 1024 / 1024
  1734. hrs := pvc.Minutes() / 60.0
  1735. return pvc.Volume.CostPerGiBHour * gib * hrs
  1736. }
  1737. // Minutes computes the number of minutes over which the PVC is defined
  1738. func (pvc *PVC) Minutes() float64 {
  1739. if pvc == nil {
  1740. return 0.0
  1741. }
  1742. return pvc.End.Sub(pvc.Start).Minutes()
  1743. }
  1744. // String returns a string representation of the PVC
  1745. func (pvc *PVC) String() string {
  1746. if pvc == nil {
  1747. return "<nil>"
  1748. }
  1749. return fmt.Sprintf("%s/%s/%s{Bytes:%.2f, Cost:%.6f, Start,End:%s}", pvc.Cluster, pvc.Namespace, pvc.Name, pvc.Bytes, pvc.Cost(), kubecost.NewWindow(&pvc.Start, &pvc.End))
  1750. }
  1751. // PV describes a PersistentVolume
  1752. // TODO:CLEANUP move to pkg/kubecost?
  1753. type PV struct {
  1754. Bytes float64 `json:"bytes"`
  1755. CostPerGiBHour float64 `json:"costPerGiBHour"`
  1756. Cluster string `json:"cluster"`
  1757. Name string `json:"name"`
  1758. StorageClass string `json:"storageClass"`
  1759. }
  1760. // String returns a string representation of the PV
  1761. func (pv *PV) String() string {
  1762. if pv == nil {
  1763. return "<nil>"
  1764. }
  1765. return fmt.Sprintf("%s/%s{Bytes:%.2f, Cost/GiB*Hr:%.6f, StorageClass:%s}", pv.Cluster, pv.Name, pv.Bytes, pv.CostPerGiBHour, pv.StorageClass)
  1766. }