allocation.go 88 KB

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