router.go 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200
  1. package costmodel
  2. import (
  3. "context"
  4. "flag"
  5. "fmt"
  6. "net/http"
  7. "reflect"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. "time"
  12. "github.com/kubecost/cost-model/pkg/services"
  13. "github.com/kubecost/cost-model/pkg/util/httputil"
  14. "github.com/kubecost/cost-model/pkg/util/timeutil"
  15. "github.com/kubecost/cost-model/pkg/util/watcher"
  16. "k8s.io/klog"
  17. "github.com/julienschmidt/httprouter"
  18. sentry "github.com/getsentry/sentry-go"
  19. "github.com/kubecost/cost-model/pkg/cloud"
  20. "github.com/kubecost/cost-model/pkg/clustercache"
  21. "github.com/kubecost/cost-model/pkg/costmodel/clusters"
  22. "github.com/kubecost/cost-model/pkg/env"
  23. "github.com/kubecost/cost-model/pkg/errors"
  24. "github.com/kubecost/cost-model/pkg/kubecost"
  25. "github.com/kubecost/cost-model/pkg/log"
  26. "github.com/kubecost/cost-model/pkg/prom"
  27. "github.com/kubecost/cost-model/pkg/thanos"
  28. "github.com/kubecost/cost-model/pkg/util/json"
  29. prometheus "github.com/prometheus/client_golang/api"
  30. prometheusAPI "github.com/prometheus/client_golang/api/prometheus/v1"
  31. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  32. "github.com/patrickmn/go-cache"
  33. "k8s.io/client-go/kubernetes"
  34. "k8s.io/client-go/rest"
  35. "k8s.io/client-go/tools/clientcmd"
  36. )
  37. const (
  38. RFC3339Milli = "2006-01-02T15:04:05.000Z"
  39. maxCacheMinutes1d = 11
  40. maxCacheMinutes2d = 17
  41. maxCacheMinutes7d = 37
  42. maxCacheMinutes30d = 137
  43. CustomPricingSetting = "CustomPricing"
  44. DiscountSetting = "Discount"
  45. )
  46. var (
  47. // gitCommit is set by the build system
  48. gitCommit string
  49. )
  50. // Accesses defines a singleton application instance, providing access to
  51. // Prometheus, Kubernetes, the cloud provider, and caches.
  52. type Accesses struct {
  53. Router *httprouter.Router
  54. PrometheusClient prometheus.Client
  55. ThanosClient prometheus.Client
  56. KubeClientSet kubernetes.Interface
  57. ClusterMap clusters.ClusterMap
  58. CloudProvider cloud.Provider
  59. Model *CostModel
  60. MetricsEmitter *CostModelMetricsEmitter
  61. OutOfClusterCache *cache.Cache
  62. AggregateCache *cache.Cache
  63. CostDataCache *cache.Cache
  64. ClusterCostsCache *cache.Cache
  65. CacheExpiration map[time.Duration]time.Duration
  66. AggAPI Aggregator
  67. // SettingsCache stores current state of app settings
  68. SettingsCache *cache.Cache
  69. // settingsSubscribers tracks channels through which changes to different
  70. // settings will be published in a pub/sub model
  71. settingsSubscribers map[string][]chan string
  72. settingsMutex sync.Mutex
  73. // registered http service instances
  74. httpServices services.HTTPServices
  75. }
  76. // GetPrometheusClient decides whether the default Prometheus client or the Thanos client
  77. // should be used.
  78. func (a *Accesses) GetPrometheusClient(remote bool) prometheus.Client {
  79. // Use Thanos Client if it exists (enabled) and remote flag set
  80. var pc prometheus.Client
  81. if remote && a.ThanosClient != nil {
  82. pc = a.ThanosClient
  83. } else {
  84. pc = a.PrometheusClient
  85. }
  86. return pc
  87. }
  88. // GetCacheExpiration looks up and returns custom cache expiration for the given duration.
  89. // If one does not exists, it returns the default cache expiration, which is defined by
  90. // the particular cache.
  91. func (a *Accesses) GetCacheExpiration(dur time.Duration) time.Duration {
  92. if expiration, ok := a.CacheExpiration[dur]; ok {
  93. return expiration
  94. }
  95. return cache.DefaultExpiration
  96. }
  97. // GetCacheRefresh determines how long to wait before refreshing the cache for the given duration,
  98. // which is done 1 minute before we expect the cache to expire, or 1 minute if expiration is
  99. // not found or is less than 2 minutes.
  100. func (a *Accesses) GetCacheRefresh(dur time.Duration) time.Duration {
  101. expiry := a.GetCacheExpiration(dur).Minutes()
  102. if expiry <= 2.0 {
  103. return time.Minute
  104. }
  105. mins := time.Duration(expiry/2.0) * time.Minute
  106. return mins
  107. }
  108. func (a *Accesses) ClusterCostsFromCacheHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  109. w.Header().Set("Content-Type", "application/json")
  110. duration := 24 * time.Hour
  111. offset := time.Minute
  112. durationHrs := "24h"
  113. fmtOffset := "1m"
  114. pClient := a.GetPrometheusClient(true)
  115. key := fmt.Sprintf("%s:%s", durationHrs, fmtOffset)
  116. if data, valid := a.ClusterCostsCache.Get(key); valid {
  117. clusterCosts := data.(map[string]*ClusterCosts)
  118. w.Write(WrapDataWithMessage(clusterCosts, nil, "clusterCosts cache hit"))
  119. } else {
  120. data, err := a.ComputeClusterCosts(pClient, a.CloudProvider, duration, offset, true)
  121. w.Write(WrapDataWithMessage(data, err, fmt.Sprintf("clusterCosts cache miss: %s", key)))
  122. }
  123. }
  124. type Response struct {
  125. Code int `json:"code"`
  126. Status string `json:"status"`
  127. Data interface{} `json:"data"`
  128. Message string `json:"message,omitempty"`
  129. Warning string `json:"warning,omitempty"`
  130. }
  131. // FilterFunc is a filter that returns true iff the given CostData should be filtered out, and the environment that was used as the filter criteria, if it was an aggregate
  132. type FilterFunc func(*CostData) (bool, string)
  133. // FilterCostData allows through only CostData that matches all the given filter functions
  134. func FilterCostData(data map[string]*CostData, retains []FilterFunc, filters []FilterFunc) (map[string]*CostData, int, map[string]int) {
  135. result := make(map[string]*CostData)
  136. filteredEnvironments := make(map[string]int)
  137. filteredContainers := 0
  138. DataLoop:
  139. for key, datum := range data {
  140. for _, rf := range retains {
  141. if ok, _ := rf(datum); ok {
  142. result[key] = datum
  143. // if any retain function passes, the data is retained and move on
  144. continue DataLoop
  145. }
  146. }
  147. for _, ff := range filters {
  148. if ok, environment := ff(datum); !ok {
  149. if environment != "" {
  150. filteredEnvironments[environment]++
  151. }
  152. filteredContainers++
  153. // if any filter function check fails, move on to the next datum
  154. continue DataLoop
  155. }
  156. }
  157. result[key] = datum
  158. }
  159. return result, filteredContainers, filteredEnvironments
  160. }
  161. func filterFields(fields string, data map[string]*CostData) map[string]CostData {
  162. fs := strings.Split(fields, ",")
  163. fmap := make(map[string]bool)
  164. for _, f := range fs {
  165. fieldNameLower := strings.ToLower(f) // convert to go struct name by uppercasing first letter
  166. klog.V(1).Infof("to delete: %s", fieldNameLower)
  167. fmap[fieldNameLower] = true
  168. }
  169. filteredData := make(map[string]CostData)
  170. for cname, costdata := range data {
  171. s := reflect.TypeOf(*costdata)
  172. val := reflect.ValueOf(*costdata)
  173. costdata2 := CostData{}
  174. cd2 := reflect.New(reflect.Indirect(reflect.ValueOf(costdata2)).Type()).Elem()
  175. n := s.NumField()
  176. for i := 0; i < n; i++ {
  177. field := s.Field(i)
  178. value := val.Field(i)
  179. value2 := cd2.Field(i)
  180. if _, ok := fmap[strings.ToLower(field.Name)]; !ok {
  181. value2.Set(reflect.Value(value))
  182. }
  183. }
  184. filteredData[cname] = cd2.Interface().(CostData)
  185. }
  186. return filteredData
  187. }
  188. func normalizeTimeParam(param string) (string, error) {
  189. if param == "" {
  190. return "", fmt.Errorf("invalid time param")
  191. }
  192. // convert days to hours
  193. if param[len(param)-1:] == "d" {
  194. count := param[:len(param)-1]
  195. val, err := strconv.ParseInt(count, 10, 64)
  196. if err != nil {
  197. return "", err
  198. }
  199. val = val * 24
  200. param = fmt.Sprintf("%dh", val)
  201. }
  202. return param, nil
  203. }
  204. // ParsePercentString takes a string of expected format "N%" and returns a floating point 0.0N.
  205. // If the "%" symbol is missing, it just returns 0.0N. Empty string is interpreted as "0%" and
  206. // return 0.0.
  207. func ParsePercentString(percentStr string) (float64, error) {
  208. if len(percentStr) == 0 {
  209. return 0.0, nil
  210. }
  211. if percentStr[len(percentStr)-1:] == "%" {
  212. percentStr = percentStr[:len(percentStr)-1]
  213. }
  214. discount, err := strconv.ParseFloat(percentStr, 64)
  215. if err != nil {
  216. return 0.0, err
  217. }
  218. discount *= 0.01
  219. return discount, nil
  220. }
  221. func WrapData(data interface{}, err error) []byte {
  222. var resp []byte
  223. if err != nil {
  224. klog.V(1).Infof("Error returned to client: %s", err.Error())
  225. resp, _ = json.Marshal(&Response{
  226. Code: http.StatusInternalServerError,
  227. Status: "error",
  228. Message: err.Error(),
  229. Data: data,
  230. })
  231. } else {
  232. resp, _ = json.Marshal(&Response{
  233. Code: http.StatusOK,
  234. Status: "success",
  235. Data: data,
  236. })
  237. }
  238. return resp
  239. }
  240. func WrapDataWithMessage(data interface{}, err error, message string) []byte {
  241. var resp []byte
  242. if err != nil {
  243. klog.V(1).Infof("Error returned to client: %s", err.Error())
  244. resp, _ = json.Marshal(&Response{
  245. Code: http.StatusInternalServerError,
  246. Status: "error",
  247. Message: err.Error(),
  248. Data: data,
  249. })
  250. } else {
  251. resp, _ = json.Marshal(&Response{
  252. Code: http.StatusOK,
  253. Status: "success",
  254. Data: data,
  255. Message: message,
  256. })
  257. }
  258. return resp
  259. }
  260. func WrapDataWithWarning(data interface{}, err error, warning string) []byte {
  261. var resp []byte
  262. if err != nil {
  263. klog.V(1).Infof("Error returned to client: %s", err.Error())
  264. resp, _ = json.Marshal(&Response{
  265. Code: http.StatusInternalServerError,
  266. Status: "error",
  267. Message: err.Error(),
  268. Warning: warning,
  269. Data: data,
  270. })
  271. } else {
  272. resp, _ = json.Marshal(&Response{
  273. Code: http.StatusOK,
  274. Status: "success",
  275. Data: data,
  276. Warning: warning,
  277. })
  278. }
  279. return resp
  280. }
  281. func WrapDataWithMessageAndWarning(data interface{}, err error, message, warning string) []byte {
  282. var resp []byte
  283. if err != nil {
  284. klog.V(1).Infof("Error returned to client: %s", err.Error())
  285. resp, _ = json.Marshal(&Response{
  286. Code: http.StatusInternalServerError,
  287. Status: "error",
  288. Message: err.Error(),
  289. Warning: warning,
  290. Data: data,
  291. })
  292. } else {
  293. resp, _ = json.Marshal(&Response{
  294. Code: http.StatusOK,
  295. Status: "success",
  296. Data: data,
  297. Message: message,
  298. Warning: warning,
  299. })
  300. }
  301. return resp
  302. }
  303. // RefreshPricingData needs to be called when a new node joins the fleet, since we cache the relevant subsets of pricing data to avoid storing the whole thing.
  304. func (a *Accesses) RefreshPricingData(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  305. w.Header().Set("Content-Type", "application/json")
  306. w.Header().Set("Access-Control-Allow-Origin", "*")
  307. err := a.CloudProvider.DownloadPricingData()
  308. w.Write(WrapData(nil, err))
  309. }
  310. func (a *Accesses) CostDataModel(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  311. w.Header().Set("Content-Type", "application/json")
  312. w.Header().Set("Access-Control-Allow-Origin", "*")
  313. window := r.URL.Query().Get("timeWindow")
  314. offset := r.URL.Query().Get("offset")
  315. fields := r.URL.Query().Get("filterFields")
  316. namespace := r.URL.Query().Get("namespace")
  317. if offset != "" {
  318. offset = "offset " + offset
  319. }
  320. data, err := a.Model.ComputeCostData(a.PrometheusClient, a.CloudProvider, window, offset, namespace)
  321. if fields != "" {
  322. filteredData := filterFields(fields, data)
  323. w.Write(WrapData(filteredData, err))
  324. } else {
  325. w.Write(WrapData(data, err))
  326. }
  327. }
  328. func (a *Accesses) ClusterCosts(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  329. w.Header().Set("Content-Type", "application/json")
  330. w.Header().Set("Access-Control-Allow-Origin", "*")
  331. window := r.URL.Query().Get("window")
  332. offset := r.URL.Query().Get("offset")
  333. if window == "" {
  334. w.Write(WrapData(nil, fmt.Errorf("missing window arguement")))
  335. return
  336. }
  337. windowDur, err := timeutil.ParseDuration(window)
  338. if err != nil {
  339. w.Write(WrapData(nil, fmt.Errorf("error parsing window (%s): %s", window, err)))
  340. return
  341. }
  342. // offset is not a required parameter
  343. var offsetDur time.Duration
  344. if offset != "" {
  345. offsetDur, err = timeutil.ParseDuration(offset)
  346. if err != nil {
  347. w.Write(WrapData(nil, fmt.Errorf("error parsing offset (%s): %s", offset, err)))
  348. return
  349. }
  350. }
  351. useThanos, _ := strconv.ParseBool(r.URL.Query().Get("multi"))
  352. if useThanos && !thanos.IsEnabled() {
  353. w.Write(WrapData(nil, fmt.Errorf("Multi=true while Thanos is not enabled.")))
  354. return
  355. }
  356. var client prometheus.Client
  357. if useThanos {
  358. client = a.ThanosClient
  359. offsetDur = thanos.OffsetDuration()
  360. } else {
  361. client = a.PrometheusClient
  362. }
  363. data, err := a.ComputeClusterCosts(client, a.CloudProvider, windowDur, offsetDur, true)
  364. w.Write(WrapData(data, err))
  365. }
  366. func (a *Accesses) ClusterCostsOverTime(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  367. w.Header().Set("Content-Type", "application/json")
  368. w.Header().Set("Access-Control-Allow-Origin", "*")
  369. start := r.URL.Query().Get("start")
  370. end := r.URL.Query().Get("end")
  371. window := r.URL.Query().Get("window")
  372. offset := r.URL.Query().Get("offset")
  373. if window == "" {
  374. w.Write(WrapData(nil, fmt.Errorf("missing window arguement")))
  375. return
  376. }
  377. windowDur, err := timeutil.ParseDuration(window)
  378. if err != nil {
  379. w.Write(WrapData(nil, fmt.Errorf("error parsing window (%s): %s", window, err)))
  380. return
  381. }
  382. // offset is not a required parameter
  383. var offsetDur time.Duration
  384. if offset != "" {
  385. offsetDur, err = timeutil.ParseDuration(offset)
  386. if err != nil {
  387. w.Write(WrapData(nil, fmt.Errorf("error parsing offset (%s): %s", offset, err)))
  388. return
  389. }
  390. }
  391. data, err := ClusterCostsOverTime(a.PrometheusClient, a.CloudProvider, start, end, windowDur, offsetDur)
  392. w.Write(WrapData(data, err))
  393. }
  394. func (a *Accesses) CostDataModelRange(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  395. w.Header().Set("Content-Type", "application/json")
  396. w.Header().Set("Access-Control-Allow-Origin", "*")
  397. startStr := r.URL.Query().Get("start")
  398. endStr := r.URL.Query().Get("end")
  399. windowStr := r.URL.Query().Get("window")
  400. fields := r.URL.Query().Get("filterFields")
  401. namespace := r.URL.Query().Get("namespace")
  402. cluster := r.URL.Query().Get("cluster")
  403. remote := r.URL.Query().Get("remote")
  404. remoteEnabled := env.IsRemoteEnabled() && remote != "false"
  405. layout := "2006-01-02T15:04:05.000Z"
  406. start, err := time.Parse(layout, startStr)
  407. if err != nil {
  408. w.Write(WrapDataWithMessage(nil, fmt.Errorf("invalid start date: %s", startStr), fmt.Sprintf("invalid start date: %s", startStr)))
  409. return
  410. }
  411. end, err := time.Parse(layout, endStr)
  412. if err != nil {
  413. w.Write(WrapDataWithMessage(nil, fmt.Errorf("invalid end date: %s", endStr), fmt.Sprintf("invalid end date: %s", endStr)))
  414. return
  415. }
  416. window := kubecost.NewWindow(&start, &end)
  417. if window.IsOpen() || window.IsEmpty() || window.IsNegative() {
  418. w.Write(WrapDataWithMessage(nil, fmt.Errorf("invalid date range: %s", window), fmt.Sprintf("invalid date range: %s", window)))
  419. return
  420. }
  421. resolution := time.Hour
  422. if resDur, err := time.ParseDuration(windowStr); err == nil {
  423. resolution = resDur
  424. }
  425. // Use Thanos Client if it exists (enabled) and remote flag set
  426. var pClient prometheus.Client
  427. if remote != "false" && a.ThanosClient != nil {
  428. pClient = a.ThanosClient
  429. } else {
  430. pClient = a.PrometheusClient
  431. }
  432. data, err := a.Model.ComputeCostDataRange(pClient, a.CloudProvider, window, resolution, namespace, cluster, remoteEnabled)
  433. if err != nil {
  434. w.Write(WrapData(nil, err))
  435. }
  436. if fields != "" {
  437. filteredData := filterFields(fields, data)
  438. w.Write(WrapData(filteredData, err))
  439. } else {
  440. w.Write(WrapData(data, err))
  441. }
  442. }
  443. func parseAggregations(customAggregation, aggregator, filterType string) (string, []string, string) {
  444. var key string
  445. var filter string
  446. var val []string
  447. if customAggregation != "" {
  448. key = customAggregation
  449. filter = filterType
  450. val = strings.Split(customAggregation, ",")
  451. } else {
  452. aggregations := strings.Split(aggregator, ",")
  453. for i, agg := range aggregations {
  454. aggregations[i] = "kubernetes_" + agg
  455. }
  456. key = strings.Join(aggregations, ",")
  457. filter = "kubernetes_" + filterType
  458. val = aggregations
  459. }
  460. return key, val, filter
  461. }
  462. func (a *Accesses) OutofClusterCosts(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  463. w.Header().Set("Content-Type", "application/json")
  464. w.Header().Set("Access-Control-Allow-Origin", "*")
  465. start := r.URL.Query().Get("start")
  466. end := r.URL.Query().Get("end")
  467. aggregator := r.URL.Query().Get("aggregator")
  468. customAggregation := r.URL.Query().Get("customAggregation")
  469. filterType := r.URL.Query().Get("filterType")
  470. filterValue := r.URL.Query().Get("filterValue")
  471. var data []*cloud.OutOfClusterAllocation
  472. var err error
  473. _, aggregations, filter := parseAggregations(customAggregation, aggregator, filterType)
  474. data, err = a.CloudProvider.ExternalAllocations(start, end, aggregations, filter, filterValue, false)
  475. w.Write(WrapData(data, err))
  476. }
  477. func (a *Accesses) OutOfClusterCostsWithCache(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  478. w.Header().Set("Content-Type", "application/json")
  479. w.Header().Set("Access-Control-Allow-Origin", "*")
  480. // start date for which to query costs, inclusive; format YYYY-MM-DD
  481. start := r.URL.Query().Get("start")
  482. // end date for which to query costs, inclusive; format YYYY-MM-DD
  483. end := r.URL.Query().Get("end")
  484. // aggregator sets the field by which to aggregate; default, prepended by "kubernetes_"
  485. kubernetesAggregation := r.URL.Query().Get("aggregator")
  486. // customAggregation allows full customization of aggregator w/o prepending
  487. customAggregation := r.URL.Query().Get("customAggregation")
  488. // disableCache, if set to "true", tells this function to recompute and
  489. // cache the requested data
  490. disableCache := r.URL.Query().Get("disableCache") == "true"
  491. // clearCache, if set to "true", tells this function to flush the cache,
  492. // then recompute and cache the requested data
  493. clearCache := r.URL.Query().Get("clearCache") == "true"
  494. filterType := r.URL.Query().Get("filterType")
  495. filterValue := r.URL.Query().Get("filterValue")
  496. aggregationkey, aggregation, filter := parseAggregations(customAggregation, kubernetesAggregation, filterType)
  497. // clear cache prior to checking the cache so that a clearCache=true
  498. // request always returns a freshly computed value
  499. if clearCache {
  500. a.OutOfClusterCache.Flush()
  501. }
  502. // attempt to retrieve cost data from cache
  503. key := fmt.Sprintf(`%s:%s:%s:%s:%s`, start, end, aggregationkey, filter, filterValue)
  504. if value, found := a.OutOfClusterCache.Get(key); found && !disableCache {
  505. if data, ok := value.([]*cloud.OutOfClusterAllocation); ok {
  506. w.Write(WrapDataWithMessage(data, nil, fmt.Sprintf("out of cluster cache hit: %s", key)))
  507. return
  508. }
  509. klog.Errorf("caching error: failed to type cast data: %s", key)
  510. }
  511. data, err := a.CloudProvider.ExternalAllocations(start, end, aggregation, filter, filterValue, false)
  512. if err == nil {
  513. a.OutOfClusterCache.Set(key, data, cache.DefaultExpiration)
  514. }
  515. w.Write(WrapDataWithMessage(data, err, fmt.Sprintf("out of cluser cache miss: %s", key)))
  516. }
  517. func (a *Accesses) GetAllNodePricing(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  518. w.Header().Set("Content-Type", "application/json")
  519. w.Header().Set("Access-Control-Allow-Origin", "*")
  520. data, err := a.CloudProvider.AllNodePricing()
  521. w.Write(WrapData(data, err))
  522. }
  523. func (a *Accesses) GetConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  524. w.Header().Set("Content-Type", "application/json")
  525. w.Header().Set("Access-Control-Allow-Origin", "*")
  526. data, err := a.CloudProvider.GetConfig()
  527. w.Write(WrapData(data, err))
  528. }
  529. func (a *Accesses) UpdateSpotInfoConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  530. w.Header().Set("Content-Type", "application/json")
  531. w.Header().Set("Access-Control-Allow-Origin", "*")
  532. data, err := a.CloudProvider.UpdateConfig(r.Body, cloud.SpotInfoUpdateType)
  533. if err != nil {
  534. w.Write(WrapData(data, err))
  535. return
  536. }
  537. w.Write(WrapData(data, err))
  538. err = a.CloudProvider.DownloadPricingData()
  539. if err != nil {
  540. klog.V(1).Infof("Error redownloading data on config update: %s", err.Error())
  541. }
  542. return
  543. }
  544. func (a *Accesses) UpdateAthenaInfoConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  545. w.Header().Set("Content-Type", "application/json")
  546. w.Header().Set("Access-Control-Allow-Origin", "*")
  547. data, err := a.CloudProvider.UpdateConfig(r.Body, cloud.AthenaInfoUpdateType)
  548. if err != nil {
  549. w.Write(WrapData(data, err))
  550. return
  551. }
  552. w.Write(WrapData(data, err))
  553. return
  554. }
  555. func (a *Accesses) UpdateBigQueryInfoConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  556. w.Header().Set("Content-Type", "application/json")
  557. w.Header().Set("Access-Control-Allow-Origin", "*")
  558. data, err := a.CloudProvider.UpdateConfig(r.Body, cloud.BigqueryUpdateType)
  559. if err != nil {
  560. w.Write(WrapData(data, err))
  561. return
  562. }
  563. w.Write(WrapData(data, err))
  564. return
  565. }
  566. func (a *Accesses) UpdateConfigByKey(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  567. w.Header().Set("Content-Type", "application/json")
  568. w.Header().Set("Access-Control-Allow-Origin", "*")
  569. data, err := a.CloudProvider.UpdateConfig(r.Body, "")
  570. if err != nil {
  571. w.Write(WrapData(data, err))
  572. return
  573. }
  574. w.Write(WrapData(data, err))
  575. return
  576. }
  577. func (a *Accesses) ManagementPlatform(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  578. w.Header().Set("Content-Type", "application/json")
  579. w.Header().Set("Access-Control-Allow-Origin", "*")
  580. data, err := a.CloudProvider.GetManagementPlatform()
  581. if err != nil {
  582. w.Write(WrapData(data, err))
  583. return
  584. }
  585. w.Write(WrapData(data, err))
  586. return
  587. }
  588. func (a *Accesses) ClusterInfo(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  589. w.Header().Set("Content-Type", "application/json")
  590. w.Header().Set("Access-Control-Allow-Origin", "*")
  591. data := GetClusterInfo(a.KubeClientSet, a.CloudProvider)
  592. w.Write(WrapData(data, nil))
  593. }
  594. func (a *Accesses) GetClusterInfoMap(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  595. w.Header().Set("Content-Type", "application/json")
  596. w.Header().Set("Access-Control-Allow-Origin", "*")
  597. data := a.ClusterMap.AsMap()
  598. w.Write(WrapData(data, nil))
  599. }
  600. func (a *Accesses) GetServiceAccountStatus(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  601. w.Header().Set("Content-Type", "application/json")
  602. w.Header().Set("Access-Control-Allow-Origin", "*")
  603. w.Write(WrapData(a.CloudProvider.ServiceAccountStatus(), nil))
  604. }
  605. func (a *Accesses) GetPricingSourceStatus(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  606. w.Header().Set("Content-Type", "application/json")
  607. w.Header().Set("Access-Control-Allow-Origin", "*")
  608. w.Write(WrapData(a.CloudProvider.PricingSourceStatus(), nil))
  609. }
  610. func (a *Accesses) GetPricingSourceCounts(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  611. w.Header().Set("Content-Type", "application/json")
  612. w.Header().Set("Access-Control-Allow-Origin", "*")
  613. w.Write(WrapData(a.Model.GetPricingSourceCounts()))
  614. }
  615. func (a *Accesses) GetPrometheusMetadata(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  616. w.Header().Set("Content-Type", "application/json")
  617. w.Header().Set("Access-Control-Allow-Origin", "*")
  618. w.Write(WrapData(prom.Validate(a.PrometheusClient)))
  619. }
  620. func (a *Accesses) PrometheusQuery(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  621. w.Header().Set("Content-Type", "application/json")
  622. w.Header().Set("Access-Control-Allow-Origin", "*")
  623. qp := httputil.NewQueryParams(r.URL.Query())
  624. query := qp.Get("query", "")
  625. if query == "" {
  626. w.Write(WrapData(nil, fmt.Errorf("Query Parameter 'query' is unset'")))
  627. return
  628. }
  629. ctx := prom.NewNamedContext(a.PrometheusClient, prom.FrontendContextName)
  630. body, err := ctx.RawQuery(query)
  631. if err != nil {
  632. w.Write(WrapData(nil, fmt.Errorf("Error running query %s. Error: %s", query, err)))
  633. return
  634. }
  635. w.Write(body)
  636. }
  637. func (a *Accesses) PrometheusQueryRange(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  638. w.Header().Set("Content-Type", "application/json")
  639. w.Header().Set("Access-Control-Allow-Origin", "*")
  640. qp := httputil.NewQueryParams(r.URL.Query())
  641. query := qp.Get("query", "")
  642. if query == "" {
  643. fmt.Fprintf(w, "Error parsing query from request parameters.")
  644. return
  645. }
  646. start, end, duration, err := toStartEndStep(qp)
  647. if err != nil {
  648. fmt.Fprintf(w, err.Error())
  649. return
  650. }
  651. ctx := prom.NewNamedContext(a.PrometheusClient, prom.FrontendContextName)
  652. body, err := ctx.RawQueryRange(query, start, end, duration)
  653. if err != nil {
  654. fmt.Fprintf(w, "Error running query %s. Error: %s", query, err)
  655. return
  656. }
  657. w.Write(body)
  658. }
  659. func (a *Accesses) ThanosQuery(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  660. w.Header().Set("Content-Type", "application/json")
  661. w.Header().Set("Access-Control-Allow-Origin", "*")
  662. if !thanos.IsEnabled() {
  663. w.Write(WrapData(nil, fmt.Errorf("ThanosDisabled")))
  664. return
  665. }
  666. qp := httputil.NewQueryParams(r.URL.Query())
  667. query := qp.Get("query", "")
  668. if query == "" {
  669. w.Write(WrapData(nil, fmt.Errorf("Query Parameter 'query' is unset'")))
  670. return
  671. }
  672. ctx := prom.NewNamedContext(a.ThanosClient, prom.FrontendContextName)
  673. body, err := ctx.RawQuery(query)
  674. if err != nil {
  675. w.Write(WrapData(nil, fmt.Errorf("Error running query %s. Error: %s", query, err)))
  676. return
  677. }
  678. w.Write(body)
  679. }
  680. func (a *Accesses) ThanosQueryRange(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  681. w.Header().Set("Content-Type", "application/json")
  682. w.Header().Set("Access-Control-Allow-Origin", "*")
  683. if !thanos.IsEnabled() {
  684. w.Write(WrapData(nil, fmt.Errorf("ThanosDisabled")))
  685. return
  686. }
  687. qp := httputil.NewQueryParams(r.URL.Query())
  688. query := qp.Get("query", "")
  689. if query == "" {
  690. fmt.Fprintf(w, "Error parsing query from request parameters.")
  691. return
  692. }
  693. start, end, duration, err := toStartEndStep(qp)
  694. if err != nil {
  695. fmt.Fprintf(w, err.Error())
  696. return
  697. }
  698. ctx := prom.NewNamedContext(a.ThanosClient, prom.FrontendContextName)
  699. body, err := ctx.RawQueryRange(query, start, end, duration)
  700. if err != nil {
  701. fmt.Fprintf(w, "Error running query %s. Error: %s", query, err)
  702. return
  703. }
  704. w.Write(body)
  705. }
  706. // helper for query range proxy requests
  707. func toStartEndStep(qp httputil.QueryParams) (start, end time.Time, step time.Duration, err error) {
  708. var e error
  709. ss := qp.Get("start", "")
  710. es := qp.Get("end", "")
  711. ds := qp.Get("duration", "")
  712. layout := "2006-01-02T15:04:05.000Z"
  713. start, e = time.Parse(layout, ss)
  714. if e != nil {
  715. err = fmt.Errorf("Error parsing time %s. Error: %s", ss, err)
  716. return
  717. }
  718. end, e = time.Parse(layout, es)
  719. if e != nil {
  720. err = fmt.Errorf("Error parsing time %s. Error: %s", es, err)
  721. return
  722. }
  723. step, e = time.ParseDuration(ds)
  724. if e != nil {
  725. err = fmt.Errorf("Error parsing duration %s. Error: %s", ds, err)
  726. return
  727. }
  728. err = nil
  729. return
  730. }
  731. func (a *Accesses) GetPrometheusQueueState(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  732. w.Header().Set("Content-Type", "application/json")
  733. w.Header().Set("Access-Control-Allow-Origin", "*")
  734. promQueueState, err := prom.GetPrometheusQueueState(a.PrometheusClient)
  735. if err != nil {
  736. w.Write(WrapData(nil, err))
  737. return
  738. }
  739. result := map[string]*prom.PrometheusQueueState{
  740. "prometheus": promQueueState,
  741. }
  742. if thanos.IsEnabled() {
  743. thanosQueueState, err := prom.GetPrometheusQueueState(a.ThanosClient)
  744. if err != nil {
  745. log.Warningf("Error getting Thanos queue state: %s", err)
  746. } else {
  747. result["thanos"] = thanosQueueState
  748. }
  749. }
  750. w.Write(WrapData(result, nil))
  751. }
  752. // GetPrometheusMetrics retrieves availability of Prometheus and Thanos metrics
  753. func (a *Accesses) GetPrometheusMetrics(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  754. w.Header().Set("Content-Type", "application/json")
  755. w.Header().Set("Access-Control-Allow-Origin", "*")
  756. promMetrics, err := prom.GetPrometheusMetrics(a.PrometheusClient, "")
  757. if err != nil {
  758. w.Write(WrapData(nil, err))
  759. return
  760. }
  761. result := map[string][]*prom.PrometheusDiagnostic{
  762. "prometheus": promMetrics,
  763. }
  764. if thanos.IsEnabled() {
  765. thanosMetrics, err := prom.GetPrometheusMetrics(a.ThanosClient, thanos.QueryOffset())
  766. if err != nil {
  767. log.Warningf("Error getting Thanos queue state: %s", err)
  768. } else {
  769. result["thanos"] = thanosMetrics
  770. }
  771. }
  772. w.Write(WrapData(result, nil))
  773. }
  774. // captures the panic event in sentry
  775. func capturePanicEvent(err string, stack string) {
  776. msg := fmt.Sprintf("Panic: %s\nStackTrace: %s\n", err, stack)
  777. klog.V(1).Infoln(msg)
  778. sentry.CurrentHub().CaptureEvent(&sentry.Event{
  779. Level: sentry.LevelError,
  780. Message: msg,
  781. })
  782. sentry.Flush(5 * time.Second)
  783. }
  784. // handle any panics reported by the errors package
  785. func handlePanic(p errors.Panic) bool {
  786. err := p.Error
  787. if err != nil {
  788. if err, ok := err.(error); ok {
  789. capturePanicEvent(err.Error(), p.Stack)
  790. }
  791. if err, ok := err.(string); ok {
  792. capturePanicEvent(err, p.Stack)
  793. }
  794. }
  795. // Return true to recover iff the type is http, otherwise allow kubernetes
  796. // to recover.
  797. return p.Type == errors.PanicTypeHTTP
  798. }
  799. func Initialize(additionalConfigWatchers ...*watcher.ConfigMapWatcher) *Accesses {
  800. klog.InitFlags(nil)
  801. flag.Set("v", "3")
  802. flag.Parse()
  803. klog.V(1).Infof("Starting cost-model (git commit \"%s\")", env.GetAppVersion())
  804. configWatchers := watcher.NewConfigMapWatchers(additionalConfigWatchers...)
  805. var err error
  806. if errorReportingEnabled {
  807. err = sentry.Init(sentry.ClientOptions{Release: env.GetAppVersion()})
  808. if err != nil {
  809. klog.Infof("Failed to initialize sentry for error reporting")
  810. } else {
  811. err = errors.SetPanicHandler(handlePanic)
  812. if err != nil {
  813. klog.Infof("Failed to set panic handler: %s", err)
  814. }
  815. }
  816. }
  817. address := env.GetPrometheusServerEndpoint()
  818. if address == "" {
  819. klog.Fatalf("No address for prometheus set in $%s. Aborting.", env.PrometheusServerEndpointEnvVar)
  820. }
  821. queryConcurrency := env.GetMaxQueryConcurrency()
  822. klog.Infof("Prometheus/Thanos Client Max Concurrency set to %d", queryConcurrency)
  823. timeout := 120 * time.Second
  824. keepAlive := 120 * time.Second
  825. scrapeInterval := time.Minute
  826. promCli, err := prom.NewPrometheusClient(address, timeout, keepAlive, queryConcurrency, "")
  827. if err != nil {
  828. klog.Fatalf("Failed to create prometheus client, Error: %v", err)
  829. }
  830. m, err := prom.Validate(promCli)
  831. if err != nil || !m.Running {
  832. if err != nil {
  833. klog.Errorf("Failed to query prometheus at %s. Error: %s . Troubleshooting help available at: %s", address, err.Error(), prom.PrometheusTroubleshootingURL)
  834. } else if !m.Running {
  835. klog.Errorf("Prometheus at %s is not running. Troubleshooting help available at: %s", address, prom.PrometheusTroubleshootingURL)
  836. }
  837. } else {
  838. klog.V(1).Info("Success: retrieved the 'up' query against prometheus at: " + address)
  839. }
  840. api := prometheusAPI.NewAPI(promCli)
  841. _, err = api.Config(context.Background())
  842. if err != nil {
  843. klog.Infof("No valid prometheus config file at %s. Error: %s . Troubleshooting help available at: %s. Ignore if using cortex/thanos here.", address, err.Error(), prom.PrometheusTroubleshootingURL)
  844. } else {
  845. klog.Infof("Retrieved a prometheus config file from: %s", address)
  846. }
  847. // Lookup scrape interval for kubecost job, update if found
  848. si, err := prom.ScrapeIntervalFor(promCli, env.GetKubecostJobName())
  849. if err == nil {
  850. scrapeInterval = si
  851. }
  852. klog.Infof("Using scrape interval of %f", scrapeInterval.Seconds())
  853. // Kubernetes API setup
  854. var kc *rest.Config
  855. if kubeconfig := env.GetKubeConfigPath(); kubeconfig != "" {
  856. kc, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
  857. } else {
  858. kc, err = rest.InClusterConfig()
  859. }
  860. if err != nil {
  861. panic(err.Error())
  862. }
  863. kubeClientset, err := kubernetes.NewForConfig(kc)
  864. if err != nil {
  865. panic(err.Error())
  866. }
  867. // Create Kubernetes Cluster Cache + Watchers
  868. k8sCache := clustercache.NewKubernetesClusterCache(kubeClientset)
  869. k8sCache.Run()
  870. cloudProviderKey := env.GetCloudProviderAPIKey()
  871. cloudProvider, err := cloud.NewProvider(k8sCache, cloudProviderKey)
  872. if err != nil {
  873. panic(err.Error())
  874. }
  875. // Append the pricing config watcher
  876. configWatchers.AddWatcher(cloud.ConfigWatcherFor(cloudProvider))
  877. watchConfigFunc := configWatchers.ToWatchFunc()
  878. watchedConfigs := configWatchers.GetWatchedConfigs()
  879. kubecostNamespace := env.GetKubecostNamespace()
  880. // We need an initial invocation because the init of the cache has happened before we had access to the provider.
  881. for _, cw := range watchedConfigs {
  882. configs, err := kubeClientset.CoreV1().ConfigMaps(kubecostNamespace).Get(context.Background(), cw, metav1.GetOptions{})
  883. if err != nil {
  884. klog.Infof("No %s configmap found at install time, using existing configs: %s", cw, err.Error())
  885. } else {
  886. klog.Infof("Found configmap %s, watching...", configs.Name)
  887. watchConfigFunc(configs)
  888. }
  889. }
  890. k8sCache.SetConfigMapUpdateFunc(watchConfigFunc)
  891. remoteEnabled := env.IsRemoteEnabled()
  892. if remoteEnabled {
  893. info, err := cloudProvider.ClusterInfo()
  894. klog.Infof("Saving cluster with id:'%s', and name:'%s' to durable storage", info["id"], info["name"])
  895. if err != nil {
  896. klog.Infof("Error saving cluster id %s", err.Error())
  897. }
  898. _, _, err = cloud.GetOrCreateClusterMeta(info["id"], info["name"])
  899. if err != nil {
  900. klog.Infof("Unable to set cluster id '%s' for cluster '%s', %s", info["id"], info["name"], err.Error())
  901. }
  902. }
  903. // Thanos Client
  904. var thanosClient prometheus.Client
  905. if thanos.IsEnabled() {
  906. thanosAddress := thanos.QueryURL()
  907. if thanosAddress != "" {
  908. thanosCli, _ := thanos.NewThanosClient(thanosAddress, timeout, keepAlive, queryConcurrency, env.GetQueryLoggingFile())
  909. _, err = prom.Validate(thanosCli)
  910. if err != nil {
  911. klog.V(1).Infof("[Warning] Failed to query Thanos at %s. Error: %s.", thanosAddress, err.Error())
  912. thanosClient = thanosCli
  913. } else {
  914. klog.V(1).Info("Success: retrieved the 'up' query against Thanos at: " + thanosAddress)
  915. thanosClient = thanosCli
  916. }
  917. } else {
  918. klog.Infof("Error resolving environment variable: $%s", env.ThanosQueryUrlEnvVar)
  919. }
  920. }
  921. // Initialize ClusterMap for maintaining ClusterInfo by ClusterID
  922. var clusterMap clusters.ClusterMap
  923. localCIProvider := NewLocalClusterInfoProvider(kubeClientset, cloudProvider)
  924. if thanosClient != nil {
  925. clusterMap = clusters.NewClusterMap(thanosClient, localCIProvider, 10*time.Minute)
  926. } else {
  927. clusterMap = clusters.NewClusterMap(promCli, localCIProvider, 5*time.Minute)
  928. }
  929. // cache responses from model and aggregation for a default of 10 minutes;
  930. // clear expired responses every 20 minutes
  931. aggregateCache := cache.New(time.Minute*10, time.Minute*20)
  932. costDataCache := cache.New(time.Minute*10, time.Minute*20)
  933. clusterCostsCache := cache.New(cache.NoExpiration, cache.NoExpiration)
  934. outOfClusterCache := cache.New(time.Minute*5, time.Minute*10)
  935. settingsCache := cache.New(cache.NoExpiration, cache.NoExpiration)
  936. // query durations that should be cached longer should be registered here
  937. // use relatively prime numbers to minimize likelihood of synchronized
  938. // attempts at cache warming
  939. day := 24 * time.Hour
  940. cacheExpiration := map[time.Duration]time.Duration{
  941. day: maxCacheMinutes1d * time.Minute,
  942. 2 * day: maxCacheMinutes2d * time.Minute,
  943. 7 * day: maxCacheMinutes7d * time.Minute,
  944. 30 * day: maxCacheMinutes30d * time.Minute,
  945. }
  946. var pc prometheus.Client
  947. if thanosClient != nil {
  948. pc = thanosClient
  949. } else {
  950. pc = promCli
  951. }
  952. costModel := NewCostModel(pc, cloudProvider, k8sCache, clusterMap, scrapeInterval)
  953. metricsEmitter := NewCostModelMetricsEmitter(promCli, k8sCache, cloudProvider, costModel)
  954. a := &Accesses{
  955. Router: httprouter.New(),
  956. PrometheusClient: promCli,
  957. ThanosClient: thanosClient,
  958. KubeClientSet: kubeClientset,
  959. ClusterMap: clusterMap,
  960. CloudProvider: cloudProvider,
  961. Model: costModel,
  962. MetricsEmitter: metricsEmitter,
  963. AggregateCache: aggregateCache,
  964. CostDataCache: costDataCache,
  965. ClusterCostsCache: clusterCostsCache,
  966. OutOfClusterCache: outOfClusterCache,
  967. SettingsCache: settingsCache,
  968. CacheExpiration: cacheExpiration,
  969. httpServices: services.NewCostModelServices(),
  970. }
  971. // Use the Accesses instance, itself, as the CostModelAggregator. This is
  972. // confusing and unconventional, but necessary so that we can swap it
  973. // out for the ETL-adapted version elsewhere.
  974. // TODO clean this up once ETL is open-sourced.
  975. a.AggAPI = a
  976. // Initialize mechanism for subscribing to settings changes
  977. a.InitializeSettingsPubSub()
  978. err = a.CloudProvider.DownloadPricingData()
  979. if err != nil {
  980. klog.V(1).Info("Failed to download pricing data: " + err.Error())
  981. }
  982. // Warm the aggregate cache unless explicitly set to false
  983. if env.IsCacheWarmingEnabled() {
  984. log.Infof("Init: AggregateCostModel cache warming enabled")
  985. a.warmAggregateCostModelCache()
  986. } else {
  987. log.Infof("Init: AggregateCostModel cache warming disabled")
  988. }
  989. if !env.IsKubecostMetricsPodEnabled() {
  990. a.MetricsEmitter.Start()
  991. }
  992. a.Router.GET("/costDataModel", a.CostDataModel)
  993. a.Router.GET("/costDataModelRange", a.CostDataModelRange)
  994. a.Router.GET("/aggregatedCostModel", a.AggregateCostModelHandler)
  995. a.Router.GET("/allocation/compute", a.ComputeAllocationHandler)
  996. a.Router.GET("/outOfClusterCosts", a.OutOfClusterCostsWithCache)
  997. a.Router.GET("/allNodePricing", a.GetAllNodePricing)
  998. a.Router.POST("/refreshPricing", a.RefreshPricingData)
  999. a.Router.GET("/clusterCostsOverTime", a.ClusterCostsOverTime)
  1000. a.Router.GET("/clusterCosts", a.ClusterCosts)
  1001. a.Router.GET("/clusterCostsFromCache", a.ClusterCostsFromCacheHandler)
  1002. a.Router.GET("/validatePrometheus", a.GetPrometheusMetadata)
  1003. a.Router.GET("/managementPlatform", a.ManagementPlatform)
  1004. a.Router.GET("/clusterInfo", a.ClusterInfo)
  1005. a.Router.GET("/clusterInfoMap", a.GetClusterInfoMap)
  1006. a.Router.GET("/serviceAccountStatus", a.GetServiceAccountStatus)
  1007. a.Router.GET("/pricingSourceStatus", a.GetPricingSourceStatus)
  1008. a.Router.GET("/pricingSourceCounts", a.GetPricingSourceCounts)
  1009. // prom query proxies
  1010. a.Router.GET("/prometheusQuery", a.PrometheusQuery)
  1011. a.Router.GET("/prometheusQueryRange", a.PrometheusQueryRange)
  1012. a.Router.GET("/thanosQuery", a.ThanosQuery)
  1013. a.Router.GET("/thanosQueryRange", a.ThanosQueryRange)
  1014. // diagnostics
  1015. a.Router.GET("/diagnostics/requestQueue", a.GetPrometheusQueueState)
  1016. a.Router.GET("/diagnostics/prometheusMetrics", a.GetPrometheusMetrics)
  1017. a.httpServices.RegisterAll(a.Router)
  1018. return a
  1019. }