cluster.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. package costmodel
  2. import (
  3. "fmt"
  4. "os"
  5. "sync"
  6. "time"
  7. "github.com/kubecost/cost-model/pkg/cloud"
  8. "github.com/kubecost/cost-model/pkg/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 -> util.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 = `count_over_time(sum(kube_node_status_capacity_cpu_cores) by (cluster_id)[%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. defaultClusterID := os.Getenv(clusterIDKey)
  197. dataMinsByCluster := map[string]float64{}
  198. for _, result := range resultsDataCount {
  199. clusterID, _ := result.GetString("cluster_id")
  200. if clusterID == "" {
  201. clusterID = defaultClusterID
  202. }
  203. dataMins := mins
  204. if len(result.Values) > 0 {
  205. dataMins = result.Values[0].Value
  206. } else {
  207. klog.V(3).Infof("[Warning] cluster cost data count returned no results for cluster %s", clusterID)
  208. }
  209. dataMinsByCluster[clusterID] = dataMins
  210. }
  211. // Intermediate structure storing mapping of [clusterID][type ∈ {cpu, ram, storage, total}]=cost
  212. costData := make(map[string]map[string]float64)
  213. // Helper function to iterate over Prom query results, parsing the raw values into
  214. // the intermediate costData structure.
  215. setCostsFromResults := func(costData map[string]map[string]float64, results []*PromQueryResult, name string) {
  216. for _, result := range results {
  217. clusterID, _ := result.GetString("cluster_id")
  218. if clusterID == "" {
  219. clusterID = defaultClusterID
  220. }
  221. if _, ok := costData[clusterID]; !ok {
  222. costData[clusterID] = map[string]float64{}
  223. }
  224. if len(result.Values) > 0 {
  225. costData[clusterID][name] += result.Values[0].Value
  226. costData[clusterID]["total"] += result.Values[0].Value
  227. }
  228. }
  229. }
  230. setCostsFromResults(costData, resultsTotalGPU, "gpu")
  231. setCostsFromResults(costData, resultsTotalCPU, "cpu")
  232. setCostsFromResults(costData, resultsTotalRAM, "ram")
  233. setCostsFromResults(costData, resultsTotalStorage, "storage")
  234. // Convert intermediate structure to Costs instances
  235. costsByCluster := map[string]*ClusterCosts{}
  236. for id, cd := range costData {
  237. dataMins, ok := dataMinsByCluster[id]
  238. if !ok {
  239. dataMins = mins
  240. klog.V(3).Infof("[Warning] cluster cost data count not found for cluster %s", id)
  241. }
  242. costs, err := NewClusterCostsFromCumulative(cd["cpu"], cd["gpu"], cd["ram"], cd["storage"], window, offset, dataMins/util.MinsPerHour)
  243. if err != nil {
  244. klog.V(3).Infof("[Warning] Failed to parse cluster costs on %s (%s) from cumulative data: %+v", window, offset, cd)
  245. return nil, err
  246. }
  247. costsByCluster[id] = costs
  248. }
  249. return costsByCluster, nil
  250. }
  251. type Totals struct {
  252. TotalCost [][]string `json:"totalcost"`
  253. CPUCost [][]string `json:"cpucost"`
  254. MemCost [][]string `json:"memcost"`
  255. StorageCost [][]string `json:"storageCost"`
  256. }
  257. func resultToTotals(qr interface{}) ([][]string, error) {
  258. results, err := NewQueryResults(qr)
  259. if err != nil {
  260. return nil, err
  261. }
  262. if len(results) == 0 {
  263. return nil, fmt.Errorf("Not enough data available in the selected time range")
  264. }
  265. result := results[0]
  266. totals := [][]string{}
  267. for _, value := range result.Values {
  268. d0 := fmt.Sprintf("%f", value.Timestamp)
  269. d1 := fmt.Sprintf("%f", value.Value)
  270. toAppend := []string{
  271. d0,
  272. d1,
  273. }
  274. totals = append(totals, toAppend)
  275. }
  276. return totals, nil
  277. }
  278. func resultToTotal(qr interface{}) (map[string][][]string, error) {
  279. defaultClusterID := os.Getenv(clusterIDKey)
  280. results, err := NewQueryResults(qr)
  281. if err != nil {
  282. return nil, err
  283. }
  284. toReturn := make(map[string][][]string)
  285. for _, result := range results {
  286. clusterID, _ := result.GetString("cluster_id")
  287. if clusterID == "" {
  288. clusterID = defaultClusterID
  289. }
  290. // Expect a single value only
  291. if len(result.Values) == 0 {
  292. klog.V(1).Infof("[Warning] Metric values did not contain any valid data.")
  293. continue
  294. }
  295. value := result.Values[0]
  296. d0 := fmt.Sprintf("%f", value.Timestamp)
  297. d1 := fmt.Sprintf("%f", value.Value)
  298. toAppend := []string{
  299. d0,
  300. d1,
  301. }
  302. if t, ok := toReturn[clusterID]; ok {
  303. t = append(t, toAppend)
  304. } else {
  305. toReturn[clusterID] = [][]string{toAppend}
  306. }
  307. }
  308. return toReturn, nil
  309. }
  310. // ClusterCostsForAllClusters gives the cluster costs averaged over a window of time for all clusters.
  311. func ClusterCostsForAllClusters(cli prometheus.Client, provider cloud.Provider, window, offset string) (map[string]*Totals, error) {
  312. localStorageQuery := provider.GetLocalStorageQuery(window, offset, true)
  313. if localStorageQuery != "" {
  314. localStorageQuery = fmt.Sprintf("+ %s", localStorageQuery)
  315. }
  316. fmtOffset := ""
  317. if offset != "" {
  318. fmtOffset = fmt.Sprintf("offset %s", offset)
  319. }
  320. qCores := fmt.Sprintf(queryClusterCores, window, fmtOffset, window, fmtOffset, window, fmtOffset)
  321. qRAM := fmt.Sprintf(queryClusterRAM, window, fmtOffset, window, fmtOffset)
  322. qStorage := fmt.Sprintf(queryStorage, window, fmtOffset, window, fmtOffset, localStorageQuery)
  323. klog.V(4).Infof("Running query %s", qCores)
  324. resultClusterCores, err := Query(cli, qCores)
  325. if err != nil {
  326. return nil, fmt.Errorf("Error for query %s: %s", qCores, err.Error())
  327. }
  328. klog.V(4).Infof("Running query %s", qRAM)
  329. resultClusterRAM, err := Query(cli, qRAM)
  330. if err != nil {
  331. return nil, fmt.Errorf("Error for query %s: %s", qRAM, err.Error())
  332. }
  333. klog.V(4).Infof("Running query %s", qRAM)
  334. resultStorage, err := Query(cli, qStorage)
  335. if err != nil {
  336. return nil, fmt.Errorf("Error for query %s: %s", qStorage, err.Error())
  337. }
  338. toReturn := make(map[string]*Totals)
  339. coreTotal, err := resultToTotal(resultClusterCores)
  340. if err != nil {
  341. return nil, fmt.Errorf("Error for query %s: %s", qCores, err.Error())
  342. }
  343. for clusterID, total := range coreTotal {
  344. if _, ok := toReturn[clusterID]; !ok {
  345. toReturn[clusterID] = &Totals{}
  346. }
  347. toReturn[clusterID].CPUCost = total
  348. }
  349. ramTotal, err := resultToTotal(resultClusterRAM)
  350. if err != nil {
  351. return nil, fmt.Errorf("Error for query %s: %s", qRAM, err.Error())
  352. }
  353. for clusterID, total := range ramTotal {
  354. if _, ok := toReturn[clusterID]; !ok {
  355. toReturn[clusterID] = &Totals{}
  356. }
  357. toReturn[clusterID].MemCost = total
  358. }
  359. storageTotal, err := resultToTotal(resultStorage)
  360. if err != nil {
  361. return nil, fmt.Errorf("Error for query %s: %s", qStorage, err.Error())
  362. }
  363. for clusterID, total := range storageTotal {
  364. if _, ok := toReturn[clusterID]; !ok {
  365. toReturn[clusterID] = &Totals{}
  366. }
  367. toReturn[clusterID].StorageCost = total
  368. }
  369. return toReturn, nil
  370. }
  371. // AverageClusterTotals gives the current full cluster costs averaged over a window of time.
  372. // Used to be ClutserCosts, but has been deprecated for that use.
  373. func AverageClusterTotals(cli prometheus.Client, provider cloud.Provider, windowString, offset string) (*Totals, error) {
  374. // turn offsets of the format "[0-9+]h" into the format "offset [0-9+]h" for use in query templatess
  375. fmtOffset := ""
  376. if offset != "" {
  377. fmtOffset = fmt.Sprintf("offset %s", offset)
  378. }
  379. localStorageQuery := provider.GetLocalStorageQuery(windowString, offset, true)
  380. if localStorageQuery != "" {
  381. localStorageQuery = fmt.Sprintf("+ %s", localStorageQuery)
  382. }
  383. qCores := fmt.Sprintf(queryClusterCores, windowString, fmtOffset, windowString, fmtOffset, windowString, fmtOffset)
  384. qRAM := fmt.Sprintf(queryClusterRAM, windowString, fmtOffset, windowString, fmtOffset)
  385. qStorage := fmt.Sprintf(queryStorage, windowString, fmtOffset, windowString, fmtOffset, localStorageQuery)
  386. qTotal := fmt.Sprintf(queryTotal, localStorageQuery)
  387. resultClusterCores, err := Query(cli, qCores)
  388. if err != nil {
  389. return nil, err
  390. }
  391. resultClusterRAM, err := Query(cli, qRAM)
  392. if err != nil {
  393. return nil, err
  394. }
  395. resultStorage, err := Query(cli, qStorage)
  396. if err != nil {
  397. return nil, err
  398. }
  399. resultTotal, err := Query(cli, qTotal)
  400. if err != nil {
  401. return nil, err
  402. }
  403. coreTotal, err := resultToTotal(resultClusterCores)
  404. if err != nil {
  405. return nil, err
  406. }
  407. ramTotal, err := resultToTotal(resultClusterRAM)
  408. if err != nil {
  409. return nil, err
  410. }
  411. storageTotal, err := resultToTotal(resultStorage)
  412. if err != nil {
  413. return nil, err
  414. }
  415. clusterTotal, err := resultToTotal(resultTotal)
  416. if err != nil {
  417. return nil, err
  418. }
  419. defaultClusterID := os.Getenv(clusterIDKey)
  420. return &Totals{
  421. TotalCost: clusterTotal[defaultClusterID],
  422. CPUCost: coreTotal[defaultClusterID],
  423. MemCost: ramTotal[defaultClusterID],
  424. StorageCost: storageTotal[defaultClusterID],
  425. }, nil
  426. }
  427. // ClusterCostsOverTime gives the full cluster costs over time
  428. func ClusterCostsOverTime(cli prometheus.Client, provider cloud.Provider, startString, endString, windowString, offset string) (*Totals, error) {
  429. localStorageQuery := provider.GetLocalStorageQuery(windowString, offset, true)
  430. if localStorageQuery != "" {
  431. localStorageQuery = fmt.Sprintf("+ %s", localStorageQuery)
  432. }
  433. layout := "2006-01-02T15:04:05.000Z"
  434. start, err := time.Parse(layout, startString)
  435. if err != nil {
  436. klog.V(1).Infof("Error parsing time " + startString + ". Error: " + err.Error())
  437. return nil, err
  438. }
  439. end, err := time.Parse(layout, endString)
  440. if err != nil {
  441. klog.V(1).Infof("Error parsing time " + endString + ". Error: " + err.Error())
  442. return nil, err
  443. }
  444. window, err := time.ParseDuration(windowString)
  445. if err != nil {
  446. klog.V(1).Infof("Error parsing time " + windowString + ". Error: " + err.Error())
  447. return nil, err
  448. }
  449. // turn offsets of the format "[0-9+]h" into the format "offset [0-9+]h" for use in query templatess
  450. if offset != "" {
  451. offset = fmt.Sprintf("offset %s", offset)
  452. }
  453. qCores := fmt.Sprintf(queryClusterCores, windowString, offset, windowString, offset, windowString, offset)
  454. qRAM := fmt.Sprintf(queryClusterRAM, windowString, offset, windowString, offset)
  455. qStorage := fmt.Sprintf(queryStorage, windowString, offset, windowString, offset, localStorageQuery)
  456. qTotal := fmt.Sprintf(queryTotal, localStorageQuery)
  457. resultClusterCores, err := QueryRange(cli, qCores, start, end, window)
  458. if err != nil {
  459. return nil, err
  460. }
  461. resultClusterRAM, err := QueryRange(cli, qRAM, start, end, window)
  462. if err != nil {
  463. return nil, err
  464. }
  465. resultStorage, err := QueryRange(cli, qStorage, start, end, window)
  466. if err != nil {
  467. return nil, err
  468. }
  469. resultTotal, err := QueryRange(cli, qTotal, start, end, window)
  470. if err != nil {
  471. return nil, err
  472. }
  473. coreTotal, err := resultToTotals(resultClusterCores)
  474. if err != nil {
  475. return nil, err
  476. }
  477. ramTotal, err := resultToTotals(resultClusterRAM)
  478. if err != nil {
  479. return nil, err
  480. }
  481. storageTotal, err := resultToTotals(resultStorage)
  482. if err != nil {
  483. klog.V(3).Infof("[Warning] no storage data: %s", err)
  484. }
  485. clusterTotal, err := resultToTotals(resultTotal)
  486. if err != nil {
  487. return nil, err
  488. }
  489. return &Totals{
  490. TotalCost: clusterTotal,
  491. CPUCost: coreTotal,
  492. MemCost: ramTotal,
  493. StorageCost: storageTotal,
  494. }, nil
  495. }