allocation.go 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683
  1. package costmodel
  2. import (
  3. "fmt"
  4. "time"
  5. "github.com/kubecost/cost-model/pkg/env"
  6. "github.com/kubecost/cost-model/pkg/kubecost"
  7. "github.com/kubecost/cost-model/pkg/log"
  8. "github.com/kubecost/cost-model/pkg/prom"
  9. "github.com/kubecost/cost-model/pkg/util"
  10. "k8s.io/apimachinery/pkg/labels"
  11. )
  12. const (
  13. queryFmtPods = `avg(kube_pod_container_status_running{}) by (pod, namespace, cluster_id)[%s:%s]%s`
  14. queryFmtRAMBytesAllocated = `avg(avg_over_time(container_memory_allocation_bytes{container!="", container!="POD", node!=""}[%s]%s)) by (container, pod, namespace, node, cluster_id)`
  15. queryFmtRAMRequests = `avg(avg_over_time(kube_pod_container_resource_requests_memory_bytes{container!="", container!="POD", node!=""}[%s]%s)) by (container, pod, namespace, node, cluster_id)`
  16. queryFmtRAMUsage = `avg(avg_over_time(container_memory_working_set_bytes{container_name!="", container_name!="POD", instance!=""}[%s]%s)) by (container_name, pod_name, namespace, instance, cluster_id)`
  17. queryFmtCPUCoresAllocated = `avg(avg_over_time(container_cpu_allocation{container!="", container!="POD", node!=""}[%s]%s)) by (container, pod, namespace, node, cluster_id)`
  18. queryFmtCPURequests = `avg(avg_over_time(kube_pod_container_resource_requests_cpu_cores{container!="", container!="POD", node!=""}[%s]%s)) by (container, pod, namespace, node, cluster_id)`
  19. queryFmtCPUUsage = `avg(rate(container_cpu_usage_seconds_total{container_name!="", container_name!="POD", instance!=""}[%s]%s)) by (container_name, pod_name, namespace, instance, cluster_id)`
  20. 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, cluster_id)`
  21. queryFmtNodeCostPerCPUHr = `avg(avg_over_time(node_cpu_hourly_cost[%s]%s)) by (node, cluster_id, instance_type)`
  22. queryFmtNodeCostPerRAMGiBHr = `avg(avg_over_time(node_ram_hourly_cost[%s]%s)) by (node, cluster_id, instance_type)`
  23. queryFmtNodeCostPerGPUHr = `avg(avg_over_time(node_gpu_hourly_cost[%s]%s)) by (node, cluster_id, instance_type)`
  24. queryFmtNodeIsSpot = `avg_over_time(kubecost_node_is_spot[%s]%s)`
  25. queryFmtPVCInfo = `avg(kube_persistentvolumeclaim_info{volumename != ""}) by (persistentvolumeclaim, storageclass, volumename, namespace, cluster_id)[%s:%s]%s`
  26. queryFmtPVBytes = `avg(avg_over_time(kube_persistentvolume_capacity_bytes[%s]%s)) by (persistentvolume, cluster_id)`
  27. queryFmtPodPVCAllocation = `avg(avg_over_time(pod_pvc_allocation[%s]%s)) by (persistentvolume, persistentvolumeclaim, pod, namespace, cluster_id)`
  28. queryFmtPVCBytesRequested = `avg(avg_over_time(kube_persistentvolumeclaim_resource_requests_storage_bytes{}[%s]%s)) by (persistentvolumeclaim, namespace, cluster_id)`
  29. queryFmtPVCostPerGiBHour = `avg(avg_over_time(pv_hourly_cost[%s]%s)) by (volumename, cluster_id)`
  30. queryFmtNetZoneGiB = `sum(increase(kubecost_pod_network_egress_bytes_total{internet="false", sameZone="false", sameRegion="true"}[%s]%s)) by (pod_name, namespace, cluster_id) / 1024 / 1024 / 1024`
  31. queryFmtNetZoneCostPerGiB = `avg(avg_over_time(kubecost_network_zone_egress_cost{}[%s]%s)) by (cluster_id)`
  32. queryFmtNetRegionGiB = `sum(increase(kubecost_pod_network_egress_bytes_total{internet="false", sameZone="false", sameRegion="false"}[%s]%s)) by (pod_name, namespace, cluster_id) / 1024 / 1024 / 1024`
  33. queryFmtNetRegionCostPerGiB = `avg(avg_over_time(kubecost_network_region_egress_cost{}[%s]%s)) by (cluster_id)`
  34. queryFmtNetInternetGiB = `sum(increase(kubecost_pod_network_egress_bytes_total{internet="true"}[%s]%s)) by (pod_name, namespace, cluster_id) / 1024 / 1024 / 1024`
  35. queryFmtNetInternetCostPerGiB = `avg(avg_over_time(kubecost_network_internet_egress_cost{}[%s]%s)) by (cluster_id)`
  36. queryFmtNamespaceLabels = `avg_over_time(kube_namespace_labels[%s]%s)`
  37. queryFmtNamespaceAnnotations = `avg_over_time(kube_namespace_annotations[%s]%s)`
  38. queryFmtPodLabels = `avg_over_time(kube_pod_labels[%s]%s)`
  39. queryFmtPodAnnotations = `avg_over_time(kube_pod_annotations[%s]%s)`
  40. queryFmtServiceLabels = `avg_over_time(service_selector_labels[%s]%s)`
  41. queryFmtDeploymentLabels = `avg_over_time(deployment_match_labels[%s]%s)`
  42. queryFmtStatefulSetLabels = `avg_over_time(statefulSet_match_labels[%s]%s)`
  43. queryFmtDaemonSetLabels = `sum(avg_over_time(kube_pod_owner{owner_kind="DaemonSet"}[%s]%s)) by (pod, owner_name, namespace, cluster_id)`
  44. queryFmtJobLabels = `sum(avg_over_time(kube_pod_owner{owner_kind="Job"}[%s]%s)) by (pod, owner_name, namespace ,cluster_id)`
  45. )
  46. // ComputeAllocation uses the CostModel instance to compute an AllocationSet
  47. // for the window defined by the given start and end times. The Allocations
  48. // returned are unaggregated (i.e. down to the container level).
  49. func (cm *CostModel) ComputeAllocation(start, end time.Time, resolution time.Duration) (*kubecost.AllocationSet, error) {
  50. // 1. Build out Pod map from resolution-tuned, batched Pod start/end query
  51. // 2. Run and apply the results of the remaining queries to
  52. // 3. Build out AllocationSet from completed Pod map
  53. // Create a window spanning the requested query
  54. window := kubecost.NewWindow(&start, &end)
  55. // Create an empty AllocationSet. For safety, in the case of an error, we
  56. // should prefer to return this empty set with the error. (In the case of
  57. // no error, of course we populate the set and return it.)
  58. allocSet := kubecost.NewAllocationSet(start, end)
  59. // (1) Build out Pod map
  60. // Build out a map of Allocations as a mapping from pod-to-container-to-
  61. // underlying-Allocation instance, starting with (start, end) so that we
  62. // begin with minutes, from which we compute resource allocation and cost
  63. // totals from measured rate data.
  64. podMap := map[podKey]*Pod{}
  65. // clusterStarts and clusterEnds record the earliest start and latest end
  66. // times, respectively, on a cluster-basis. These are used for unmounted
  67. // PVs and other "virtual" Allocations so that minutes are maximally
  68. // accurate during start-up or spin-down of a cluster
  69. clusterStart := map[string]time.Time{}
  70. clusterEnd := map[string]time.Time{}
  71. // TODO niko/computeallocation make this configurable?
  72. batchSize := 6 * time.Hour
  73. cm.buildPodMap(window, resolution, batchSize, podMap, clusterStart, clusterEnd)
  74. // (2) Run and apply remaining queries
  75. // Convert window (start, end) to (duration, offset) for querying Prometheus,
  76. // including handling Thanos offset
  77. durStr, offStr, err := window.DurationOffsetForPrometheus()
  78. if err != nil {
  79. // Negative duration, so return empty set
  80. return allocSet, nil
  81. }
  82. // Convert resolution duration to a query-ready string
  83. resStr := util.DurationString(resolution)
  84. ctx := prom.NewContext(cm.PrometheusClient)
  85. startQuerying := time.Now()
  86. // TODO niko/computeallocation split into required and optional queries?
  87. queryRAMBytesAllocated := fmt.Sprintf(queryFmtRAMBytesAllocated, durStr, offStr)
  88. resChRAMBytesAllocated := ctx.Query(queryRAMBytesAllocated)
  89. queryRAMRequests := fmt.Sprintf(queryFmtRAMRequests, durStr, offStr)
  90. resChRAMRequests := ctx.Query(queryRAMRequests)
  91. queryRAMUsage := fmt.Sprintf(queryFmtRAMUsage, durStr, offStr)
  92. resChRAMUsage := ctx.Query(queryRAMUsage)
  93. queryCPUCoresAllocated := fmt.Sprintf(queryFmtCPUCoresAllocated, durStr, offStr)
  94. resChCPUCoresAllocated := ctx.Query(queryCPUCoresAllocated)
  95. queryCPURequests := fmt.Sprintf(queryFmtCPURequests, durStr, offStr)
  96. resChCPURequests := ctx.Query(queryCPURequests)
  97. queryCPUUsage := fmt.Sprintf(queryFmtCPUUsage, durStr, offStr)
  98. resChCPUUsage := ctx.Query(queryCPUUsage)
  99. queryGPUsRequested := fmt.Sprintf(queryFmtGPUsRequested, durStr, offStr)
  100. resChGPUsRequested := ctx.Query(queryGPUsRequested)
  101. queryNodeCostPerCPUHr := fmt.Sprintf(queryFmtNodeCostPerCPUHr, durStr, offStr)
  102. resChNodeCostPerCPUHr := ctx.Query(queryNodeCostPerCPUHr)
  103. queryNodeCostPerRAMGiBHr := fmt.Sprintf(queryFmtNodeCostPerRAMGiBHr, durStr, offStr)
  104. resChNodeCostPerRAMGiBHr := ctx.Query(queryNodeCostPerRAMGiBHr)
  105. queryNodeCostPerGPUHr := fmt.Sprintf(queryFmtNodeCostPerGPUHr, durStr, offStr)
  106. resChNodeCostPerGPUHr := ctx.Query(queryNodeCostPerGPUHr)
  107. queryNodeIsSpot := fmt.Sprintf(queryFmtNodeIsSpot, durStr, offStr)
  108. resChNodeIsSpot := ctx.Query(queryNodeIsSpot)
  109. queryPVCInfo := fmt.Sprintf(queryFmtPVCInfo, durStr, resStr, offStr)
  110. resChPVCInfo := ctx.Query(queryPVCInfo)
  111. queryPVBytes := fmt.Sprintf(queryFmtPVBytes, durStr, offStr)
  112. resChPVBytes := ctx.Query(queryPVBytes)
  113. queryPodPVCAllocation := fmt.Sprintf(queryFmtPodPVCAllocation, durStr, offStr)
  114. resChPodPVCAllocation := ctx.Query(queryPodPVCAllocation)
  115. queryPVCBytesRequested := fmt.Sprintf(queryFmtPVCBytesRequested, durStr, offStr)
  116. resChPVCBytesRequested := ctx.Query(queryPVCBytesRequested)
  117. queryPVCostPerGiBHour := fmt.Sprintf(queryFmtPVCostPerGiBHour, durStr, offStr)
  118. resChPVCostPerGiBHour := ctx.Query(queryPVCostPerGiBHour)
  119. queryNetZoneGiB := fmt.Sprintf(queryFmtNetZoneGiB, durStr, offStr)
  120. resChNetZoneGiB := ctx.Query(queryNetZoneGiB)
  121. queryNetZoneCostPerGiB := fmt.Sprintf(queryFmtNetZoneCostPerGiB, durStr, offStr)
  122. resChNetZoneCostPerGiB := ctx.Query(queryNetZoneCostPerGiB)
  123. queryNetRegionGiB := fmt.Sprintf(queryFmtNetRegionGiB, durStr, offStr)
  124. resChNetRegionGiB := ctx.Query(queryNetRegionGiB)
  125. queryNetRegionCostPerGiB := fmt.Sprintf(queryFmtNetRegionCostPerGiB, durStr, offStr)
  126. resChNetRegionCostPerGiB := ctx.Query(queryNetRegionCostPerGiB)
  127. queryNetInternetGiB := fmt.Sprintf(queryFmtNetInternetGiB, durStr, offStr)
  128. resChNetInternetGiB := ctx.Query(queryNetInternetGiB)
  129. queryNetInternetCostPerGiB := fmt.Sprintf(queryFmtNetInternetCostPerGiB, durStr, offStr)
  130. resChNetInternetCostPerGiB := ctx.Query(queryNetInternetCostPerGiB)
  131. queryNamespaceLabels := fmt.Sprintf(queryFmtNamespaceLabels, durStr, offStr)
  132. resChNamespaceLabels := ctx.Query(queryNamespaceLabels)
  133. queryNamespaceAnnotations := fmt.Sprintf(queryFmtNamespaceAnnotations, durStr, offStr)
  134. resChNamespaceAnnotations := ctx.Query(queryNamespaceAnnotations)
  135. queryPodLabels := fmt.Sprintf(queryFmtPodLabels, durStr, offStr)
  136. resChPodLabels := ctx.Query(queryPodLabels)
  137. queryPodAnnotations := fmt.Sprintf(queryFmtPodAnnotations, durStr, offStr)
  138. resChPodAnnotations := ctx.Query(queryPodAnnotations)
  139. queryServiceLabels := fmt.Sprintf(queryFmtServiceLabels, durStr, offStr)
  140. resChServiceLabels := ctx.Query(queryServiceLabels)
  141. queryDeploymentLabels := fmt.Sprintf(queryFmtDeploymentLabels, durStr, offStr)
  142. resChDeploymentLabels := ctx.Query(queryDeploymentLabels)
  143. queryStatefulSetLabels := fmt.Sprintf(queryFmtStatefulSetLabels, durStr, offStr)
  144. resChStatefulSetLabels := ctx.Query(queryStatefulSetLabels)
  145. queryDaemonSetLabels := fmt.Sprintf(queryFmtDaemonSetLabels, durStr, offStr)
  146. resChDaemonSetLabels := ctx.Query(queryDaemonSetLabels)
  147. queryJobLabels := fmt.Sprintf(queryFmtJobLabels, durStr, offStr)
  148. resChJobLabels := ctx.Query(queryJobLabels)
  149. resCPUCoresAllocated, _ := resChCPUCoresAllocated.Await()
  150. resCPURequests, _ := resChCPURequests.Await()
  151. resCPUUsage, _ := resChCPUUsage.Await()
  152. resRAMBytesAllocated, _ := resChRAMBytesAllocated.Await()
  153. resRAMRequests, _ := resChRAMRequests.Await()
  154. resRAMUsage, _ := resChRAMUsage.Await()
  155. resGPUsRequested, _ := resChGPUsRequested.Await()
  156. resNodeCostPerCPUHr, _ := resChNodeCostPerCPUHr.Await()
  157. resNodeCostPerRAMGiBHr, _ := resChNodeCostPerRAMGiBHr.Await()
  158. resNodeCostPerGPUHr, _ := resChNodeCostPerGPUHr.Await()
  159. resNodeIsSpot, _ := resChNodeIsSpot.Await()
  160. resPVBytes, _ := resChPVBytes.Await()
  161. resPVCostPerGiBHour, _ := resChPVCostPerGiBHour.Await()
  162. resPVCInfo, _ := resChPVCInfo.Await()
  163. resPVCBytesRequested, _ := resChPVCBytesRequested.Await()
  164. resPodPVCAllocation, _ := resChPodPVCAllocation.Await()
  165. resNetZoneGiB, _ := resChNetZoneGiB.Await()
  166. resNetZoneCostPerGiB, _ := resChNetZoneCostPerGiB.Await()
  167. resNetRegionGiB, _ := resChNetRegionGiB.Await()
  168. resNetRegionCostPerGiB, _ := resChNetRegionCostPerGiB.Await()
  169. resNetInternetGiB, _ := resChNetInternetGiB.Await()
  170. resNetInternetCostPerGiB, _ := resChNetInternetCostPerGiB.Await()
  171. resNamespaceLabels, _ := resChNamespaceLabels.Await()
  172. resNamespaceAnnotations, _ := resChNamespaceAnnotations.Await()
  173. resPodLabels, _ := resChPodLabels.Await()
  174. resPodAnnotations, _ := resChPodAnnotations.Await()
  175. resServiceLabels, _ := resChServiceLabels.Await()
  176. resDeploymentLabels, _ := resChDeploymentLabels.Await()
  177. resStatefulSetLabels, _ := resChStatefulSetLabels.Await()
  178. resDaemonSetLabels, _ := resChDaemonSetLabels.Await()
  179. resJobLabels, _ := resChJobLabels.Await()
  180. log.Profile(startQuerying, "CostModel.ComputeAllocation: queries complete")
  181. if ctx.HasErrors() {
  182. for _, err := range ctx.Errors() {
  183. log.Errorf("CostModel.ComputeAllocation: %s", err)
  184. }
  185. return allocSet, ctx.ErrorCollection()
  186. }
  187. defer log.Profile(time.Now(), "CostModel.ComputeAllocation: processing complete")
  188. applyCPUCoresAllocated(podMap, resCPUCoresAllocated)
  189. applyCPUCoresRequested(podMap, resCPURequests)
  190. applyCPUCoresUsed(podMap, resCPUUsage)
  191. applyRAMBytesAllocated(podMap, resRAMBytesAllocated)
  192. applyRAMBytesRequested(podMap, resRAMRequests)
  193. applyRAMBytesUsed(podMap, resRAMUsage)
  194. applyGPUsRequested(podMap, resGPUsRequested)
  195. applyNetworkAllocation(podMap, resNetZoneGiB, resNetZoneCostPerGiB)
  196. applyNetworkAllocation(podMap, resNetRegionGiB, resNetRegionCostPerGiB)
  197. applyNetworkAllocation(podMap, resNetInternetGiB, resNetInternetCostPerGiB)
  198. // TODO niko/computeallocation pruneDuplicateData? (see costmodel.go)
  199. namespaceLabels := resToNamespaceLabels(resNamespaceLabels)
  200. podLabels := resToPodLabels(resPodLabels)
  201. namespaceAnnotations := resToNamespaceAnnotations(resNamespaceAnnotations)
  202. podAnnotations := resToPodAnnotations(resPodAnnotations)
  203. applyLabels(podMap, namespaceLabels, podLabels)
  204. applyAnnotations(podMap, namespaceAnnotations, podAnnotations)
  205. serviceLabels := getServiceLabels(resServiceLabels)
  206. applyServicesToPods(podMap, podLabels, serviceLabels)
  207. podDeploymentMap := labelsToPodControllerMap(podLabels, resToDeploymentLabels(resDeploymentLabels))
  208. podStatefulSetMap := labelsToPodControllerMap(podLabels, resToStatefulSetLabels(resStatefulSetLabels))
  209. podDaemonSetMap := resToPodDaemonSetMap(resDaemonSetLabels)
  210. podJobMap := resToPodJobMap(resJobLabels)
  211. applyControllersToPods(podMap, podDeploymentMap)
  212. applyControllersToPods(podMap, podStatefulSetMap)
  213. applyControllersToPods(podMap, podDaemonSetMap)
  214. applyControllersToPods(podMap, podJobMap)
  215. // TODO breakdown network costs?
  216. // Build out a map of Nodes with resource costs, discounts, and node types
  217. // for converting resource allocation data to cumulative costs.
  218. nodeMap := map[nodeKey]*Node{}
  219. applyNodeCostPerCPUHr(nodeMap, resNodeCostPerCPUHr)
  220. applyNodeCostPerRAMGiBHr(nodeMap, resNodeCostPerRAMGiBHr)
  221. applyNodeCostPerGPUHr(nodeMap, resNodeCostPerGPUHr)
  222. applyNodeSpot(nodeMap, resNodeIsSpot)
  223. applyNodeDiscount(nodeMap, cm)
  224. // Build out the map of all PVs with class, size and cost-per-hour.
  225. // Note: this does not record time running, which we may want to
  226. // include later for increased PV precision. (As long as the PV has
  227. // a PVC, we get time running there, so this is only inaccurate
  228. // for short-lived, unmounted PVs.)
  229. pvMap := map[pvKey]*PV{}
  230. buildPVMap(pvMap, resPVCostPerGiBHour)
  231. applyPVBytes(pvMap, resPVBytes)
  232. // Build out the map of all PVCs with time running, bytes requested,
  233. // and connect to the correct PV from pvMap. (If no PV exists, that
  234. // is noted, but does not result in any allocation/cost.)
  235. pvcMap := map[pvcKey]*PVC{}
  236. buildPVCMap(window, pvcMap, pvMap, resPVCInfo)
  237. applyPVCBytesRequested(pvcMap, resPVCBytesRequested)
  238. // Build out the relationships of pods to their PVCs. This step
  239. // populates the PVC.Count field so that PVC allocation can be
  240. // split appropriately among each pod's container allocation.
  241. podPVCMap := map[podKey][]*PVC{}
  242. buildPodPVCMap(podPVCMap, pvMap, pvcMap, podMap, resPodPVCAllocation)
  243. // Identify unmounted PVs (PVs without PVCs) and add one Allocation per
  244. // cluster representing each cluster's unmounted PVs (if necessary).
  245. applyUnmountedPVs(window, podMap, pvMap, pvcMap)
  246. // (3) Build out AllocationSet from Pod map
  247. for _, pod := range podMap {
  248. for _, alloc := range pod.Allocations {
  249. cluster, _ := alloc.Properties.GetCluster()
  250. node, _ := alloc.Properties.GetNode()
  251. namespace, _ := alloc.Properties.GetNamespace()
  252. pod, _ := alloc.Properties.GetPod()
  253. container, _ := alloc.Properties.GetContainer()
  254. podKey := newPodKey(cluster, namespace, pod)
  255. nodeKey := newNodeKey(cluster, node)
  256. if n, ok := nodeMap[nodeKey]; !ok {
  257. if pod != kubecost.UnmountedSuffix {
  258. log.Warningf("CostModel.ComputeAllocation: failed to find node %s for %s", nodeKey, alloc.Name)
  259. }
  260. } else {
  261. alloc.CPUCost = alloc.CPUCoreHours * n.CostPerCPUHr
  262. alloc.RAMCost = (alloc.RAMByteHours / 1024 / 1024 / 1024) * n.CostPerRAMGiBHr
  263. alloc.GPUCost = alloc.GPUHours * n.CostPerGPUHr
  264. }
  265. if pvcs, ok := podPVCMap[podKey]; ok {
  266. for _, pvc := range pvcs {
  267. // Determine the (start, end) of the relationship between the
  268. // given PVC and the associated Allocation so that a precise
  269. // number of hours can be used to compute cumulative cost.
  270. s, e := alloc.Start, alloc.End
  271. if pvc.Start.After(alloc.Start) {
  272. s = pvc.Start
  273. }
  274. if pvc.End.Before(alloc.End) {
  275. e = pvc.End
  276. }
  277. minutes := e.Sub(s).Minutes()
  278. hrs := minutes / 60.0
  279. count := float64(pvc.Count)
  280. if pvc.Count < 1 {
  281. count = 1
  282. }
  283. gib := pvc.Bytes / 1024 / 1024 / 1024
  284. cost := pvc.Volume.CostPerGiBHour * gib * hrs
  285. // Apply the size and cost of the PV to the allocation, each
  286. // weighted by count (i.e. the number of containers in the pod)
  287. alloc.PVByteHours += pvc.Bytes * hrs / count
  288. alloc.PVCost += cost / count
  289. }
  290. }
  291. alloc.TotalCost = 0.0
  292. alloc.TotalCost += alloc.CPUCost
  293. alloc.TotalCost += alloc.RAMCost
  294. alloc.TotalCost += alloc.GPUCost
  295. alloc.TotalCost += alloc.PVCost
  296. alloc.TotalCost += alloc.NetworkCost
  297. alloc.TotalCost += alloc.SharedCost
  298. alloc.TotalCost += alloc.ExternalCost
  299. // Make sure that the name is correct (node may not be present at this
  300. // point due to it missing from queryMinutes) then insert.
  301. alloc.Name = fmt.Sprintf("%s/%s/%s/%s/%s", cluster, node, namespace, pod, container)
  302. allocSet.Set(alloc)
  303. }
  304. }
  305. return allocSet, nil
  306. }
  307. func (cm *CostModel) buildPodMap(window kubecost.Window, resolution, maxBatchSize time.Duration, podMap map[podKey]*Pod, clusterStart, clusterEnd map[string]time.Time) error {
  308. // Assumes that window is positive and closed
  309. start, end := *window.Start(), *window.End()
  310. // Convert resolution duration to a query-ready string
  311. resStr := util.DurationString(resolution)
  312. ctx := prom.NewContext(cm.PrometheusClient)
  313. profile := time.Now()
  314. // Query for (start, end) by (pod, namespace, cluster) over the given
  315. // window, using the given resolution, and if necessary in batches no
  316. // larger than the given maximum batch size. If working in batches, track
  317. // overall progress by starting with (window.start, window.start) and
  318. // querying in batches no larger than maxBatchSize from start-to-end,
  319. // folding each result set into podMap as the results come back.
  320. coverage := kubecost.NewWindow(&start, &start)
  321. numQuery := 1
  322. for coverage.End().Before(end) {
  323. batchProfile := time.Now()
  324. // Determine the (start, end) of the current batch
  325. batchStart := *coverage.End()
  326. batchEnd := coverage.End().Add(maxBatchSize)
  327. if batchEnd.After(end) {
  328. batchEnd = end
  329. }
  330. batchWindow := kubecost.NewWindow(&batchStart, &batchEnd)
  331. var resPods []*prom.QueryResult
  332. var err error
  333. maxTries := 3
  334. numTries := 0
  335. for resPods == nil && numTries < maxTries {
  336. numTries++
  337. // Convert window (start, end) to (duration, offset) for querying Prometheus,
  338. // including handling Thanos offset
  339. durStr, offStr, err := batchWindow.DurationOffsetForPrometheus()
  340. if err != nil {
  341. // Negative duration, so set empty results and don't query
  342. // TODO niko/computeallocation test this!!!
  343. resPods = []*prom.QueryResult{}
  344. err = nil
  345. break
  346. }
  347. // Submit and profile query
  348. queryPods := fmt.Sprintf(queryFmtPods, durStr, resStr, offStr)
  349. queryProfile := time.Now()
  350. resPods, err = ctx.Query(queryPods).Await()
  351. if err != nil {
  352. // TODO niko/computeallocation do what with the error?
  353. log.Profile(queryProfile, fmt.Sprintf("CostModel.ComputeAllocation: pod query batch %d try %d failed: %s", numQuery, numTries, queryPods))
  354. resPods = nil
  355. }
  356. }
  357. if err != nil {
  358. return err
  359. }
  360. // ------------------------------------------------------------------------
  361. // TODO niko/compute-allocation remove logs
  362. log.Profile(batchProfile, fmt.Sprintf("CostModel.ComputeAllocation: pod query batch %d try %d complete: %s", numQuery, numTries, batchWindow))
  363. // ------------------------------------------------------------------------
  364. applyPodResults(window, resolution, podMap, clusterStart, clusterEnd, resPods)
  365. coverage = coverage.ExpandEnd(batchEnd)
  366. numQuery++
  367. }
  368. log.Profile(profile, "CostModel.ComputeAllocation: pod map built")
  369. return nil
  370. }
  371. func applyPodResults(window kubecost.Window, resolution time.Duration, podMap map[podKey]*Pod, clusterStart, clusterEnd map[string]time.Time, resPods []*prom.QueryResult) {
  372. for _, res := range resPods {
  373. if len(res.Values) == 0 {
  374. log.Warningf("CostModel.ComputeAllocation: empty minutes result")
  375. continue
  376. }
  377. cluster, err := res.GetString("cluster_id")
  378. if err != nil {
  379. cluster = env.GetClusterID()
  380. }
  381. labels, err := res.GetStrings("namespace", "pod")
  382. if err != nil {
  383. log.Warningf("CostModel.ComputeAllocation: minutes query result missing field: %s", err)
  384. continue
  385. }
  386. namespace := labels["namespace"]
  387. pod := labels["pod"]
  388. key := newPodKey(cluster, namespace, pod)
  389. // allocStart and allocEnd are the timestamps of the first and last
  390. // minutes the pod was running, respectively. We subtract one resolution
  391. // from allocStart because this point will actually represent the end
  392. // of the first minute. We don't subtract from allocEnd because it
  393. // already represents the end of the last minute.
  394. var allocStart, allocEnd time.Time
  395. startAdjustmentCoeff, endAdjustmentCoeff := 1.0, 1.0
  396. for _, datum := range res.Values {
  397. t := time.Unix(int64(datum.Timestamp), 0)
  398. if allocStart.IsZero() && datum.Value > 0 && window.Contains(t) {
  399. // Set the start timestamp to the earliest non-zero timestamp
  400. allocStart = t
  401. // Record adjustment coefficient, i.e. the portion of the start
  402. // timestamp to "ignore". That is, sometimes the value will be
  403. // 0.5, meaning that we should discount the time running by
  404. // half of the resolution the timestamp stands for.
  405. startAdjustmentCoeff = (1.0 - datum.Value)
  406. }
  407. if datum.Value > 0 && window.Contains(t) {
  408. // Set the end timestamp to the latest non-zero timestamp
  409. allocEnd = t
  410. // If the end timestamp differs from the start, then record the
  411. // adjustment coefficient, i.e. the portion of the end
  412. // timestamp to "ignore". That is, sometimes the value will be
  413. // 0.5, meaning that we should discount the time running by
  414. // half of the resolution the timestamp stands for.
  415. if !allocStart.Equal(t) {
  416. endAdjustmentCoeff = (1.0 - datum.Value)
  417. }
  418. }
  419. }
  420. if allocStart.IsZero() || allocEnd.IsZero() {
  421. continue
  422. }
  423. // Adjust timestamps accorind to the resolution and the adjustment
  424. // coefficients, as described above. That is, count the start timestamp
  425. // from the beginning of the resolution, not the end. Then "reduce" the
  426. // start and end by the correct amount, in the case that the "running"
  427. // value of the first or last timestamp was not a full 1.0.
  428. allocStart = allocStart.Add(-resolution)
  429. allocStart = allocStart.Add(time.Duration(startAdjustmentCoeff) * resolution)
  430. allocEnd = allocEnd.Add(-time.Duration(endAdjustmentCoeff) * resolution)
  431. // Set start if unset or this datum's start time is earlier than the
  432. // current earliest time.
  433. if _, ok := clusterStart[cluster]; !ok || allocStart.Before(clusterStart[cluster]) {
  434. clusterStart[cluster] = allocStart
  435. }
  436. // Set end if unset or this datum's end time is later than the
  437. // current latest time.
  438. if _, ok := clusterEnd[cluster]; !ok || allocEnd.After(clusterEnd[cluster]) {
  439. clusterEnd[cluster] = allocEnd
  440. }
  441. if pod, ok := podMap[key]; ok {
  442. // Pod has already been recorded, so update it accordingly
  443. if allocStart.Before(pod.Start) {
  444. pod.Start = allocStart
  445. }
  446. if allocEnd.After(pod.End) {
  447. pod.End = allocEnd
  448. }
  449. // ------------------------------------------------------------------------
  450. // TODO niko/compute-allocation remove logs
  451. log.Infof("CostModel.ComputeAllocation: update pod: %s (%s, %s)", key, pod.Start.Format("2006-01-02T15:04:05"), pod.End.Format("2006-01-02T15:04:05"))
  452. // ------------------------------------------------------------------------
  453. } else {
  454. // Pod has not been recorded yet, so insert it
  455. podMap[key] = &Pod{
  456. Window: window.Clone(),
  457. Start: allocStart,
  458. End: allocEnd,
  459. Key: key,
  460. Allocations: map[string]*kubecost.Allocation{},
  461. }
  462. // ------------------------------------------------------------------------
  463. // TODO niko/compute-allocation remove logs
  464. log.Infof("CostModel.ComputeAllocation: found pod: %s (%s, %s)", key, allocStart.Format("2006-01-02T15:04:05"), allocEnd.Format("2006-01-02T15:04:05"))
  465. // ------------------------------------------------------------------------
  466. }
  467. }
  468. }
  469. func applyCPUCoresAllocated(podMap map[podKey]*Pod, resCPUCoresAllocated []*prom.QueryResult) {
  470. for _, res := range resCPUCoresAllocated {
  471. key, err := resultPodKey(res, "cluster_id", "namespace", "pod")
  472. if err != nil {
  473. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU allocation result missing field: %s", err)
  474. continue
  475. }
  476. pod, ok := podMap[key]
  477. if !ok {
  478. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU allocation result for unidentified pod: %s", key)
  479. continue
  480. }
  481. container, err := res.GetString("container")
  482. if err != nil {
  483. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU allocation query result missing 'container': %s", key)
  484. continue
  485. }
  486. if _, ok := pod.Allocations[container]; !ok {
  487. pod.AppendContainer(container)
  488. }
  489. cpuCores := res.Values[0].Value
  490. hours := pod.Allocations[container].Minutes() / 60.0
  491. pod.Allocations[container].CPUCoreHours = cpuCores * hours
  492. node, err := res.GetString("node")
  493. if err != nil {
  494. log.Warningf("CostModel.ComputeAllocation: CPU allocation query result missing 'node': %s", key)
  495. continue
  496. }
  497. pod.Allocations[container].Properties.SetNode(node)
  498. }
  499. }
  500. func applyCPUCoresRequested(podMap map[podKey]*Pod, resCPUCoresRequested []*prom.QueryResult) {
  501. for _, res := range resCPUCoresRequested {
  502. key, err := resultPodKey(res, "cluster_id", "namespace", "pod")
  503. if err != nil {
  504. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU request result missing field: %s", err)
  505. continue
  506. }
  507. pod, ok := podMap[key]
  508. if !ok {
  509. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU request result for unidentified pod: %s", key)
  510. continue
  511. }
  512. container, err := res.GetString("container")
  513. if err != nil {
  514. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU request query result missing 'container': %s", key)
  515. continue
  516. }
  517. if _, ok := pod.Allocations[container]; !ok {
  518. pod.AppendContainer(container)
  519. }
  520. pod.Allocations[container].CPUCoreRequestAverage = res.Values[0].Value
  521. // If CPU allocation is less than requests, set CPUCoreHours to
  522. // request level.
  523. if pod.Allocations[container].CPUCores() < res.Values[0].Value {
  524. pod.Allocations[container].CPUCoreHours = res.Values[0].Value * (pod.Allocations[container].Minutes() / 60.0)
  525. }
  526. node, err := res.GetString("node")
  527. if err != nil {
  528. log.Warningf("CostModel.ComputeAllocation: CPU request query result missing 'node': %s", key)
  529. continue
  530. }
  531. pod.Allocations[container].Properties.SetNode(node)
  532. }
  533. }
  534. func applyCPUCoresUsed(podMap map[podKey]*Pod, resCPUCoresUsed []*prom.QueryResult) {
  535. for _, res := range resCPUCoresUsed {
  536. key, err := resultPodKey(res, "cluster_id", "namespace", "pod_name")
  537. if err != nil {
  538. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU usage result missing field: %s", err)
  539. continue
  540. }
  541. pod, ok := podMap[key]
  542. if !ok {
  543. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU usage result for unidentified pod: %s", key)
  544. continue
  545. }
  546. container, err := res.GetString("container_name")
  547. if err != nil {
  548. log.DedupedWarningf(10, "CostModel.ComputeAllocation: CPU usage query result missing 'container': %s", key)
  549. continue
  550. }
  551. if _, ok := pod.Allocations[container]; !ok {
  552. pod.AppendContainer(container)
  553. }
  554. pod.Allocations[container].CPUCoreUsageAverage = res.Values[0].Value
  555. }
  556. }
  557. func applyRAMBytesAllocated(podMap map[podKey]*Pod, resRAMBytesAllocated []*prom.QueryResult) {
  558. for _, res := range resRAMBytesAllocated {
  559. key, err := resultPodKey(res, "cluster_id", "namespace", "pod")
  560. if err != nil {
  561. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM allocation result missing field: %s", err)
  562. continue
  563. }
  564. pod, ok := podMap[key]
  565. if !ok {
  566. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM allocation result for unidentified pod: %s", key)
  567. continue
  568. }
  569. container, err := res.GetString("container")
  570. if err != nil {
  571. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM allocation query result missing 'container': %s", key)
  572. continue
  573. }
  574. if _, ok := pod.Allocations[container]; !ok {
  575. pod.AppendContainer(container)
  576. }
  577. ramBytes := res.Values[0].Value
  578. hours := pod.Allocations[container].Minutes() / 60.0
  579. pod.Allocations[container].RAMByteHours = ramBytes * hours
  580. node, err := res.GetString("node")
  581. if err != nil {
  582. log.Warningf("CostModel.ComputeAllocation: RAM allocation query result missing 'node': %s", key)
  583. continue
  584. }
  585. pod.Allocations[container].Properties.SetNode(node)
  586. }
  587. }
  588. func applyRAMBytesRequested(podMap map[podKey]*Pod, resRAMBytesRequested []*prom.QueryResult) {
  589. for _, res := range resRAMBytesRequested {
  590. key, err := resultPodKey(res, "cluster_id", "namespace", "pod")
  591. if err != nil {
  592. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM request result missing field: %s", err)
  593. continue
  594. }
  595. pod, ok := podMap[key]
  596. if !ok {
  597. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM request result for unidentified pod: %s", key)
  598. continue
  599. }
  600. container, err := res.GetString("container")
  601. if err != nil {
  602. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM request query result missing 'container': %s", key)
  603. continue
  604. }
  605. if _, ok := pod.Allocations[container]; !ok {
  606. pod.AppendContainer(container)
  607. }
  608. pod.Allocations[container].RAMBytesRequestAverage = res.Values[0].Value
  609. // If RAM allocation is less than requests, set RAMByteHours to
  610. // request level.
  611. if pod.Allocations[container].RAMBytes() < res.Values[0].Value {
  612. pod.Allocations[container].RAMByteHours = res.Values[0].Value * (pod.Allocations[container].Minutes() / 60.0)
  613. }
  614. node, err := res.GetString("node")
  615. if err != nil {
  616. log.Warningf("CostModel.ComputeAllocation: RAM request query result missing 'node': %s", key)
  617. continue
  618. }
  619. pod.Allocations[container].Properties.SetNode(node)
  620. }
  621. }
  622. func applyRAMBytesUsed(podMap map[podKey]*Pod, resRAMBytesUsed []*prom.QueryResult) {
  623. for _, res := range resRAMBytesUsed {
  624. key, err := resultPodKey(res, "cluster_id", "namespace", "pod_name")
  625. if err != nil {
  626. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM usage result missing field: %s", err)
  627. continue
  628. }
  629. pod, ok := podMap[key]
  630. if !ok {
  631. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM usage result for unidentified pod: %s", key)
  632. continue
  633. }
  634. container, err := res.GetString("container_name")
  635. if err != nil {
  636. log.DedupedWarningf(10, "CostModel.ComputeAllocation: RAM usage query result missing 'container': %s", key)
  637. continue
  638. }
  639. if _, ok := pod.Allocations[container]; !ok {
  640. pod.AppendContainer(container)
  641. }
  642. pod.Allocations[container].RAMBytesUsageAverage = res.Values[0].Value
  643. }
  644. }
  645. func applyGPUsRequested(podMap map[podKey]*Pod, resGPUsRequested []*prom.QueryResult) {
  646. for _, res := range resGPUsRequested {
  647. key, err := resultPodKey(res, "cluster_id", "namespace", "pod")
  648. if err != nil {
  649. log.DedupedWarningf(10, "CostModel.ComputeAllocation: GPU request result missing field: %s", err)
  650. continue
  651. }
  652. pod, ok := podMap[key]
  653. if !ok {
  654. log.DedupedWarningf(10, "CostModel.ComputeAllocation: GPU request result for unidentified pod: %s", key)
  655. continue
  656. }
  657. container, err := res.GetString("container")
  658. if err != nil {
  659. log.DedupedWarningf(10, "CostModel.ComputeAllocation: GPU request query result missing 'container': %s", key)
  660. continue
  661. }
  662. if _, ok := pod.Allocations[container]; !ok {
  663. pod.AppendContainer(container)
  664. }
  665. // TODO niko/computeallocation remove log
  666. log.Infof("CostModel.ComputeAllocation: GPU results: %s=%f", key, res.Values[0].Value)
  667. hrs := pod.Allocations[container].Minutes() / 60.0
  668. pod.Allocations[container].GPUHours = res.Values[0].Value * hrs
  669. }
  670. }
  671. func applyNetworkAllocation(podMap map[podKey]*Pod, resNetworkGiB []*prom.QueryResult, resNetworkCostPerGiB []*prom.QueryResult) {
  672. costPerGiBByCluster := map[string]float64{}
  673. for _, res := range resNetworkCostPerGiB {
  674. cluster, err := res.GetString("cluster_id")
  675. if err != nil {
  676. cluster = env.GetClusterID()
  677. }
  678. costPerGiBByCluster[cluster] = res.Values[0].Value
  679. }
  680. for _, res := range resNetworkGiB {
  681. podKey, err := resultPodKey(res, "cluster_id", "namespace", "pod_name")
  682. if err != nil {
  683. log.DedupedWarningf(10, "CostModel.ComputeAllocation: Network allocation query result missing field: %s", err)
  684. continue
  685. }
  686. pod, ok := podMap[podKey]
  687. if !ok {
  688. log.DedupedWarningf(10, "CostModel.ComputeAllocation: Network allocation query result for unidentified pod: %s", podKey)
  689. continue
  690. }
  691. for _, alloc := range pod.Allocations {
  692. gib := res.Values[0].Value / float64(len(pod.Allocations))
  693. costPerGiB := costPerGiBByCluster[podKey.Cluster]
  694. alloc.NetworkCost = gib * costPerGiB
  695. }
  696. }
  697. }
  698. func resToNamespaceLabels(resNamespaceLabels []*prom.QueryResult) map[string]map[string]string {
  699. namespaceLabels := map[string]map[string]string{}
  700. for _, res := range resNamespaceLabels {
  701. namespace, err := res.GetString("namespace")
  702. if err != nil {
  703. continue
  704. }
  705. if _, ok := namespaceLabels[namespace]; !ok {
  706. namespaceLabels[namespace] = map[string]string{}
  707. }
  708. for k, l := range res.GetLabels() {
  709. namespaceLabels[namespace][k] = l
  710. }
  711. }
  712. return namespaceLabels
  713. }
  714. func resToPodLabels(resPodLabels []*prom.QueryResult) map[podKey]map[string]string {
  715. podLabels := map[podKey]map[string]string{}
  716. for _, res := range resPodLabels {
  717. podKey, err := resultPodKey(res, "cluster_id", "namespace", "pod")
  718. if err != nil {
  719. continue
  720. }
  721. if _, ok := podLabels[podKey]; !ok {
  722. podLabels[podKey] = map[string]string{}
  723. }
  724. for k, l := range res.GetLabels() {
  725. podLabels[podKey][k] = l
  726. }
  727. }
  728. return podLabels
  729. }
  730. func resToNamespaceAnnotations(resNamespaceAnnotations []*prom.QueryResult) map[string]map[string]string {
  731. namespaceAnnotations := map[string]map[string]string{}
  732. for _, res := range resNamespaceAnnotations {
  733. namespace, err := res.GetString("namespace")
  734. if err != nil {
  735. continue
  736. }
  737. if _, ok := namespaceAnnotations[namespace]; !ok {
  738. namespaceAnnotations[namespace] = map[string]string{}
  739. }
  740. for k, l := range res.GetAnnotations() {
  741. namespaceAnnotations[namespace][k] = l
  742. }
  743. }
  744. return namespaceAnnotations
  745. }
  746. func resToPodAnnotations(resPodAnnotations []*prom.QueryResult) map[podKey]map[string]string {
  747. podAnnotations := map[podKey]map[string]string{}
  748. for _, res := range resPodAnnotations {
  749. podKey, err := resultPodKey(res, "cluster_id", "namespace", "pod")
  750. if err != nil {
  751. continue
  752. }
  753. if _, ok := podAnnotations[podKey]; !ok {
  754. podAnnotations[podKey] = map[string]string{}
  755. }
  756. for k, l := range res.GetAnnotations() {
  757. podAnnotations[podKey][k] = l
  758. }
  759. }
  760. return podAnnotations
  761. }
  762. func applyLabels(podMap map[podKey]*Pod, namespaceLabels map[string]map[string]string, podLabels map[podKey]map[string]string) {
  763. for key, pod := range podMap {
  764. for _, alloc := range pod.Allocations {
  765. allocLabels, err := alloc.Properties.GetLabels()
  766. if err != nil {
  767. allocLabels = map[string]string{}
  768. }
  769. // Apply namespace labels first, then pod labels so that pod labels
  770. // overwrite namespace labels.
  771. if labels, ok := namespaceLabels[key.Namespace]; ok {
  772. for k, v := range labels {
  773. allocLabels[k] = v
  774. }
  775. }
  776. if labels, ok := podLabels[key]; ok {
  777. for k, v := range labels {
  778. allocLabels[k] = v
  779. }
  780. }
  781. alloc.Properties.SetLabels(allocLabels)
  782. }
  783. }
  784. }
  785. func applyAnnotations(podMap map[podKey]*Pod, namespaceAnnotations map[string]map[string]string, podAnnotations map[podKey]map[string]string) {
  786. for key, pod := range podMap {
  787. for _, alloc := range pod.Allocations {
  788. allocAnnotations, err := alloc.Properties.GetAnnotations()
  789. if err != nil {
  790. allocAnnotations = map[string]string{}
  791. }
  792. // Apply namespace annotations first, then pod annotations so that
  793. // pod labels overwrite namespace labels.
  794. if labels, ok := namespaceAnnotations[key.Namespace]; ok {
  795. for k, v := range labels {
  796. allocAnnotations[k] = v
  797. }
  798. }
  799. if labels, ok := podAnnotations[key]; ok {
  800. for k, v := range labels {
  801. allocAnnotations[k] = v
  802. }
  803. }
  804. alloc.Properties.SetAnnotations(allocAnnotations)
  805. }
  806. }
  807. }
  808. func getServiceLabels(resServiceLabels []*prom.QueryResult) map[serviceKey]map[string]string {
  809. serviceLabels := map[serviceKey]map[string]string{}
  810. for _, res := range resServiceLabels {
  811. serviceKey, err := resultServiceKey(res, "cluster_id", "namespace", "service")
  812. if err != nil {
  813. continue
  814. }
  815. if _, ok := serviceLabels[serviceKey]; !ok {
  816. serviceLabels[serviceKey] = map[string]string{}
  817. }
  818. for k, l := range res.GetLabels() {
  819. serviceLabels[serviceKey][k] = l
  820. }
  821. }
  822. return serviceLabels
  823. }
  824. func resToDeploymentLabels(resDeploymentLabels []*prom.QueryResult) map[controllerKey]map[string]string {
  825. deploymentLabels := map[controllerKey]map[string]string{}
  826. for _, res := range resDeploymentLabels {
  827. controllerKey, err := resultDeploymentKey(res, "cluster_id", "namespace", "deployment")
  828. if err != nil {
  829. continue
  830. }
  831. if _, ok := deploymentLabels[controllerKey]; !ok {
  832. deploymentLabels[controllerKey] = map[string]string{}
  833. }
  834. for k, l := range res.GetLabels() {
  835. deploymentLabels[controllerKey][k] = l
  836. }
  837. }
  838. return deploymentLabels
  839. }
  840. func resToStatefulSetLabels(resStatefulSetLabels []*prom.QueryResult) map[controllerKey]map[string]string {
  841. statefulSetLabels := map[controllerKey]map[string]string{}
  842. for _, res := range resStatefulSetLabels {
  843. controllerKey, err := resultStatefulSetKey(res, "cluster_id", "namespace", "statefulSet")
  844. if err != nil {
  845. continue
  846. }
  847. if _, ok := statefulSetLabels[controllerKey]; !ok {
  848. statefulSetLabels[controllerKey] = map[string]string{}
  849. }
  850. for k, l := range res.GetLabels() {
  851. statefulSetLabels[controllerKey][k] = l
  852. }
  853. }
  854. return statefulSetLabels
  855. }
  856. func labelsToPodControllerMap(podLabels map[podKey]map[string]string, controllerLabels map[controllerKey]map[string]string) map[podKey]controllerKey {
  857. podControllerMap := map[podKey]controllerKey{}
  858. // For each controller, turn the labels into a selector and attempt to
  859. // match it with each set of pod labels. A match indicates that the pod
  860. // belongs to the controller.
  861. for cKey, cLabels := range controllerLabels {
  862. selector := labels.Set(cLabels).AsSelectorPreValidated()
  863. for pKey, pLabels := range podLabels {
  864. // If the pod is in a different cluster or namespace, there is
  865. // no need to compare the labels.
  866. if cKey.Cluster != pKey.Cluster || cKey.Namespace != pKey.Namespace {
  867. continue
  868. }
  869. podLabelSet := labels.Set(pLabels)
  870. if selector.Matches(podLabelSet) {
  871. if _, ok := podControllerMap[pKey]; ok {
  872. log.Warningf("CostModel.ComputeAllocation: PodControllerMap match already exists: %s matches %s and %s", pKey, podControllerMap[pKey], cKey)
  873. }
  874. podControllerMap[pKey] = cKey
  875. }
  876. }
  877. }
  878. return podControllerMap
  879. }
  880. func resToPodDaemonSetMap(resDaemonSetLabels []*prom.QueryResult) map[podKey]controllerKey {
  881. daemonSetLabels := map[podKey]controllerKey{}
  882. for _, res := range resDaemonSetLabels {
  883. controllerKey, err := resultDaemonSetKey(res, "cluster_id", "namespace", "owner_name")
  884. if err != nil {
  885. continue
  886. }
  887. pod, err := res.GetString("pod")
  888. if err != nil {
  889. log.Warningf("CostModel.ComputeAllocation: DaemonSetLabel result without pod: %s", controllerKey)
  890. }
  891. podKey := newPodKey(controllerKey.Cluster, controllerKey.Namespace, pod)
  892. daemonSetLabels[podKey] = controllerKey
  893. }
  894. return daemonSetLabels
  895. }
  896. func resToPodJobMap(resJobLabels []*prom.QueryResult) map[podKey]controllerKey {
  897. jobLabels := map[podKey]controllerKey{}
  898. for _, res := range resJobLabels {
  899. controllerKey, err := resultJobKey(res, "cluster_id", "namespace", "owner_name")
  900. if err != nil {
  901. continue
  902. }
  903. pod, err := res.GetString("pod")
  904. if err != nil {
  905. log.Warningf("CostModel.ComputeAllocation: JobLabel result without pod: %s", controllerKey)
  906. }
  907. podKey := newPodKey(controllerKey.Cluster, controllerKey.Namespace, pod)
  908. jobLabels[podKey] = controllerKey
  909. }
  910. return jobLabels
  911. }
  912. func applyServicesToPods(podMap map[podKey]*Pod, podLabels map[podKey]map[string]string, serviceLabels map[serviceKey]map[string]string) {
  913. podServicesMap := map[podKey][]serviceKey{}
  914. // For each service, turn the labels into a selector and attempt to
  915. // match it with each set of pod labels. A match indicates that the pod
  916. // belongs to the service.
  917. for sKey, sLabels := range serviceLabels {
  918. selector := labels.Set(sLabels).AsSelectorPreValidated()
  919. for pKey, pLabels := range podLabels {
  920. // If the pod is in a different cluster or namespace, there is
  921. // no need to compare the labels.
  922. if sKey.Cluster != pKey.Cluster || sKey.Namespace != pKey.Namespace {
  923. continue
  924. }
  925. podLabelSet := labels.Set(pLabels)
  926. if selector.Matches(podLabelSet) {
  927. if _, ok := podServicesMap[pKey]; !ok {
  928. podServicesMap[pKey] = []serviceKey{}
  929. }
  930. podServicesMap[pKey] = append(podServicesMap[pKey], sKey)
  931. }
  932. }
  933. }
  934. // For each allocation in each pod, attempt to find and apply the list of
  935. // services associated with the allocation's pod.
  936. for key, pod := range podMap {
  937. for _, alloc := range pod.Allocations {
  938. if sKeys, ok := podServicesMap[key]; ok {
  939. services := []string{}
  940. for _, sKey := range sKeys {
  941. services = append(services, sKey.Service)
  942. }
  943. alloc.Properties.SetServices(services)
  944. }
  945. }
  946. }
  947. }
  948. func applyControllersToPods(podMap map[podKey]*Pod, podControllerMap map[podKey]controllerKey) {
  949. for key, pod := range podMap {
  950. for _, alloc := range pod.Allocations {
  951. if controllerKey, ok := podControllerMap[key]; ok {
  952. alloc.Properties.SetControllerKind(controllerKey.ControllerKind)
  953. alloc.Properties.SetController(controllerKey.Controller)
  954. }
  955. }
  956. }
  957. }
  958. func applyNodeCostPerCPUHr(nodeMap map[nodeKey]*Node, resNodeCostPerCPUHr []*prom.QueryResult) {
  959. for _, res := range resNodeCostPerCPUHr {
  960. cluster, err := res.GetString("cluster_id")
  961. if err != nil {
  962. cluster = env.GetClusterID()
  963. }
  964. node, err := res.GetString("node")
  965. if err != nil {
  966. log.Warningf("CostModel.ComputeAllocation: Node CPU cost query result missing field: %s", err)
  967. continue
  968. }
  969. instanceType, err := res.GetString("instance_type")
  970. if err != nil {
  971. log.Warningf("CostModel.ComputeAllocation: Node CPU cost query result missing field: %s", err)
  972. continue
  973. }
  974. key := newNodeKey(cluster, node)
  975. if _, ok := nodeMap[key]; !ok {
  976. nodeMap[key] = &Node{
  977. Name: node,
  978. NodeType: instanceType,
  979. }
  980. }
  981. nodeMap[key].CostPerCPUHr = res.Values[0].Value
  982. }
  983. }
  984. func applyNodeCostPerRAMGiBHr(nodeMap map[nodeKey]*Node, resNodeCostPerRAMGiBHr []*prom.QueryResult) {
  985. for _, res := range resNodeCostPerRAMGiBHr {
  986. cluster, err := res.GetString("cluster_id")
  987. if err != nil {
  988. cluster = env.GetClusterID()
  989. }
  990. node, err := res.GetString("node")
  991. if err != nil {
  992. log.Warningf("CostModel.ComputeAllocation: Node RAM cost query result missing field: %s", err)
  993. continue
  994. }
  995. instanceType, err := res.GetString("instance_type")
  996. if err != nil {
  997. log.Warningf("CostModel.ComputeAllocation: Node RAM cost query result missing field: %s", err)
  998. continue
  999. }
  1000. key := newNodeKey(cluster, node)
  1001. if _, ok := nodeMap[key]; !ok {
  1002. nodeMap[key] = &Node{
  1003. Name: node,
  1004. NodeType: instanceType,
  1005. }
  1006. }
  1007. nodeMap[key].CostPerRAMGiBHr = res.Values[0].Value
  1008. }
  1009. }
  1010. func applyNodeCostPerGPUHr(nodeMap map[nodeKey]*Node, resNodeCostPerGPUHr []*prom.QueryResult) {
  1011. for _, res := range resNodeCostPerGPUHr {
  1012. cluster, err := res.GetString("cluster_id")
  1013. if err != nil {
  1014. cluster = env.GetClusterID()
  1015. }
  1016. node, err := res.GetString("node")
  1017. if err != nil {
  1018. log.Warningf("CostModel.ComputeAllocation: Node GPU cost query result missing field: %s", err)
  1019. continue
  1020. }
  1021. instanceType, err := res.GetString("instance_type")
  1022. if err != nil {
  1023. log.Warningf("CostModel.ComputeAllocation: Node GPU cost query result missing field: %s", err)
  1024. continue
  1025. }
  1026. key := newNodeKey(cluster, node)
  1027. if _, ok := nodeMap[key]; !ok {
  1028. nodeMap[key] = &Node{
  1029. Name: node,
  1030. NodeType: instanceType,
  1031. }
  1032. }
  1033. nodeMap[key].CostPerGPUHr = res.Values[0].Value
  1034. }
  1035. }
  1036. func applyNodeSpot(nodeMap map[nodeKey]*Node, resNodeIsSpot []*prom.QueryResult) {
  1037. for _, res := range resNodeIsSpot {
  1038. cluster, err := res.GetString("cluster_id")
  1039. if err != nil {
  1040. cluster = env.GetClusterID()
  1041. }
  1042. node, err := res.GetString("node")
  1043. if err != nil {
  1044. log.Warningf("CostModel.ComputeAllocation: Node spot query result missing field: %s", err)
  1045. continue
  1046. }
  1047. key := newNodeKey(cluster, node)
  1048. if _, ok := nodeMap[key]; !ok {
  1049. log.Warningf("CostModel.ComputeAllocation: Node spot query result for missing node: %s", key)
  1050. continue
  1051. }
  1052. nodeMap[key].Preemptible = res.Values[0].Value > 0
  1053. }
  1054. }
  1055. func applyNodeDiscount(nodeMap map[nodeKey]*Node, cm *CostModel) {
  1056. if cm == nil {
  1057. return
  1058. }
  1059. c, err := cm.Provider.GetConfig()
  1060. if err != nil {
  1061. log.Errorf("CostModel.ComputeAllocation: applyNodeDiscount: %s", err)
  1062. return
  1063. }
  1064. discount, err := ParsePercentString(c.Discount)
  1065. if err != nil {
  1066. log.Errorf("CostModel.ComputeAllocation: applyNodeDiscount: %s", err)
  1067. return
  1068. }
  1069. negotiatedDiscount, err := ParsePercentString(c.NegotiatedDiscount)
  1070. if err != nil {
  1071. log.Errorf("CostModel.ComputeAllocation: applyNodeDiscount: %s", err)
  1072. return
  1073. }
  1074. for _, node := range nodeMap {
  1075. // TODO niko/computeallocation GKE Reserved Instances into account
  1076. node.Discount = cm.Provider.CombinedDiscountForNode(node.NodeType, node.Preemptible, discount, negotiatedDiscount)
  1077. node.CostPerCPUHr *= (1.0 - node.Discount)
  1078. node.CostPerRAMGiBHr *= (1.0 - node.Discount)
  1079. }
  1080. }
  1081. func buildPVMap(pvMap map[pvKey]*PV, resPVCostPerGiBHour []*prom.QueryResult) {
  1082. for _, res := range resPVCostPerGiBHour {
  1083. cluster, err := res.GetString("cluster_id")
  1084. if err != nil {
  1085. cluster = env.GetClusterID()
  1086. }
  1087. name, err := res.GetString("volumename")
  1088. if err != nil {
  1089. log.Warningf("CostModel.ComputeAllocation: PV cost without volumename")
  1090. continue
  1091. }
  1092. key := newPVKey(cluster, name)
  1093. pvMap[key] = &PV{
  1094. Cluster: cluster,
  1095. Name: name,
  1096. CostPerGiBHour: res.Values[0].Value,
  1097. }
  1098. }
  1099. }
  1100. func applyPVBytes(pvMap map[pvKey]*PV, resPVBytes []*prom.QueryResult) {
  1101. for _, res := range resPVBytes {
  1102. key, err := resultPVKey(res, "cluster_id", "persistentvolume")
  1103. if err != nil {
  1104. log.Warningf("CostModel.ComputeAllocation: PV bytes query result missing field: %s", err)
  1105. continue
  1106. }
  1107. if _, ok := pvMap[key]; !ok {
  1108. log.Warningf("CostModel.ComputeAllocation: PV bytes result for missing PV: %s", err)
  1109. continue
  1110. }
  1111. pvMap[key].Bytes = res.Values[0].Value
  1112. }
  1113. }
  1114. func buildPVCMap(window kubecost.Window, pvcMap map[pvcKey]*PVC, pvMap map[pvKey]*PV, resPVCInfo []*prom.QueryResult) {
  1115. for _, res := range resPVCInfo {
  1116. cluster, err := res.GetString("cluster_id")
  1117. if err != nil {
  1118. cluster = env.GetClusterID()
  1119. }
  1120. values, err := res.GetStrings("persistentvolumeclaim", "storageclass", "volumename", "namespace")
  1121. if err != nil {
  1122. log.Warningf("CostModel.ComputeAllocation: PVC info query result missing field: %s", err)
  1123. continue
  1124. }
  1125. namespace := values["namespace"]
  1126. name := values["persistentvolumeclaim"]
  1127. volume := values["volumename"]
  1128. storageClass := values["storageclass"]
  1129. pvKey := newPVKey(cluster, volume)
  1130. pvcKey := newPVCKey(cluster, namespace, name)
  1131. // pvcStart and pvcEnd are the timestamps of the first and last minutes
  1132. // the PVC was running, respectively. We subtract 1m from pvcStart
  1133. // because this point will actually represent the end of the first
  1134. // minute. We don't subtract from pvcEnd because it already represents
  1135. // the end of the last minute.
  1136. var pvcStart, pvcEnd time.Time
  1137. for _, datum := range res.Values {
  1138. t := time.Unix(int64(datum.Timestamp), 0)
  1139. if pvcStart.IsZero() && datum.Value > 0 && window.Contains(t) {
  1140. pvcStart = t
  1141. }
  1142. if datum.Value > 0 && window.Contains(t) {
  1143. pvcEnd = t
  1144. }
  1145. }
  1146. if pvcStart.IsZero() || pvcEnd.IsZero() {
  1147. log.Warningf("CostModel.ComputeAllocation: PVC %s has no running time", pvcKey)
  1148. }
  1149. pvcStart = pvcStart.Add(-time.Minute)
  1150. if _, ok := pvMap[pvKey]; !ok {
  1151. log.Warningf("CostModel.ComputeAllocation: PV missing for PVC info query result: %s", pvKey)
  1152. continue
  1153. }
  1154. pvMap[pvKey].StorageClass = storageClass
  1155. if _, ok := pvcMap[pvcKey]; !ok {
  1156. pvcMap[pvcKey] = &PVC{}
  1157. }
  1158. pvcMap[pvcKey].Name = name
  1159. pvcMap[pvcKey].Namespace = namespace
  1160. pvcMap[pvcKey].Volume = pvMap[pvKey]
  1161. pvcMap[pvcKey].Start = pvcStart
  1162. pvcMap[pvcKey].End = pvcEnd
  1163. }
  1164. }
  1165. func applyPVCBytesRequested(pvcMap map[pvcKey]*PVC, resPVCBytesRequested []*prom.QueryResult) {
  1166. for _, res := range resPVCBytesRequested {
  1167. key, err := resultPVCKey(res, "cluster_id", "namespace", "persistentvolumeclaim")
  1168. if err != nil {
  1169. log.Warningf("CostModel.ComputeAllocation: PVC bytes requested query result missing field: %s", err)
  1170. continue
  1171. }
  1172. if _, ok := pvcMap[key]; !ok {
  1173. log.Warningf("CostModel.ComputeAllocation: PVC bytes requested result for missing PVC: %s", key)
  1174. continue
  1175. }
  1176. pvcMap[key].Bytes = res.Values[0].Value
  1177. }
  1178. }
  1179. func buildPodPVCMap(podPVCMap map[podKey][]*PVC, pvMap map[pvKey]*PV, pvcMap map[pvcKey]*PVC, podMap map[podKey]*Pod, resPodPVCAllocation []*prom.QueryResult) {
  1180. for _, res := range resPodPVCAllocation {
  1181. cluster, err := res.GetString("cluster_id")
  1182. if err != nil {
  1183. cluster = env.GetClusterID()
  1184. }
  1185. values, err := res.GetStrings("persistentvolume", "persistentvolumeclaim", "pod", "namespace")
  1186. if err != nil {
  1187. log.Warningf("CostModel.ComputeAllocation: PVC allocation query result missing field: %s", err)
  1188. continue
  1189. }
  1190. namespace := values["namespace"]
  1191. pod := values["pod"]
  1192. name := values["persistentvolumeclaim"]
  1193. volume := values["persistentvolume"]
  1194. podKey := newPodKey(cluster, namespace, pod)
  1195. pvKey := newPVKey(cluster, volume)
  1196. pvcKey := newPVCKey(cluster, namespace, name)
  1197. if _, ok := pvMap[pvKey]; !ok {
  1198. log.Warningf("CostModel.ComputeAllocation: PV missing for PVC allocation query result: %s", pvKey)
  1199. continue
  1200. }
  1201. if _, ok := podPVCMap[podKey]; !ok {
  1202. podPVCMap[podKey] = []*PVC{}
  1203. }
  1204. pvc, ok := pvcMap[pvcKey]
  1205. if !ok {
  1206. log.Warningf("CostModel.ComputeAllocation: PVC missing for PVC allocation query: %s", pvcKey)
  1207. continue
  1208. }
  1209. count := 1
  1210. if pod, ok := podMap[podKey]; ok && len(pod.Allocations) > 0 {
  1211. count = len(pod.Allocations)
  1212. } else {
  1213. log.DedupedWarningf(10, "CostModel.ComputeAllocation: PVC %s for missing pod %s", pvcKey, podKey)
  1214. }
  1215. pvc.Count = count
  1216. pvc.Mounted = true
  1217. podPVCMap[podKey] = append(podPVCMap[podKey], pvc)
  1218. }
  1219. }
  1220. func applyUnmountedPVs(window kubecost.Window, podMap map[podKey]*Pod, pvMap map[pvKey]*PV, pvcMap map[pvcKey]*PVC) {
  1221. unmountedPVBytes := map[string]float64{}
  1222. unmountedPVCost := map[string]float64{}
  1223. for _, pv := range pvMap {
  1224. mounted := false
  1225. for _, pvc := range pvcMap {
  1226. if pvc.Volume == nil {
  1227. continue
  1228. }
  1229. if pvc.Volume == pv {
  1230. mounted = true
  1231. break
  1232. }
  1233. }
  1234. if !mounted {
  1235. gib := pv.Bytes / 1024 / 1024 / 1024
  1236. hrs := window.Minutes() / 60.0 // TODO niko/computeallocation PV hours, not window hours?
  1237. cost := pv.CostPerGiBHour * gib * hrs
  1238. unmountedPVCost[pv.Cluster] += cost
  1239. unmountedPVBytes[pv.Cluster] += pv.Bytes
  1240. }
  1241. }
  1242. for cluster, amount := range unmountedPVCost {
  1243. container := kubecost.UnmountedSuffix
  1244. pod := kubecost.UnmountedSuffix
  1245. namespace := kubecost.UnmountedSuffix
  1246. node := ""
  1247. key := newPodKey(cluster, namespace, pod)
  1248. podMap[key] = &Pod{
  1249. Window: window.Clone(),
  1250. Start: *window.Start(),
  1251. End: *window.End(),
  1252. Key: key,
  1253. Allocations: map[string]*kubecost.Allocation{},
  1254. }
  1255. podMap[key].AppendContainer(container)
  1256. podMap[key].Allocations[container].Properties.SetCluster(cluster)
  1257. podMap[key].Allocations[container].Properties.SetNode(node)
  1258. podMap[key].Allocations[container].Properties.SetNamespace(namespace)
  1259. podMap[key].Allocations[container].Properties.SetPod(pod)
  1260. podMap[key].Allocations[container].Properties.SetContainer(container)
  1261. podMap[key].Allocations[container].PVByteHours = unmountedPVBytes[cluster] * window.Minutes() / 60.0
  1262. podMap[key].Allocations[container].PVCost = amount
  1263. podMap[key].Allocations[container].TotalCost = amount
  1264. }
  1265. }
  1266. func applyUnmountedPVCs(window kubecost.Window, podMap map[podKey]*Pod, pvcMap map[pvcKey]*PVC) {
  1267. unmountedPVCBytes := map[namespaceKey]float64{}
  1268. unmountedPVCCost := map[namespaceKey]float64{}
  1269. for _, pvc := range pvcMap {
  1270. if !pvc.Mounted && pvc.Volume != nil {
  1271. key := newNamespaceKey(pvc.Cluster, pvc.Namespace)
  1272. gib := pvc.Volume.Bytes / 1024 / 1024 / 1024
  1273. hrs := pvc.Minutes() / 60.0
  1274. cost := pvc.Volume.CostPerGiBHour * gib * hrs
  1275. unmountedPVCCost[key] += cost
  1276. unmountedPVCBytes[key] += pvc.Volume.Bytes
  1277. }
  1278. }
  1279. for key, amount := range unmountedPVCCost {
  1280. container := kubecost.UnmountedSuffix
  1281. pod := kubecost.UnmountedSuffix
  1282. namespace := key.Namespace
  1283. node := ""
  1284. cluster := key.Cluster
  1285. podKey := newPodKey(cluster, namespace, pod)
  1286. podMap[podKey] = &Pod{
  1287. Window: window.Clone(),
  1288. Start: *window.Start(),
  1289. End: *window.End(),
  1290. Key: podKey,
  1291. Allocations: map[string]*kubecost.Allocation{},
  1292. }
  1293. podMap[podKey].AppendContainer(container)
  1294. podMap[podKey].Allocations[container].Properties.SetCluster(cluster)
  1295. podMap[podKey].Allocations[container].Properties.SetNode(node)
  1296. podMap[podKey].Allocations[container].Properties.SetNamespace(namespace)
  1297. podMap[podKey].Allocations[container].Properties.SetPod(pod)
  1298. podMap[podKey].Allocations[container].Properties.SetContainer(container)
  1299. podMap[podKey].Allocations[container].PVByteHours = unmountedPVCBytes[key] * window.Minutes() / 60.0
  1300. podMap[podKey].Allocations[container].PVCost = amount
  1301. podMap[podKey].Allocations[container].TotalCost = amount
  1302. }
  1303. }
  1304. // TODO niko/computealloction comment
  1305. type Pod struct {
  1306. Window kubecost.Window
  1307. Start time.Time
  1308. End time.Time
  1309. Key podKey
  1310. Allocations map[string]*kubecost.Allocation
  1311. }
  1312. // TODO niko/computealloction comment
  1313. func (p Pod) AppendContainer(container string) {
  1314. name := fmt.Sprintf("%s/%s/%s/%s", p.Key.Cluster, p.Key.Namespace, p.Key.Pod, container)
  1315. alloc := &kubecost.Allocation{
  1316. Name: name,
  1317. Properties: kubecost.Properties{},
  1318. Window: p.Window.Clone(),
  1319. Start: p.Start,
  1320. End: p.End,
  1321. }
  1322. alloc.Properties.SetContainer(container)
  1323. alloc.Properties.SetPod(p.Key.Pod)
  1324. alloc.Properties.SetNamespace(p.Key.Namespace)
  1325. alloc.Properties.SetCluster(p.Key.Cluster)
  1326. p.Allocations[container] = alloc
  1327. }
  1328. // PVC describes a PersistentVolumeClaim
  1329. // TODO move to pkg/kubecost? [TODO:CLEANUP]
  1330. // TODO add PersistentVolumeClaims field to type Allocation? [TODO:CLEANUP]
  1331. type PVC struct {
  1332. Bytes float64 `json:"bytes"`
  1333. Count int `json:"count"`
  1334. Name string `json:"name"`
  1335. Cluster string `json:"cluster"`
  1336. Namespace string `json:"namespace"`
  1337. Volume *PV `json:"persistentVolume"`
  1338. Mounted bool `json:"mounted"`
  1339. Start time.Time `json:"start"`
  1340. End time.Time `json:"end"`
  1341. }
  1342. // Cost computes the cumulative cost of the PVC
  1343. func (pvc *PVC) Cost() float64 {
  1344. if pvc == nil || pvc.Volume == nil {
  1345. return 0.0
  1346. }
  1347. gib := pvc.Bytes / 1024 / 1024 / 1024
  1348. hrs := pvc.Minutes() / 60.0
  1349. return pvc.Volume.CostPerGiBHour * gib * hrs
  1350. }
  1351. // Minutes computes the number of minutes over which the PVC is defined
  1352. func (pvc *PVC) Minutes() float64 {
  1353. if pvc == nil {
  1354. return 0.0
  1355. }
  1356. return pvc.End.Sub(pvc.Start).Minutes()
  1357. }
  1358. // String returns a string representation of the PVC
  1359. func (pvc *PVC) String() string {
  1360. if pvc == nil {
  1361. return "<nil>"
  1362. }
  1363. 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))
  1364. }
  1365. // PV describes a PersistentVolume
  1366. // TODO move to pkg/kubecost? [TODO:CLEANUP]
  1367. type PV struct {
  1368. Bytes float64 `json:"bytes"`
  1369. CostPerGiBHour float64 `json:"costPerGiBHour"` // TODO niko/computeallocation GiB or GB?
  1370. Cluster string `json:"cluster"`
  1371. Name string `json:"name"`
  1372. StorageClass string `json:"storageClass"`
  1373. }
  1374. // String returns a string representation of the PV
  1375. func (pv *PV) String() string {
  1376. if pv == nil {
  1377. return "<nil>"
  1378. }
  1379. return fmt.Sprintf("%s/%s{Bytes:%.2f, Cost/GiB*Hr:%.6f, StorageClass:%s}", pv.Cluster, pv.Name, pv.Bytes, pv.CostPerGiBHour, pv.StorageClass)
  1380. }