cluster.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. package costmodel
  2. import (
  3. "fmt"
  4. "os"
  5. "sync"
  6. "time"
  7. "github.com/kubecost/cost-model/cloud"
  8. "github.com/kubecost/cost-model/util"
  9. prometheus "github.com/prometheus/client_golang/api"
  10. "k8s.io/klog"
  11. )
  12. const (
  13. queryClusterCores = `sum(
  14. avg(avg_over_time(kube_node_status_capacity_cpu_cores[%s] %s)) by (node, cluster_id) * avg(avg_over_time(node_cpu_hourly_cost[%s] %s)) by (node, cluster_id) * 730 +
  15. avg(avg_over_time(node_gpu_hourly_cost[%s] %s)) by (node, cluster_id) * 730
  16. ) by (cluster_id)`
  17. queryClusterRAM = `sum(
  18. avg(avg_over_time(kube_node_status_capacity_memory_bytes[%s] %s)) by (node, cluster_id) / 1024 / 1024 / 1024 * avg(avg_over_time(node_ram_hourly_cost[%s] %s)) by (node, cluster_id) * 730
  19. ) by (cluster_id)`
  20. queryStorage = `sum(
  21. avg(avg_over_time(pv_hourly_cost[%s] %s)) by (persistentvolume, cluster_id) * 730
  22. * avg(avg_over_time(kube_persistentvolume_capacity_bytes[%s] %s)) by (persistentvolume, cluster_id) / 1024 / 1024 / 1024
  23. ) by (cluster_id) %s`
  24. queryTotal = `sum(avg(node_total_hourly_cost) by (node, cluster_id)) * 730 +
  25. sum(
  26. avg(avg_over_time(pv_hourly_cost[1h])) by (persistentvolume, cluster_id) * 730
  27. * avg(avg_over_time(kube_persistentvolume_capacity_bytes[1h])) by (persistentvolume, cluster_id) / 1024 / 1024 / 1024
  28. ) by (cluster_id) %s`
  29. )
  30. // TODO move this to a package-accessible helper
  31. type PromQueryContext struct {
  32. client prometheus.Client
  33. ec *util.ErrorCollector
  34. wg *sync.WaitGroup
  35. }
  36. // TODO move this to a package-accessible helper function once dependencies are able to
  37. // be extricated from costmodel package (PromQueryResult -> Vector). Otherwise, circular deps.
  38. func AsyncPromQuery(query string, resultCh chan []*PromQueryResult, ctx PromQueryContext) {
  39. if ctx.wg != nil {
  40. defer ctx.wg.Done()
  41. }
  42. raw, promErr := Query(ctx.client, query)
  43. ctx.ec.Report(promErr)
  44. results, parseErr := NewQueryResults(raw)
  45. ctx.ec.Report(parseErr)
  46. resultCh <- results
  47. }
  48. // Costs represents cumulative and monthly cluster costs over a given duration. Costs
  49. // are broken down by cores, memory, and storage.
  50. type ClusterCosts struct {
  51. Start *time.Time `json:"startTime"`
  52. End *time.Time `json:"endTime"`
  53. CPUCumulative float64 `json:"cpuCumulativeCost"`
  54. CPUMonthly float64 `json:"cpuMonthlyCost"`
  55. GPUCumulative float64 `json:"gpuCumulativeCost"`
  56. GPUMonthly float64 `json:"gpuMonthlyCost"`
  57. RAMCumulative float64 `json:"ramCumulativeCost"`
  58. RAMMonthly float64 `json:"ramMonthlyCost"`
  59. StorageCumulative float64 `json:"storageCumulativeCost"`
  60. StorageMonthly float64 `json:"storageMonthlyCost"`
  61. TotalCumulative float64 `json:"totalCumulativeCost"`
  62. TotalMonthly float64 `json:"totalMonthlyCost"`
  63. }
  64. // NewClusterCostsFromCumulative takes cumulative cost data over a given time range, computes
  65. // the associated monthly rate data, and returns the Costs.
  66. func NewClusterCostsFromCumulative(cpu, gpu, ram, storage float64, window, offset string, dataHours float64) (*ClusterCosts, error) {
  67. start, end, err := util.ParseTimeRange(window, offset)
  68. if err != nil {
  69. return nil, err
  70. }
  71. // If the number of hours is not given (i.e. is zero) compute one from the window and offset
  72. if dataHours == 0 {
  73. dataHours = end.Sub(*start).Hours()
  74. }
  75. // Do not allow zero-length windows to prevent divide-by-zero issues
  76. if dataHours == 0 {
  77. return nil, fmt.Errorf("illegal time range: window %s, offset %s", window, offset)
  78. }
  79. cc := &ClusterCosts{
  80. Start: start,
  81. End: end,
  82. CPUCumulative: cpu,
  83. GPUCumulative: gpu,
  84. RAMCumulative: ram,
  85. StorageCumulative: storage,
  86. TotalCumulative: cpu + gpu + ram + storage,
  87. CPUMonthly: cpu / dataHours * (util.HoursPerMonth),
  88. GPUMonthly: gpu / dataHours * (util.HoursPerMonth),
  89. RAMMonthly: ram / dataHours * (util.HoursPerMonth),
  90. StorageMonthly: storage / dataHours * (util.HoursPerMonth),
  91. }
  92. cc.TotalMonthly = cc.CPUMonthly + cc.GPUMonthly + cc.RAMMonthly + cc.StorageMonthly
  93. return cc, nil
  94. }
  95. // NewClusterCostsFromMonthly takes monthly-rate cost data over a given time range, computes
  96. // the associated cumulative cost data, and returns the Costs.
  97. func NewClusterCostsFromMonthly(cpuMonthly, gpuMonthly, ramMonthly, storageMonthly float64, window, offset string, dataHours float64) (*ClusterCosts, error) {
  98. start, end, err := util.ParseTimeRange(window, offset)
  99. if err != nil {
  100. return nil, err
  101. }
  102. // If the number of hours is not given (i.e. is zero) compute one from the window and offset
  103. if dataHours == 0 {
  104. dataHours = end.Sub(*start).Hours()
  105. }
  106. // Do not allow zero-length windows to prevent divide-by-zero issues
  107. if dataHours == 0 {
  108. return nil, fmt.Errorf("illegal time range: window %s, offset %s", window, offset)
  109. }
  110. cc := &ClusterCosts{
  111. Start: start,
  112. End: end,
  113. CPUMonthly: cpuMonthly,
  114. GPUMonthly: gpuMonthly,
  115. RAMMonthly: ramMonthly,
  116. StorageMonthly: storageMonthly,
  117. TotalMonthly: cpuMonthly + gpuMonthly + ramMonthly + storageMonthly,
  118. CPUCumulative: cpuMonthly / util.HoursPerMonth * dataHours,
  119. GPUCumulative: gpuMonthly / util.HoursPerMonth * dataHours,
  120. RAMCumulative: ramMonthly / util.HoursPerMonth * dataHours,
  121. StorageCumulative: storageMonthly / util.HoursPerMonth * dataHours,
  122. }
  123. cc.TotalCumulative = cc.CPUCumulative + cc.GPUCumulative + cc.RAMCumulative + cc.StorageCumulative
  124. return cc, nil
  125. }
  126. // ComputeClusterCosts gives the cumulative and monthly-rate cluster costs over a window of time for all clusters.
  127. func ComputeClusterCosts(client prometheus.Client, provider cloud.Provider, window, offset string) (map[string]*ClusterCosts, error) {
  128. // Compute number of minutes in the full interval, for use interpolating missed scrapes or scaling missing data
  129. start, end, err := util.ParseTimeRange(window, offset)
  130. if err != nil {
  131. return nil, err
  132. }
  133. mins := end.Sub(*start).Minutes()
  134. const fmtQueryDataCount = `max(count_over_time(kube_node_status_capacity_cpu_cores[%s:1m]%s))`
  135. const fmtQueryTotalGPU = `sum(
  136. sum_over_time(node_gpu_hourly_cost[%s:1m]%s) / 60
  137. ) by (cluster_id)`
  138. const fmtQueryTotalCPU = `sum(
  139. sum(sum_over_time(kube_node_status_capacity_cpu_cores[%s:1m]%s)) by (node, cluster_id) *
  140. avg(avg_over_time(node_cpu_hourly_cost[%s:1m]%s)) by (node, cluster_id) / 60
  141. ) by (cluster_id)`
  142. const fmtQueryTotalRAM = `sum(
  143. sum(sum_over_time(kube_node_status_capacity_memory_bytes[%s:1m]%s) / 1024 / 1024 / 1024) by (node, cluster_id) *
  144. avg(avg_over_time(node_ram_hourly_cost[%s:1m]%s)) by (node, cluster_id) / 60
  145. ) by (cluster_id)`
  146. const fmtQueryTotalStorage = `sum(
  147. sum(sum_over_time(kube_persistentvolume_capacity_bytes[%s:1m]%s)) by (persistentvolume, cluster_id) / 1024 / 1024 / 1024 *
  148. avg(avg_over_time(pv_hourly_cost[%s:1m]%s)) by (persistentvolume, cluster_id) / 60
  149. ) by (cluster_id) %s`
  150. queryTotalLocalStorage := provider.GetLocalStorageQuery(window, offset, false)
  151. if queryTotalLocalStorage != "" {
  152. queryTotalLocalStorage = fmt.Sprintf(" + %s", queryTotalLocalStorage)
  153. }
  154. fmtOffset := ""
  155. if offset != "" {
  156. fmtOffset = fmt.Sprintf("offset %s", offset)
  157. }
  158. queryDataCount := fmt.Sprintf(fmtQueryDataCount, window, fmtOffset)
  159. queryTotalGPU := fmt.Sprintf(fmtQueryTotalGPU, window, fmtOffset)
  160. queryTotalCPU := fmt.Sprintf(fmtQueryTotalCPU, window, fmtOffset, window, fmtOffset)
  161. queryTotalRAM := fmt.Sprintf(fmtQueryTotalRAM, window, fmtOffset, window, fmtOffset)
  162. queryTotalStorage := fmt.Sprintf(fmtQueryTotalStorage, window, fmtOffset, window, fmtOffset, queryTotalLocalStorage)
  163. numQueries := 5
  164. klog.V(4).Infof("[Debug] queryDataCount: %s", queryDataCount)
  165. klog.V(4).Infof("[Debug] queryTotalGPU: %s", queryTotalGPU)
  166. klog.V(4).Infof("[Debug] queryTotalCPU: %s", queryTotalCPU)
  167. klog.V(4).Infof("[Debug] queryTotalRAM: %s", queryTotalRAM)
  168. klog.V(4).Infof("[Debug] queryTotalStorage: %s", queryTotalStorage)
  169. // Submit queries to Prometheus asynchronously
  170. var ec util.ErrorCollector
  171. var wg sync.WaitGroup
  172. ctx := PromQueryContext{client, &ec, &wg}
  173. ctx.wg.Add(numQueries)
  174. chDataCount := make(chan []*PromQueryResult, 1)
  175. go AsyncPromQuery(queryDataCount, chDataCount, ctx)
  176. chTotalGPU := make(chan []*PromQueryResult, 1)
  177. go AsyncPromQuery(queryTotalGPU, chTotalGPU, ctx)
  178. chTotalCPU := make(chan []*PromQueryResult, 1)
  179. go AsyncPromQuery(queryTotalCPU, chTotalCPU, ctx)
  180. chTotalRAM := make(chan []*PromQueryResult, 1)
  181. go AsyncPromQuery(queryTotalRAM, chTotalRAM, ctx)
  182. chTotalStorage := make(chan []*PromQueryResult, 1)
  183. go AsyncPromQuery(queryTotalStorage, chTotalStorage, ctx)
  184. // After queries complete, retrieve results
  185. wg.Wait()
  186. resultsDataCount := <-chDataCount
  187. close(chDataCount)
  188. resultsTotalGPU := <-chTotalGPU
  189. close(chTotalGPU)
  190. resultsTotalCPU := <-chTotalCPU
  191. close(chTotalCPU)
  192. resultsTotalRAM := <-chTotalRAM
  193. close(chTotalRAM)
  194. resultsTotalStorage := <-chTotalStorage
  195. close(chTotalStorage)
  196. dataMins := mins
  197. if len(resultsDataCount) > 0 && len(resultsDataCount[0].Values) > 0 {
  198. dataMins = resultsDataCount[0].Values[0].Value
  199. } else {
  200. klog.V(3).Infof("[Warning] cluster cost data count returned no results")
  201. }
  202. // Intermediate structure storing mapping of [clusterID][type ∈ {cpu, ram, storage, total}]=cost
  203. costData := make(map[string]map[string]float64)
  204. defaultClusterID := os.Getenv(clusterIDKey)
  205. // Helper function to iterate over Prom query results, parsing the raw values into
  206. // the intermediate costData structure.
  207. setCostsFromResults := func(costData map[string]map[string]float64, results []*PromQueryResult, name string) {
  208. for _, result := range results {
  209. clusterID, _ := result.GetString("cluster_id")
  210. if clusterID == "" {
  211. clusterID = defaultClusterID
  212. }
  213. if _, ok := costData[clusterID]; !ok {
  214. costData[clusterID] = map[string]float64{}
  215. }
  216. if len(result.Values) > 0 {
  217. costData[clusterID][name] += result.Values[0].Value
  218. costData[clusterID]["total"] += result.Values[0].Value
  219. }
  220. }
  221. }
  222. setCostsFromResults(costData, resultsTotalGPU, "gpu")
  223. setCostsFromResults(costData, resultsTotalCPU, "cpu")
  224. setCostsFromResults(costData, resultsTotalRAM, "ram")
  225. setCostsFromResults(costData, resultsTotalStorage, "storage")
  226. // Convert intermediate structure to Costs instances
  227. costsByCluster := map[string]*ClusterCosts{}
  228. for id, cd := range costData {
  229. costs, err := NewClusterCostsFromCumulative(cd["cpu"], cd["gpu"], cd["ram"], cd["storage"], window, offset, dataMins/util.MinsPerHour)
  230. if err != nil {
  231. klog.V(3).Infof("[Warning] Failed to parse cluster costs on %s (%s) from cumulative data: %+v", window, offset, cd)
  232. return nil, err
  233. }
  234. costsByCluster[id] = costs
  235. }
  236. return costsByCluster, nil
  237. }
  238. type Totals struct {
  239. TotalCost [][]string `json:"totalcost"`
  240. CPUCost [][]string `json:"cpucost"`
  241. MemCost [][]string `json:"memcost"`
  242. StorageCost [][]string `json:"storageCost"`
  243. }
  244. func resultToTotals(qr interface{}) ([][]string, error) {
  245. results, err := NewQueryResults(qr)
  246. if err != nil {
  247. return nil, err
  248. }
  249. if len(results) == 0 {
  250. return nil, fmt.Errorf("Not enough data available in the selected time range")
  251. }
  252. result := results[0]
  253. totals := [][]string{}
  254. for _, value := range result.Values {
  255. d0 := fmt.Sprintf("%f", value.Timestamp)
  256. d1 := fmt.Sprintf("%f", value.Value)
  257. toAppend := []string{
  258. d0,
  259. d1,
  260. }
  261. totals = append(totals, toAppend)
  262. }
  263. return totals, nil
  264. }
  265. func resultToTotal(qr interface{}) (map[string][][]string, error) {
  266. defaultClusterID := os.Getenv(clusterIDKey)
  267. results, err := NewQueryResults(qr)
  268. if err != nil {
  269. return nil, err
  270. }
  271. toReturn := make(map[string][][]string)
  272. for _, result := range results {
  273. clusterID, _ := result.GetString("cluster_id")
  274. if clusterID == "" {
  275. clusterID = defaultClusterID
  276. }
  277. // Expect a single value only
  278. if len(result.Values) == 0 {
  279. klog.V(1).Infof("[Warning] Metric values did not contain any valid data.")
  280. continue
  281. }
  282. value := result.Values[0]
  283. d0 := fmt.Sprintf("%f", value.Timestamp)
  284. d1 := fmt.Sprintf("%f", value.Value)
  285. toAppend := []string{
  286. d0,
  287. d1,
  288. }
  289. if t, ok := toReturn[clusterID]; ok {
  290. t = append(t, toAppend)
  291. } else {
  292. toReturn[clusterID] = [][]string{toAppend}
  293. }
  294. }
  295. return toReturn, nil
  296. }
  297. // ClusterCostsForAllClusters gives the cluster costs averaged over a window of time for all clusters.
  298. func ClusterCostsForAllClusters(cli prometheus.Client, provider cloud.Provider, window, offset string) (map[string]*Totals, error) {
  299. localStorageQuery := provider.GetLocalStorageQuery(window, offset, true)
  300. if localStorageQuery != "" {
  301. localStorageQuery = fmt.Sprintf("+ %s", localStorageQuery)
  302. }
  303. fmtOffset := ""
  304. if offset != "" {
  305. fmtOffset = fmt.Sprintf("offset %s", offset)
  306. }
  307. qCores := fmt.Sprintf(queryClusterCores, window, fmtOffset, window, fmtOffset, window, fmtOffset)
  308. qRAM := fmt.Sprintf(queryClusterRAM, window, fmtOffset, window, fmtOffset)
  309. qStorage := fmt.Sprintf(queryStorage, window, fmtOffset, window, fmtOffset, localStorageQuery)
  310. klog.V(4).Infof("Running query %s", qCores)
  311. resultClusterCores, err := Query(cli, qCores)
  312. if err != nil {
  313. return nil, fmt.Errorf("Error for query %s: %s", qCores, err.Error())
  314. }
  315. klog.V(4).Infof("Running query %s", qRAM)
  316. resultClusterRAM, err := Query(cli, qRAM)
  317. if err != nil {
  318. return nil, fmt.Errorf("Error for query %s: %s", qRAM, err.Error())
  319. }
  320. klog.V(4).Infof("Running query %s", qRAM)
  321. resultStorage, err := Query(cli, qStorage)
  322. if err != nil {
  323. return nil, fmt.Errorf("Error for query %s: %s", qStorage, err.Error())
  324. }
  325. toReturn := make(map[string]*Totals)
  326. coreTotal, err := resultToTotal(resultClusterCores)
  327. if err != nil {
  328. return nil, fmt.Errorf("Error for query %s: %s", qCores, err.Error())
  329. }
  330. for clusterID, total := range coreTotal {
  331. if _, ok := toReturn[clusterID]; !ok {
  332. toReturn[clusterID] = &Totals{}
  333. }
  334. toReturn[clusterID].CPUCost = total
  335. }
  336. ramTotal, err := resultToTotal(resultClusterRAM)
  337. if err != nil {
  338. return nil, fmt.Errorf("Error for query %s: %s", qRAM, err.Error())
  339. }
  340. for clusterID, total := range ramTotal {
  341. if _, ok := toReturn[clusterID]; !ok {
  342. toReturn[clusterID] = &Totals{}
  343. }
  344. toReturn[clusterID].MemCost = total
  345. }
  346. storageTotal, err := resultToTotal(resultStorage)
  347. if err != nil {
  348. return nil, fmt.Errorf("Error for query %s: %s", qStorage, err.Error())
  349. }
  350. for clusterID, total := range storageTotal {
  351. if _, ok := toReturn[clusterID]; !ok {
  352. toReturn[clusterID] = &Totals{}
  353. }
  354. toReturn[clusterID].StorageCost = total
  355. }
  356. return toReturn, nil
  357. }
  358. // AverageClusterTotals gives the current full cluster costs averaged over a window of time.
  359. // Used to be ClutserCosts, but has been deprecated for that use.
  360. func AverageClusterTotals(cli prometheus.Client, provider cloud.Provider, windowString, offset string) (*Totals, error) {
  361. // turn offsets of the format "[0-9+]h" into the format "offset [0-9+]h" for use in query templatess
  362. fmtOffset := ""
  363. if offset != "" {
  364. fmtOffset = fmt.Sprintf("offset %s", offset)
  365. }
  366. localStorageQuery := provider.GetLocalStorageQuery(windowString, offset, true)
  367. if localStorageQuery != "" {
  368. localStorageQuery = fmt.Sprintf("+ %s", localStorageQuery)
  369. }
  370. qCores := fmt.Sprintf(queryClusterCores, windowString, fmtOffset, windowString, fmtOffset, windowString, fmtOffset)
  371. qRAM := fmt.Sprintf(queryClusterRAM, windowString, fmtOffset, windowString, fmtOffset)
  372. qStorage := fmt.Sprintf(queryStorage, windowString, fmtOffset, windowString, fmtOffset, localStorageQuery)
  373. qTotal := fmt.Sprintf(queryTotal, localStorageQuery)
  374. resultClusterCores, err := Query(cli, qCores)
  375. if err != nil {
  376. return nil, err
  377. }
  378. resultClusterRAM, err := Query(cli, qRAM)
  379. if err != nil {
  380. return nil, err
  381. }
  382. resultStorage, err := Query(cli, qStorage)
  383. if err != nil {
  384. return nil, err
  385. }
  386. resultTotal, err := Query(cli, qTotal)
  387. if err != nil {
  388. return nil, err
  389. }
  390. coreTotal, err := resultToTotal(resultClusterCores)
  391. if err != nil {
  392. return nil, err
  393. }
  394. ramTotal, err := resultToTotal(resultClusterRAM)
  395. if err != nil {
  396. return nil, err
  397. }
  398. storageTotal, err := resultToTotal(resultStorage)
  399. if err != nil {
  400. return nil, err
  401. }
  402. clusterTotal, err := resultToTotal(resultTotal)
  403. if err != nil {
  404. return nil, err
  405. }
  406. defaultClusterID := os.Getenv(clusterIDKey)
  407. return &Totals{
  408. TotalCost: clusterTotal[defaultClusterID],
  409. CPUCost: coreTotal[defaultClusterID],
  410. MemCost: ramTotal[defaultClusterID],
  411. StorageCost: storageTotal[defaultClusterID],
  412. }, nil
  413. }
  414. // ClusterCostsOverTime gives the full cluster costs over time
  415. func ClusterCostsOverTime(cli prometheus.Client, provider cloud.Provider, startString, endString, windowString, offset string) (*Totals, error) {
  416. localStorageQuery := provider.GetLocalStorageQuery(windowString, offset, true)
  417. if localStorageQuery != "" {
  418. localStorageQuery = fmt.Sprintf("+ %s", localStorageQuery)
  419. }
  420. layout := "2006-01-02T15:04:05.000Z"
  421. start, err := time.Parse(layout, startString)
  422. if err != nil {
  423. klog.V(1).Infof("Error parsing time " + startString + ". Error: " + err.Error())
  424. return nil, err
  425. }
  426. end, err := time.Parse(layout, endString)
  427. if err != nil {
  428. klog.V(1).Infof("Error parsing time " + endString + ". Error: " + err.Error())
  429. return nil, err
  430. }
  431. window, err := time.ParseDuration(windowString)
  432. if err != nil {
  433. klog.V(1).Infof("Error parsing time " + windowString + ". Error: " + err.Error())
  434. return nil, err
  435. }
  436. // turn offsets of the format "[0-9+]h" into the format "offset [0-9+]h" for use in query templatess
  437. if offset != "" {
  438. offset = fmt.Sprintf("offset %s", offset)
  439. }
  440. qCores := fmt.Sprintf(queryClusterCores, windowString, offset, windowString, offset, windowString, offset)
  441. qRAM := fmt.Sprintf(queryClusterRAM, windowString, offset, windowString, offset)
  442. qStorage := fmt.Sprintf(queryStorage, windowString, offset, windowString, offset, localStorageQuery)
  443. qTotal := fmt.Sprintf(queryTotal, localStorageQuery)
  444. resultClusterCores, err := QueryRange(cli, qCores, start, end, window)
  445. if err != nil {
  446. return nil, err
  447. }
  448. resultClusterRAM, err := QueryRange(cli, qRAM, start, end, window)
  449. if err != nil {
  450. return nil, err
  451. }
  452. resultStorage, err := QueryRange(cli, qStorage, start, end, window)
  453. if err != nil {
  454. return nil, err
  455. }
  456. resultTotal, err := QueryRange(cli, qTotal, start, end, window)
  457. if err != nil {
  458. return nil, err
  459. }
  460. coreTotal, err := resultToTotals(resultClusterCores)
  461. if err != nil {
  462. return nil, err
  463. }
  464. ramTotal, err := resultToTotals(resultClusterRAM)
  465. if err != nil {
  466. return nil, err
  467. }
  468. storageTotal, err := resultToTotals(resultStorage)
  469. if err != nil {
  470. return nil, err
  471. }
  472. clusterTotal, err := resultToTotals(resultTotal)
  473. if err != nil {
  474. return nil, err
  475. }
  476. return &Totals{
  477. TotalCost: clusterTotal,
  478. CPUCost: coreTotal,
  479. MemCost: ramTotal,
  480. StorageCost: storageTotal,
  481. }, nil
  482. }