router.go 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155
  1. package costmodel
  2. import (
  3. "context"
  4. "encoding/json"
  5. "flag"
  6. "fmt"
  7. "math"
  8. "net"
  9. "net/http"
  10. "os"
  11. "reflect"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "k8s.io/klog"
  16. "github.com/julienschmidt/httprouter"
  17. costAnalyzerCloud "github.com/kubecost/cost-model/pkg/cloud"
  18. "github.com/kubecost/cost-model/pkg/clustercache"
  19. cm "github.com/kubecost/cost-model/pkg/clustermanager"
  20. prometheusClient "github.com/prometheus/client_golang/api"
  21. prometheusAPI "github.com/prometheus/client_golang/api/prometheus/v1"
  22. v1 "k8s.io/api/core/v1"
  23. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  24. "github.com/patrickmn/go-cache"
  25. "github.com/prometheus/client_golang/prometheus"
  26. "k8s.io/client-go/kubernetes"
  27. "k8s.io/client-go/rest"
  28. )
  29. const (
  30. prometheusServerEndpointEnvVar = "PROMETHEUS_SERVER_ENDPOINT"
  31. prometheusTroubleshootingEp = "http://docs.kubecost.com/custom-prom#troubleshoot"
  32. RFC3339Milli = "2006-01-02T15:04:05.000Z"
  33. )
  34. var (
  35. // gitCommit is set by the build system
  36. gitCommit string
  37. )
  38. var Router = httprouter.New()
  39. var A Accesses
  40. type Accesses struct {
  41. PrometheusClient prometheusClient.Client
  42. ThanosClient prometheusClient.Client
  43. KubeClientSet kubernetes.Interface
  44. ClusterManager *cm.ClusterManager
  45. Cloud costAnalyzerCloud.Provider
  46. CPUPriceRecorder *prometheus.GaugeVec
  47. RAMPriceRecorder *prometheus.GaugeVec
  48. PersistentVolumePriceRecorder *prometheus.GaugeVec
  49. GPUPriceRecorder *prometheus.GaugeVec
  50. NodeTotalPriceRecorder *prometheus.GaugeVec
  51. RAMAllocationRecorder *prometheus.GaugeVec
  52. CPUAllocationRecorder *prometheus.GaugeVec
  53. GPUAllocationRecorder *prometheus.GaugeVec
  54. PVAllocationRecorder *prometheus.GaugeVec
  55. ContainerUptimeRecorder *prometheus.GaugeVec
  56. NetworkZoneEgressRecorder prometheus.Gauge
  57. NetworkRegionEgressRecorder prometheus.Gauge
  58. NetworkInternetEgressRecorder prometheus.Gauge
  59. ServiceSelectorRecorder *prometheus.GaugeVec
  60. DeploymentSelectorRecorder *prometheus.GaugeVec
  61. Model *CostModel
  62. OutOfClusterCache *cache.Cache
  63. }
  64. type DataEnvelope struct {
  65. Code int `json:"code"`
  66. Status string `json:"status"`
  67. Data interface{} `json:"data"`
  68. Message string `json:"message,omitempty"`
  69. }
  70. // 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
  71. type FilterFunc func(*CostData) (bool, string)
  72. // FilterCostData allows through only CostData that matches all the given filter functions
  73. func FilterCostData(data map[string]*CostData, retains []FilterFunc, filters []FilterFunc) (map[string]*CostData, int, map[string]int) {
  74. result := make(map[string]*CostData)
  75. filteredEnvironments := make(map[string]int)
  76. filteredContainers := 0
  77. DataLoop:
  78. for key, datum := range data {
  79. for _, rf := range retains {
  80. if ok, _ := rf(datum); ok {
  81. result[key] = datum
  82. // if any retain function passes, the data is retained and move on
  83. continue DataLoop
  84. }
  85. }
  86. for _, ff := range filters {
  87. if ok, environment := ff(datum); !ok {
  88. if environment != "" {
  89. filteredEnvironments[environment]++
  90. }
  91. filteredContainers++
  92. // if any filter function check fails, move on to the next datum
  93. continue DataLoop
  94. }
  95. }
  96. result[key] = datum
  97. }
  98. return result, filteredContainers, filteredEnvironments
  99. }
  100. func filterFields(fields string, data map[string]*CostData) map[string]CostData {
  101. fs := strings.Split(fields, ",")
  102. fmap := make(map[string]bool)
  103. for _, f := range fs {
  104. fieldNameLower := strings.ToLower(f) // convert to go struct name by uppercasing first letter
  105. klog.V(1).Infof("to delete: %s", fieldNameLower)
  106. fmap[fieldNameLower] = true
  107. }
  108. filteredData := make(map[string]CostData)
  109. for cname, costdata := range data {
  110. s := reflect.TypeOf(*costdata)
  111. val := reflect.ValueOf(*costdata)
  112. costdata2 := CostData{}
  113. cd2 := reflect.New(reflect.Indirect(reflect.ValueOf(costdata2)).Type()).Elem()
  114. n := s.NumField()
  115. for i := 0; i < n; i++ {
  116. field := s.Field(i)
  117. value := val.Field(i)
  118. value2 := cd2.Field(i)
  119. if _, ok := fmap[strings.ToLower(field.Name)]; !ok {
  120. value2.Set(reflect.Value(value))
  121. }
  122. }
  123. filteredData[cname] = cd2.Interface().(CostData)
  124. }
  125. return filteredData
  126. }
  127. func normalizeTimeParam(param string) (string, error) {
  128. // convert days to hours
  129. if param[len(param)-1:] == "d" {
  130. count := param[:len(param)-1]
  131. val, err := strconv.ParseInt(count, 10, 64)
  132. if err != nil {
  133. return "", err
  134. }
  135. val = val * 24
  136. param = fmt.Sprintf("%dh", val)
  137. }
  138. return param, nil
  139. }
  140. // parsePercentString takes a string of expected format "N%" and returns a floating point 0.0N.
  141. // If the "%" symbol is missing, it just returns 0.0N. Empty string is interpreted as "0%" and
  142. // return 0.0.
  143. func ParsePercentString(percentStr string) (float64, error) {
  144. if len(percentStr) == 0 {
  145. return 0.0, nil
  146. }
  147. if percentStr[len(percentStr)-1:] == "%" {
  148. percentStr = percentStr[:len(percentStr)-1]
  149. }
  150. discount, err := strconv.ParseFloat(percentStr, 64)
  151. if err != nil {
  152. return 0.0, err
  153. }
  154. discount *= 0.01
  155. return discount, nil
  156. }
  157. // parseDuration converts a Prometheus-style duration string into a Duration
  158. func ParseDuration(duration string) (*time.Duration, error) {
  159. unitStr := duration[len(duration)-1:]
  160. var unit time.Duration
  161. switch unitStr {
  162. case "s":
  163. unit = time.Second
  164. case "m":
  165. unit = time.Minute
  166. case "h":
  167. unit = time.Hour
  168. case "d":
  169. unit = 24.0 * time.Hour
  170. default:
  171. return nil, fmt.Errorf("error parsing duration: %s did not match expected format [0-9+](s|m|d|h)", duration)
  172. }
  173. amountStr := duration[:len(duration)-1]
  174. amount, err := strconv.ParseInt(amountStr, 10, 64)
  175. if err != nil {
  176. return nil, fmt.Errorf("error parsing duration: %s did not match expected format [0-9+](s|m|d|h)", duration)
  177. }
  178. dur := time.Duration(amount) * unit
  179. return &dur, nil
  180. }
  181. // ParseTimeRange returns a start and end time, respectively, which are converted from
  182. // a duration and offset, defined as strings with Prometheus-style syntax.
  183. func ParseTimeRange(duration, offset string) (*time.Time, *time.Time, error) {
  184. // endTime defaults to the current time, unless an offset is explicity declared,
  185. // in which case it shifts endTime back by given duration
  186. endTime := time.Now()
  187. if offset != "" {
  188. o, err := ParseDuration(offset)
  189. if err != nil {
  190. return nil, nil, fmt.Errorf("error parsing offset (%s): %s", offset, err)
  191. }
  192. endTime = endTime.Add(-1 * *o)
  193. }
  194. // if duration is defined in terms of days, convert to hours
  195. // e.g. convert "2d" to "48h"
  196. durationNorm, err := normalizeTimeParam(duration)
  197. if err != nil {
  198. return nil, nil, fmt.Errorf("error parsing duration (%s): %s", duration, err)
  199. }
  200. // convert time duration into start and end times, formatted
  201. // as ISO datetime strings
  202. dur, err := time.ParseDuration(durationNorm)
  203. if err != nil {
  204. return nil, nil, fmt.Errorf("errorf parsing duration (%s): %s", durationNorm, err)
  205. }
  206. startTime := endTime.Add(-1 * dur)
  207. return &startTime, &endTime, nil
  208. }
  209. func WrapDataWithMessage(data interface{}, err error, message string) []byte {
  210. var resp []byte
  211. if err != nil {
  212. klog.V(1).Infof("Error returned to client: %s", err.Error())
  213. resp, _ = json.Marshal(&DataEnvelope{
  214. Code: http.StatusInternalServerError,
  215. Status: "error",
  216. Message: err.Error(),
  217. Data: data,
  218. })
  219. } else {
  220. resp, _ = json.Marshal(&DataEnvelope{
  221. Code: http.StatusOK,
  222. Status: "success",
  223. Data: data,
  224. Message: message,
  225. })
  226. }
  227. return resp
  228. }
  229. func WrapData(data interface{}, err error) []byte {
  230. var resp []byte
  231. if err != nil {
  232. klog.V(1).Infof("Error returned to client: %s", err.Error())
  233. resp, _ = json.Marshal(&DataEnvelope{
  234. Code: http.StatusInternalServerError,
  235. Status: "error",
  236. Message: err.Error(),
  237. Data: data,
  238. })
  239. } else {
  240. resp, _ = json.Marshal(&DataEnvelope{
  241. Code: http.StatusOK,
  242. Status: "success",
  243. Data: data,
  244. })
  245. }
  246. return resp
  247. }
  248. // 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.
  249. func (a *Accesses) RefreshPricingData(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  250. w.Header().Set("Content-Type", "application/json")
  251. w.Header().Set("Access-Control-Allow-Origin", "*")
  252. err := a.Cloud.DownloadPricingData()
  253. w.Write(WrapData(nil, err))
  254. }
  255. func (a *Accesses) CostDataModel(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  256. w.Header().Set("Content-Type", "application/json")
  257. w.Header().Set("Access-Control-Allow-Origin", "*")
  258. window := r.URL.Query().Get("timeWindow")
  259. offset := r.URL.Query().Get("offset")
  260. fields := r.URL.Query().Get("filterFields")
  261. namespace := r.URL.Query().Get("namespace")
  262. if offset != "" {
  263. offset = "offset " + offset
  264. }
  265. data, err := a.Model.ComputeCostData(a.PrometheusClient, a.KubeClientSet, a.Cloud, window, offset, namespace)
  266. if fields != "" {
  267. filteredData := filterFields(fields, data)
  268. w.Write(WrapData(filteredData, err))
  269. } else {
  270. w.Write(WrapData(data, err))
  271. }
  272. }
  273. func (a *Accesses) ClusterCosts(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  274. w.Header().Set("Content-Type", "application/json")
  275. w.Header().Set("Access-Control-Allow-Origin", "*")
  276. window := r.URL.Query().Get("window")
  277. offset := r.URL.Query().Get("offset")
  278. data, err := ComputeClusterCosts(a.PrometheusClient, a.Cloud, window, offset)
  279. w.Write(WrapData(data, err))
  280. }
  281. func (a *Accesses) ClusterCostsOverTime(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  282. w.Header().Set("Content-Type", "application/json")
  283. w.Header().Set("Access-Control-Allow-Origin", "*")
  284. start := r.URL.Query().Get("start")
  285. end := r.URL.Query().Get("end")
  286. window := r.URL.Query().Get("window")
  287. offset := r.URL.Query().Get("offset")
  288. data, err := ClusterCostsOverTime(a.PrometheusClient, a.Cloud, start, end, window, offset)
  289. w.Write(WrapData(data, err))
  290. }
  291. func (a *Accesses) CostDataModelRange(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  292. w.Header().Set("Content-Type", "application/json")
  293. w.Header().Set("Access-Control-Allow-Origin", "*")
  294. start := r.URL.Query().Get("start")
  295. end := r.URL.Query().Get("end")
  296. window := r.URL.Query().Get("window")
  297. fields := r.URL.Query().Get("filterFields")
  298. namespace := r.URL.Query().Get("namespace")
  299. cluster := r.URL.Query().Get("cluster")
  300. remote := r.URL.Query().Get("remote")
  301. remoteAvailable := os.Getenv(remoteEnabled)
  302. remoteEnabled := false
  303. if remoteAvailable == "true" && remote != "false" {
  304. remoteEnabled = true
  305. }
  306. // Use Thanos Client if it exists (enabled) and remote flag set
  307. var pClient prometheusClient.Client
  308. if remote != "false" && a.ThanosClient != nil {
  309. pClient = a.ThanosClient
  310. } else {
  311. pClient = a.PrometheusClient
  312. }
  313. resolutionHours := 1.0
  314. data, err := a.Model.ComputeCostDataRange(pClient, a.KubeClientSet, a.Cloud, start, end, window, resolutionHours, namespace, cluster, remoteEnabled)
  315. if err != nil {
  316. w.Write(WrapData(nil, err))
  317. }
  318. if fields != "" {
  319. filteredData := filterFields(fields, data)
  320. w.Write(WrapData(filteredData, err))
  321. } else {
  322. w.Write(WrapData(data, err))
  323. }
  324. }
  325. // CostDataModelRangeLarge is experimental multi-cluster and long-term data storage in SQL support.
  326. func (a *Accesses) CostDataModelRangeLarge(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  327. w.Header().Set("Content-Type", "application/json")
  328. w.Header().Set("Access-Control-Allow-Origin", "*")
  329. startString := r.URL.Query().Get("start")
  330. endString := r.URL.Query().Get("end")
  331. windowString := r.URL.Query().Get("window")
  332. var start time.Time
  333. var end time.Time
  334. var err error
  335. if windowString == "" {
  336. windowString = "1h"
  337. }
  338. if startString != "" {
  339. start, err = time.Parse(RFC3339Milli, startString)
  340. if err != nil {
  341. klog.V(1).Infof("Error parsing time " + startString + ". Error: " + err.Error())
  342. w.Write(WrapData(nil, err))
  343. }
  344. } else {
  345. window, err := time.ParseDuration(windowString)
  346. if err != nil {
  347. w.Write(WrapData(nil, fmt.Errorf("Invalid duration '%s'", windowString)))
  348. }
  349. start = time.Now().Add(-2 * window)
  350. }
  351. if endString != "" {
  352. end, err = time.Parse(RFC3339Milli, endString)
  353. if err != nil {
  354. klog.V(1).Infof("Error parsing time " + endString + ". Error: " + err.Error())
  355. w.Write(WrapData(nil, err))
  356. }
  357. } else {
  358. end = time.Now()
  359. }
  360. remoteLayout := "2006-01-02T15:04:05Z"
  361. remoteStartStr := start.Format(remoteLayout)
  362. remoteEndStr := end.Format(remoteLayout)
  363. klog.V(1).Infof("Using remote database for query from %s to %s with window %s", startString, endString, windowString)
  364. data, err := CostDataRangeFromSQL("", "", windowString, remoteStartStr, remoteEndStr)
  365. w.Write(WrapData(data, err))
  366. }
  367. func parseAggregations(customAggregation, aggregator, filterType string) (string, []string, string) {
  368. var key string
  369. var filter string
  370. var val []string
  371. if customAggregation != "" {
  372. key = customAggregation
  373. filter = filterType
  374. val = strings.Split(customAggregation, ",")
  375. } else {
  376. aggregations := strings.Split(aggregator, ",")
  377. for i, agg := range aggregations {
  378. aggregations[i] = "kubernetes_" + agg
  379. }
  380. key = strings.Join(aggregations, ",")
  381. filter = "kubernetes_" + filterType
  382. val = aggregations
  383. }
  384. return key, val, filter
  385. }
  386. func (a *Accesses) OutofClusterCosts(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  387. w.Header().Set("Content-Type", "application/json")
  388. w.Header().Set("Access-Control-Allow-Origin", "*")
  389. start := r.URL.Query().Get("start")
  390. end := r.URL.Query().Get("end")
  391. aggregator := r.URL.Query().Get("aggregator")
  392. customAggregation := r.URL.Query().Get("customAggregation")
  393. filterType := r.URL.Query().Get("filterType")
  394. filterValue := r.URL.Query().Get("filterValue")
  395. var data []*costAnalyzerCloud.OutOfClusterAllocation
  396. var err error
  397. _, aggregations, filter := parseAggregations(customAggregation, aggregator, filterType)
  398. data, err = a.Cloud.ExternalAllocations(start, end, aggregations, filter, filterValue, false)
  399. w.Write(WrapData(data, err))
  400. }
  401. func (a *Accesses) OutOfClusterCostsWithCache(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  402. w.Header().Set("Content-Type", "application/json")
  403. w.Header().Set("Access-Control-Allow-Origin", "*")
  404. // start date for which to query costs, inclusive; format YYYY-MM-DD
  405. start := r.URL.Query().Get("start")
  406. // end date for which to query costs, inclusive; format YYYY-MM-DD
  407. end := r.URL.Query().Get("end")
  408. // aggregator sets the field by which to aggregate; default, prepended by "kubernetes_"
  409. kubernetesAggregation := r.URL.Query().Get("aggregator")
  410. // customAggregation allows full customization of aggregator w/o prepending
  411. customAggregation := r.URL.Query().Get("customAggregation")
  412. // disableCache, if set to "true", tells this function to recompute and
  413. // cache the requested data
  414. disableCache := r.URL.Query().Get("disableCache") == "true"
  415. // clearCache, if set to "true", tells this function to flush the cache,
  416. // then recompute and cache the requested data
  417. clearCache := r.URL.Query().Get("clearCache") == "true"
  418. filterType := r.URL.Query().Get("filterType")
  419. filterValue := r.URL.Query().Get("filterValue")
  420. aggregationkey, aggregation, filter := parseAggregations(customAggregation, kubernetesAggregation, filterType)
  421. // clear cache prior to checking the cache so that a clearCache=true
  422. // request always returns a freshly computed value
  423. if clearCache {
  424. a.OutOfClusterCache.Flush()
  425. }
  426. // attempt to retrieve cost data from cache
  427. key := fmt.Sprintf(`%s:%s:%s:%s:%s`, start, end, aggregationkey, filter, filterValue)
  428. if value, found := a.OutOfClusterCache.Get(key); found && !disableCache {
  429. if data, ok := value.([]*costAnalyzerCloud.OutOfClusterAllocation); ok {
  430. w.Write(WrapDataWithMessage(data, nil, fmt.Sprintf("out of cluster cache hit: %s", key)))
  431. return
  432. }
  433. klog.Errorf("caching error: failed to type cast data: %s", key)
  434. }
  435. data, err := a.Cloud.ExternalAllocations(start, end, aggregation, filter, filterValue, false)
  436. if err == nil {
  437. a.OutOfClusterCache.Set(key, data, cache.DefaultExpiration)
  438. }
  439. w.Write(WrapDataWithMessage(data, err, fmt.Sprintf("out of cluser cache miss: %s", key)))
  440. }
  441. func (p *Accesses) GetAllNodePricing(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  442. w.Header().Set("Content-Type", "application/json")
  443. w.Header().Set("Access-Control-Allow-Origin", "*")
  444. data, err := p.Cloud.AllNodePricing()
  445. w.Write(WrapData(data, err))
  446. }
  447. func (p *Accesses) GetConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  448. w.Header().Set("Content-Type", "application/json")
  449. w.Header().Set("Access-Control-Allow-Origin", "*")
  450. data, err := p.Cloud.GetConfig()
  451. w.Write(WrapData(data, err))
  452. }
  453. func (p *Accesses) UpdateSpotInfoConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  454. w.Header().Set("Content-Type", "application/json")
  455. w.Header().Set("Access-Control-Allow-Origin", "*")
  456. data, err := p.Cloud.UpdateConfig(r.Body, costAnalyzerCloud.SpotInfoUpdateType)
  457. if err != nil {
  458. w.Write(WrapData(data, err))
  459. return
  460. }
  461. w.Write(WrapData(data, err))
  462. err = p.Cloud.DownloadPricingData()
  463. if err != nil {
  464. klog.V(1).Infof("Error redownloading data on config update: %s", err.Error())
  465. }
  466. return
  467. }
  468. func (p *Accesses) UpdateAthenaInfoConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  469. w.Header().Set("Content-Type", "application/json")
  470. w.Header().Set("Access-Control-Allow-Origin", "*")
  471. data, err := p.Cloud.UpdateConfig(r.Body, costAnalyzerCloud.AthenaInfoUpdateType)
  472. if err != nil {
  473. w.Write(WrapData(data, err))
  474. return
  475. }
  476. w.Write(WrapData(data, err))
  477. return
  478. }
  479. func (p *Accesses) UpdateBigQueryInfoConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  480. w.Header().Set("Content-Type", "application/json")
  481. w.Header().Set("Access-Control-Allow-Origin", "*")
  482. data, err := p.Cloud.UpdateConfig(r.Body, costAnalyzerCloud.BigqueryUpdateType)
  483. if err != nil {
  484. w.Write(WrapData(data, err))
  485. return
  486. }
  487. w.Write(WrapData(data, err))
  488. return
  489. }
  490. func (p *Accesses) UpdateConfigByKey(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  491. w.Header().Set("Content-Type", "application/json")
  492. w.Header().Set("Access-Control-Allow-Origin", "*")
  493. data, err := p.Cloud.UpdateConfig(r.Body, "")
  494. if err != nil {
  495. w.Write(WrapData(data, err))
  496. return
  497. }
  498. w.Write(WrapData(data, err))
  499. return
  500. }
  501. func (p *Accesses) ManagementPlatform(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  502. w.Header().Set("Content-Type", "application/json")
  503. w.Header().Set("Access-Control-Allow-Origin", "*")
  504. data, err := p.Cloud.GetManagementPlatform()
  505. if err != nil {
  506. w.Write(WrapData(data, err))
  507. return
  508. }
  509. w.Write(WrapData(data, err))
  510. return
  511. }
  512. func (p *Accesses) ClusterInfo(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  513. w.Header().Set("Content-Type", "application/json")
  514. w.Header().Set("Access-Control-Allow-Origin", "*")
  515. data, err := p.Cloud.ClusterInfo()
  516. kc, ok := p.KubeClientSet.(*kubernetes.Clientset)
  517. if ok && data != nil {
  518. v, err := kc.ServerVersion()
  519. if err != nil {
  520. klog.Infof("Could not get k8s version info: %s", err.Error())
  521. } else if v != nil {
  522. data["version"] = v.Major + "." + v.Minor
  523. }
  524. } else {
  525. klog.Infof("Could not get k8s version info: %s", err.Error())
  526. }
  527. w.Write(WrapData(data, err))
  528. }
  529. func (p *Accesses) GetPrometheusMetadata(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  530. w.Header().Set("Content-Type", "application/json")
  531. w.Header().Set("Access-Control-Allow-Origin", "*")
  532. w.Write(WrapData(ValidatePrometheus(p.PrometheusClient, false)))
  533. }
  534. func (p *Accesses) ContainerUptimes(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  535. w.Header().Set("Content-Type", "application/json")
  536. w.Header().Set("Access-Control-Allow-Origin", "*")
  537. res, err := ComputeUptimes(p.PrometheusClient)
  538. w.Write(WrapData(res, err))
  539. }
  540. func (a *Accesses) recordPrices() {
  541. go func() {
  542. containerSeen := make(map[string]bool)
  543. nodeSeen := make(map[string]bool)
  544. pvSeen := make(map[string]bool)
  545. pvcSeen := make(map[string]bool)
  546. getKeyFromLabelStrings := func(labels ...string) string {
  547. return strings.Join(labels, ",")
  548. }
  549. getLabelStringsFromKey := func(key string) []string {
  550. return strings.Split(key, ",")
  551. }
  552. var defaultRegion string = ""
  553. nodeList := a.Model.Cache.GetAllNodes()
  554. if len(nodeList) > 0 {
  555. defaultRegion = nodeList[0].Labels[v1.LabelZoneRegion]
  556. }
  557. for {
  558. klog.V(4).Info("Recording prices...")
  559. podlist := a.Model.Cache.GetAllPods()
  560. podStatus := make(map[string]v1.PodPhase)
  561. for _, pod := range podlist {
  562. podStatus[pod.Name] = pod.Status.Phase
  563. }
  564. cfg, _ := a.Cloud.GetConfig()
  565. // Record network pricing at global scope
  566. networkCosts, err := a.Cloud.NetworkPricing()
  567. if err != nil {
  568. klog.V(4).Infof("Failed to retrieve network costs: %s", err.Error())
  569. } else {
  570. a.NetworkZoneEgressRecorder.Set(networkCosts.ZoneNetworkEgressCost)
  571. a.NetworkRegionEgressRecorder.Set(networkCosts.RegionNetworkEgressCost)
  572. a.NetworkInternetEgressRecorder.Set(networkCosts.InternetNetworkEgressCost)
  573. }
  574. data, err := a.Model.ComputeCostData(a.PrometheusClient, a.KubeClientSet, a.Cloud, "2m", "", "")
  575. if err != nil {
  576. klog.V(1).Info("Error in price recording: " + err.Error())
  577. // zero the for loop so the time.Sleep will still work
  578. data = map[string]*CostData{}
  579. }
  580. nodes, err := a.Model.GetNodeCost(a.Cloud)
  581. for nodeName, node := range nodes {
  582. // Emit costs, guarding against NaN inputs for custom pricing.
  583. cpuCost, _ := strconv.ParseFloat(node.VCPUCost, 64)
  584. if math.IsNaN(cpuCost) || math.IsInf(cpuCost, 0) {
  585. cpuCost, _ = strconv.ParseFloat(cfg.CPU, 64)
  586. if math.IsNaN(cpuCost) || math.IsInf(cpuCost, 0) {
  587. cpuCost = 0
  588. }
  589. }
  590. cpu, _ := strconv.ParseFloat(node.VCPU, 64)
  591. if math.IsNaN(cpu) || math.IsInf(cpu, 0) {
  592. cpu = 1 // Assume 1 CPU
  593. }
  594. ramCost, _ := strconv.ParseFloat(node.RAMCost, 64)
  595. if math.IsNaN(ramCost) || math.IsInf(ramCost, 0) {
  596. ramCost, _ = strconv.ParseFloat(cfg.RAM, 64)
  597. if math.IsNaN(ramCost) || math.IsInf(ramCost, 0) {
  598. ramCost = 0
  599. }
  600. }
  601. ram, _ := strconv.ParseFloat(node.RAMBytes, 64)
  602. if math.IsNaN(ram) || math.IsInf(ram, 0) {
  603. ram = 0
  604. }
  605. gpu, _ := strconv.ParseFloat(node.GPU, 64)
  606. if math.IsNaN(gpu) || math.IsInf(gpu, 0) {
  607. gpu = 0
  608. }
  609. gpuCost, _ := strconv.ParseFloat(node.GPUCost, 64)
  610. if math.IsNaN(gpuCost) || math.IsInf(gpuCost, 0) {
  611. gpuCost, _ = strconv.ParseFloat(cfg.GPU, 64)
  612. if math.IsNaN(gpuCost) || math.IsInf(gpuCost, 0) {
  613. gpuCost = 0
  614. }
  615. }
  616. nodeType := node.InstanceType
  617. nodeRegion := node.Region
  618. totalCost := cpu*cpuCost + ramCost*(ram/1024/1024/1024) + gpu*gpuCost
  619. a.CPUPriceRecorder.WithLabelValues(nodeName, nodeName, nodeType, nodeRegion).Set(cpuCost)
  620. a.RAMPriceRecorder.WithLabelValues(nodeName, nodeName, nodeType, nodeRegion).Set(ramCost)
  621. a.GPUPriceRecorder.WithLabelValues(nodeName, nodeName, nodeType, nodeRegion).Set(gpuCost)
  622. a.NodeTotalPriceRecorder.WithLabelValues(nodeName, nodeName, nodeType, nodeRegion).Set(totalCost)
  623. labelKey := getKeyFromLabelStrings(nodeName, nodeName)
  624. nodeSeen[labelKey] = true
  625. }
  626. for _, costs := range data {
  627. nodeName := costs.NodeName
  628. namespace := costs.Namespace
  629. podName := costs.PodName
  630. containerName := costs.Name
  631. if costs.PVCData != nil {
  632. for _, pvc := range costs.PVCData {
  633. if pvc.Volume != nil {
  634. a.PVAllocationRecorder.WithLabelValues(namespace, podName, pvc.Claim, pvc.VolumeName).Set(pvc.Values[0].Value)
  635. labelKey := getKeyFromLabelStrings(namespace, podName, pvc.Claim, pvc.VolumeName)
  636. pvcSeen[labelKey] = true
  637. }
  638. }
  639. }
  640. if len(costs.RAMAllocation) > 0 {
  641. a.RAMAllocationRecorder.WithLabelValues(namespace, podName, containerName, nodeName, nodeName).Set(costs.RAMAllocation[0].Value)
  642. }
  643. if len(costs.CPUAllocation) > 0 {
  644. a.CPUAllocationRecorder.WithLabelValues(namespace, podName, containerName, nodeName, nodeName).Set(costs.CPUAllocation[0].Value)
  645. }
  646. if len(costs.GPUReq) > 0 {
  647. // allocation here is set to the request because shared GPU usage not yet supported.
  648. a.GPUAllocationRecorder.WithLabelValues(namespace, podName, containerName, nodeName, nodeName).Set(costs.GPUReq[0].Value)
  649. }
  650. labelKey := getKeyFromLabelStrings(namespace, podName, containerName, nodeName, nodeName)
  651. if podStatus[podName] == v1.PodRunning { // Only report data for current pods
  652. containerSeen[labelKey] = true
  653. } else {
  654. containerSeen[labelKey] = false
  655. }
  656. storageClasses := a.Model.Cache.GetAllStorageClasses()
  657. storageClassMap := make(map[string]map[string]string)
  658. for _, storageClass := range storageClasses {
  659. params := storageClass.Parameters
  660. storageClassMap[storageClass.ObjectMeta.Name] = params
  661. if storageClass.GetAnnotations()["storageclass.kubernetes.io/is-default-class"] == "true" || storageClass.GetAnnotations()["storageclass.beta.kubernetes.io/is-default-class"] == "true" {
  662. storageClassMap["default"] = params
  663. storageClassMap[""] = params
  664. }
  665. }
  666. pvs := a.Model.Cache.GetAllPersistentVolumes()
  667. for _, pv := range pvs {
  668. parameters, ok := storageClassMap[pv.Spec.StorageClassName]
  669. if !ok {
  670. klog.V(4).Infof("Unable to find parameters for storage class \"%s\". Does pv \"%s\" have a storageClassName?", pv.Spec.StorageClassName, pv.Name)
  671. }
  672. var region string
  673. if r, ok := pv.Labels[v1.LabelZoneRegion]; ok {
  674. region = r
  675. } else {
  676. region = defaultRegion
  677. }
  678. cacPv := &costAnalyzerCloud.PV{
  679. Class: pv.Spec.StorageClassName,
  680. Region: region,
  681. Parameters: parameters,
  682. }
  683. GetPVCost(cacPv, pv, a.Cloud, region)
  684. c, _ := strconv.ParseFloat(cacPv.Cost, 64)
  685. a.PersistentVolumePriceRecorder.WithLabelValues(pv.Name, pv.Name).Set(c)
  686. labelKey := getKeyFromLabelStrings(pv.Name, pv.Name)
  687. pvSeen[labelKey] = true
  688. }
  689. containerUptime, _ := ComputeUptimes(a.PrometheusClient)
  690. for key, uptime := range containerUptime {
  691. container, _ := NewContainerMetricFromKey(key)
  692. a.ContainerUptimeRecorder.WithLabelValues(container.Namespace, container.PodName, container.ContainerName).Set(uptime)
  693. }
  694. }
  695. for labelString, seen := range nodeSeen {
  696. if !seen {
  697. labels := getLabelStringsFromKey(labelString)
  698. a.NodeTotalPriceRecorder.DeleteLabelValues(labels...)
  699. a.CPUPriceRecorder.DeleteLabelValues(labels...)
  700. a.GPUPriceRecorder.DeleteLabelValues(labels...)
  701. a.RAMPriceRecorder.DeleteLabelValues(labels...)
  702. delete(nodeSeen, labelString)
  703. }
  704. nodeSeen[labelString] = false
  705. }
  706. for labelString, seen := range containerSeen {
  707. if !seen {
  708. labels := getLabelStringsFromKey(labelString)
  709. a.RAMAllocationRecorder.DeleteLabelValues(labels...)
  710. a.CPUAllocationRecorder.DeleteLabelValues(labels...)
  711. a.GPUAllocationRecorder.DeleteLabelValues(labels...)
  712. a.ContainerUptimeRecorder.DeleteLabelValues(labels...)
  713. delete(containerSeen, labelString)
  714. }
  715. containerSeen[labelString] = false
  716. }
  717. for labelString, seen := range pvSeen {
  718. if !seen {
  719. labels := getLabelStringsFromKey(labelString)
  720. a.PersistentVolumePriceRecorder.DeleteLabelValues(labels...)
  721. delete(pvSeen, labelString)
  722. }
  723. pvSeen[labelString] = false
  724. }
  725. for labelString, seen := range pvcSeen {
  726. if !seen {
  727. labels := getLabelStringsFromKey(labelString)
  728. a.PVAllocationRecorder.DeleteLabelValues(labels...)
  729. delete(pvcSeen, labelString)
  730. }
  731. pvcSeen[labelString] = false
  732. }
  733. time.Sleep(time.Minute)
  734. }
  735. }()
  736. }
  737. // Creates a new ClusterManager instance using a boltdb storage. If that fails,
  738. // then we fall back to a memory-only storage.
  739. func newClusterManager() *cm.ClusterManager {
  740. clustersConfigFile := "/var/configs/clusters/default-clusters.yaml"
  741. // Return a memory-backed cluster manager populated by configmap
  742. return cm.NewConfiguredClusterManager(cm.NewMapDBClusterStorage(), clustersConfigFile)
  743. // NOTE: The following should be used with a persistent disk store. Since the
  744. // NOTE: configmap approach is currently the "persistent" source (entries are read-only
  745. // NOTE: on the backend), we don't currently need to store on disk.
  746. /*
  747. path := os.Getenv("CONFIG_PATH")
  748. db, err := bolt.Open(path+"costmodel.db", 0600, nil)
  749. if err != nil {
  750. klog.V(1).Infof("[Error] Failed to create costmodel.db: %s", err.Error())
  751. return cm.NewConfiguredClusterManager(cm.NewMapDBClusterStorage(), clustersConfigFile)
  752. }
  753. store, err := cm.NewBoltDBClusterStorage("clusters", db)
  754. if err != nil {
  755. klog.V(1).Infof("[Error] Failed to Create Cluster Storage: %s", err.Error())
  756. return cm.NewConfiguredClusterManager(cm.NewMapDBClusterStorage(), clustersConfigFile)
  757. }
  758. return cm.NewConfiguredClusterManager(store, clustersConfigFile)
  759. */
  760. }
  761. type ConfigWatchers struct {
  762. ConfigmapName string
  763. WatchFunc func(string, map[string]string) error
  764. }
  765. func Initialize(additionalConfigWatchers ...ConfigWatchers) {
  766. klog.InitFlags(nil)
  767. flag.Set("v", "3")
  768. flag.Parse()
  769. klog.V(1).Infof("Starting cost-model (git commit \"%s\")", gitCommit)
  770. address := os.Getenv(prometheusServerEndpointEnvVar)
  771. if address == "" {
  772. klog.Fatalf("No address for prometheus set in $%s. Aborting.", prometheusServerEndpointEnvVar)
  773. }
  774. var LongTimeoutRoundTripper http.RoundTripper = &http.Transport{ // may be necessary for long prometheus queries. TODO: make this configurable
  775. Proxy: http.ProxyFromEnvironment,
  776. DialContext: (&net.Dialer{
  777. Timeout: 120 * time.Second,
  778. KeepAlive: 120 * time.Second,
  779. }).DialContext,
  780. TLSHandshakeTimeout: 10 * time.Second,
  781. }
  782. pc := prometheusClient.Config{
  783. Address: address,
  784. RoundTripper: LongTimeoutRoundTripper,
  785. }
  786. promCli, _ := prometheusClient.NewClient(pc)
  787. api := prometheusAPI.NewAPI(promCli)
  788. _, err := api.Config(context.Background())
  789. if err != nil {
  790. klog.Fatalf("No valid prometheus config file at %s. Error: %s . Troubleshooting help available at: %s", address, err.Error(), prometheusTroubleshootingEp)
  791. }
  792. klog.V(1).Info("Success: retrieved a prometheus config file from: " + address)
  793. _, err = ValidatePrometheus(promCli, false)
  794. if err != nil {
  795. klog.Fatalf("Failed to query prometheus at %s. Error: %s . Troubleshooting help available at: %s", address, err.Error(), prometheusTroubleshootingEp)
  796. }
  797. klog.V(1).Info("Success: retrieved the 'up' query against prometheus at: " + address)
  798. // Kubernetes API setup
  799. kc, err := rest.InClusterConfig()
  800. if err != nil {
  801. panic(err.Error())
  802. }
  803. kubeClientset, err := kubernetes.NewForConfig(kc)
  804. if err != nil {
  805. panic(err.Error())
  806. }
  807. // Create Kubernetes Cluster Cache + Watchers
  808. k8sCache := clustercache.NewKubernetesClusterCache(kubeClientset)
  809. k8sCache.Run()
  810. cloudProviderKey := os.Getenv("CLOUD_PROVIDER_API_KEY")
  811. cloudProvider, err := costAnalyzerCloud.NewProvider(k8sCache, cloudProviderKey)
  812. if err != nil {
  813. panic(err.Error())
  814. }
  815. watchConfigFunc := func(c interface{}) {
  816. conf := c.(*v1.ConfigMap)
  817. if conf.GetName() == "pricing-configs" {
  818. _, err := cloudProvider.UpdateConfigFromConfigMap(conf.Data)
  819. if err != nil {
  820. klog.Infof("ERROR UPDATING %s CONFIG: %s", "pricing-configs", err.Error())
  821. }
  822. }
  823. for _, cw := range additionalConfigWatchers {
  824. if conf.GetName() == cw.ConfigmapName {
  825. err := cw.WatchFunc(conf.GetName(), conf.Data)
  826. if err != nil {
  827. klog.Infof("ERROR UPDATING %s CONFIG: %s", cw.ConfigmapName, err.Error())
  828. }
  829. }
  830. }
  831. }
  832. kubecostNamespace := os.Getenv("KUBECOST_NAMESPACE")
  833. // We need an initial invocation because the init of the cache has happened before we had access to the provider.
  834. configs, err := kubeClientset.CoreV1().ConfigMaps(kubecostNamespace).Get("pricing-configs", metav1.GetOptions{})
  835. if err != nil {
  836. klog.Infof("No %s configmap found at installtime, using existing configs: %s", "pricing-configs", err.Error())
  837. } else {
  838. watchConfigFunc(configs)
  839. }
  840. for _, cw := range additionalConfigWatchers {
  841. configs, err := kubeClientset.CoreV1().ConfigMaps(kubecostNamespace).Get(cw.ConfigmapName, metav1.GetOptions{})
  842. if err != nil {
  843. klog.Infof("No %s configmap found at installtime, using existing configs: %s", cw.ConfigmapName, err.Error())
  844. } else {
  845. watchConfigFunc(configs)
  846. }
  847. }
  848. k8sCache.SetConfigMapUpdateFunc(watchConfigFunc)
  849. // TODO: General Architecture Note: Several passes have been made to modularize a lot of
  850. // TODO: our code, but the router still continues to be the obvious entry point for new \
  851. // TODO: features. We should look to spliting out the actual "router" functionality and
  852. // TODO: implement a builder -> controller for stitching new features and other dependencies.
  853. clusterManager := newClusterManager()
  854. cpuGv := prometheus.NewGaugeVec(prometheus.GaugeOpts{
  855. Name: "node_cpu_hourly_cost",
  856. Help: "node_cpu_hourly_cost hourly cost for each cpu on this node",
  857. }, []string{"instance", "node", "instance_type", "region"})
  858. ramGv := prometheus.NewGaugeVec(prometheus.GaugeOpts{
  859. Name: "node_ram_hourly_cost",
  860. Help: "node_ram_hourly_cost hourly cost for each gb of ram on this node",
  861. }, []string{"instance", "node", "instance_type", "region"})
  862. gpuGv := prometheus.NewGaugeVec(prometheus.GaugeOpts{
  863. Name: "node_gpu_hourly_cost",
  864. Help: "node_gpu_hourly_cost hourly cost for each gpu on this node",
  865. }, []string{"instance", "node", "instance_type", "region"})
  866. totalGv := prometheus.NewGaugeVec(prometheus.GaugeOpts{
  867. Name: "node_total_hourly_cost",
  868. Help: "node_total_hourly_cost Total node cost per hour",
  869. }, []string{"instance", "node", "instance_type", "region"})
  870. pvGv := prometheus.NewGaugeVec(prometheus.GaugeOpts{
  871. Name: "pv_hourly_cost",
  872. Help: "pv_hourly_cost Cost per GB per hour on a persistent disk",
  873. }, []string{"volumename", "persistentvolume"})
  874. RAMAllocation := prometheus.NewGaugeVec(prometheus.GaugeOpts{
  875. Name: "container_memory_allocation_bytes",
  876. Help: "container_memory_allocation_bytes Bytes of RAM used",
  877. }, []string{"namespace", "pod", "container", "instance", "node"})
  878. CPUAllocation := prometheus.NewGaugeVec(prometheus.GaugeOpts{
  879. Name: "container_cpu_allocation",
  880. Help: "container_cpu_allocation Percent of a single CPU used in a minute",
  881. }, []string{"namespace", "pod", "container", "instance", "node"})
  882. GPUAllocation := prometheus.NewGaugeVec(prometheus.GaugeOpts{
  883. Name: "container_gpu_allocation",
  884. Help: "container_gpu_allocation GPU used",
  885. }, []string{"namespace", "pod", "container", "instance", "node"})
  886. PVAllocation := prometheus.NewGaugeVec(prometheus.GaugeOpts{
  887. Name: "pod_pvc_allocation",
  888. Help: "pod_pvc_allocation Bytes used by a PVC attached to a pod",
  889. }, []string{"namespace", "pod", "persistentvolumeclaim", "persistentvolume"})
  890. ContainerUptimeRecorder := prometheus.NewGaugeVec(prometheus.GaugeOpts{
  891. Name: "container_uptime_seconds",
  892. Help: "container_uptime_seconds Seconds a container has been running",
  893. }, []string{"namespace", "pod", "container"})
  894. NetworkZoneEgressRecorder := prometheus.NewGauge(prometheus.GaugeOpts{
  895. Name: "kubecost_network_zone_egress_cost",
  896. Help: "kubecost_network_zone_egress_cost Total cost per GB egress across zones",
  897. })
  898. NetworkRegionEgressRecorder := prometheus.NewGauge(prometheus.GaugeOpts{
  899. Name: "kubecost_network_region_egress_cost",
  900. Help: "kubecost_network_region_egress_cost Total cost per GB egress across regions",
  901. })
  902. NetworkInternetEgressRecorder := prometheus.NewGauge(prometheus.GaugeOpts{
  903. Name: "kubecost_network_internet_egress_cost",
  904. Help: "kubecost_network_internet_egress_cost Total cost per GB of internet egress.",
  905. })
  906. prometheus.MustRegister(cpuGv)
  907. prometheus.MustRegister(ramGv)
  908. prometheus.MustRegister(gpuGv)
  909. prometheus.MustRegister(totalGv)
  910. prometheus.MustRegister(pvGv)
  911. prometheus.MustRegister(RAMAllocation)
  912. prometheus.MustRegister(CPUAllocation)
  913. prometheus.MustRegister(ContainerUptimeRecorder)
  914. prometheus.MustRegister(PVAllocation)
  915. prometheus.MustRegister(GPUAllocation)
  916. prometheus.MustRegister(NetworkZoneEgressRecorder, NetworkRegionEgressRecorder, NetworkInternetEgressRecorder)
  917. prometheus.MustRegister(ServiceCollector{
  918. KubeClientSet: kubeClientset,
  919. })
  920. prometheus.MustRegister(DeploymentCollector{
  921. KubeClientSet: kubeClientset,
  922. })
  923. prometheus.MustRegister(StatefulsetCollector{
  924. KubeClientSet: kubeClientset,
  925. })
  926. // cache responses from model for a default of 5 minutes; clear expired responses every 10 minutes
  927. outOfClusterCache := cache.New(time.Minute*5, time.Minute*10)
  928. A = Accesses{
  929. PrometheusClient: promCli,
  930. KubeClientSet: kubeClientset,
  931. ClusterManager: clusterManager,
  932. Cloud: cloudProvider,
  933. CPUPriceRecorder: cpuGv,
  934. RAMPriceRecorder: ramGv,
  935. GPUPriceRecorder: gpuGv,
  936. NodeTotalPriceRecorder: totalGv,
  937. RAMAllocationRecorder: RAMAllocation,
  938. CPUAllocationRecorder: CPUAllocation,
  939. GPUAllocationRecorder: GPUAllocation,
  940. PVAllocationRecorder: PVAllocation,
  941. ContainerUptimeRecorder: ContainerUptimeRecorder,
  942. NetworkZoneEgressRecorder: NetworkZoneEgressRecorder,
  943. NetworkRegionEgressRecorder: NetworkRegionEgressRecorder,
  944. NetworkInternetEgressRecorder: NetworkInternetEgressRecorder,
  945. PersistentVolumePriceRecorder: pvGv,
  946. Model: NewCostModel(k8sCache),
  947. OutOfClusterCache: outOfClusterCache,
  948. }
  949. remoteEnabled := os.Getenv(remoteEnabled)
  950. if remoteEnabled == "true" {
  951. info, err := cloudProvider.ClusterInfo()
  952. klog.Infof("Saving cluster with id:'%s', and name:'%s' to durable storage", info["id"], info["name"])
  953. if err != nil {
  954. klog.Infof("Error saving cluster id %s", err.Error())
  955. }
  956. _, _, err = costAnalyzerCloud.GetOrCreateClusterMeta(info["id"], info["name"])
  957. if err != nil {
  958. klog.Infof("Unable to set cluster id '%s' for cluster '%s', %s", info["id"], info["name"], err.Error())
  959. }
  960. }
  961. // Thanos Client
  962. if os.Getenv(thanosEnabled) == "true" {
  963. thanosUrl := os.Getenv(thanosQueryUrl)
  964. if thanosUrl != "" {
  965. var thanosRT http.RoundTripper = &http.Transport{
  966. Proxy: http.ProxyFromEnvironment,
  967. DialContext: (&net.Dialer{
  968. Timeout: 120 * time.Second,
  969. KeepAlive: 120 * time.Second,
  970. }).DialContext,
  971. TLSHandshakeTimeout: 10 * time.Second,
  972. }
  973. thanosConfig := prometheusClient.Config{
  974. Address: thanosUrl,
  975. RoundTripper: thanosRT,
  976. }
  977. thanosCli, _ := prometheusClient.NewClient(thanosConfig)
  978. _, err = ValidatePrometheus(thanosCli, true)
  979. if err != nil {
  980. klog.V(1).Infof("[Warning] Failed to query Thanos at %s. Error: %s.", thanosUrl, err.Error())
  981. A.ThanosClient = thanosCli
  982. } else {
  983. klog.V(1).Info("Success: retrieved the 'up' query against Thanos at: " + thanosUrl)
  984. A.ThanosClient = thanosCli
  985. }
  986. } else {
  987. klog.Infof("Error resolving environment variable: $%s", thanosQueryUrl)
  988. }
  989. }
  990. err = A.Cloud.DownloadPricingData()
  991. if err != nil {
  992. klog.V(1).Info("Failed to download pricing data: " + err.Error())
  993. }
  994. A.recordPrices()
  995. managerEndpoints := cm.NewClusterManagerEndpoints(A.ClusterManager)
  996. Router.GET("/costDataModel", A.CostDataModel)
  997. Router.GET("/costDataModelRange", A.CostDataModelRange)
  998. Router.GET("/costDataModelRangeLarge", A.CostDataModelRangeLarge)
  999. Router.GET("/outOfClusterCosts", A.OutOfClusterCostsWithCache)
  1000. Router.GET("/allNodePricing", A.GetAllNodePricing)
  1001. Router.POST("/refreshPricing", A.RefreshPricingData)
  1002. Router.GET("/clusterCostsOverTime", A.ClusterCostsOverTime)
  1003. Router.GET("/clusterCosts", A.ClusterCosts)
  1004. Router.GET("/validatePrometheus", A.GetPrometheusMetadata)
  1005. Router.GET("/managementPlatform", A.ManagementPlatform)
  1006. Router.GET("/clusterInfo", A.ClusterInfo)
  1007. Router.GET("/containerUptimes", A.ContainerUptimes)
  1008. Router.GET("/clusters", managerEndpoints.GetAllClusters)
  1009. Router.PUT("/clusters", managerEndpoints.PutCluster)
  1010. Router.DELETE("/clusters/:id", managerEndpoints.DeleteCluster)
  1011. }