router.go 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845
  1. package costmodel
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "os"
  9. "path"
  10. "reflect"
  11. "regexp"
  12. "strconv"
  13. "strings"
  14. "sync"
  15. "time"
  16. "github.com/microcosm-cc/bluemonday"
  17. "github.com/opencost/opencost/pkg/cloud/aws"
  18. cloudconfig "github.com/opencost/opencost/pkg/cloud/config"
  19. "github.com/opencost/opencost/pkg/cloud/gcp"
  20. "github.com/opencost/opencost/pkg/cloud/provider"
  21. "github.com/opencost/opencost/pkg/cloudcost"
  22. "github.com/opencost/opencost/pkg/config"
  23. "github.com/opencost/opencost/pkg/kubeconfig"
  24. "github.com/opencost/opencost/pkg/metrics"
  25. "github.com/opencost/opencost/pkg/services"
  26. "github.com/opencost/opencost/pkg/util/httputil"
  27. "github.com/opencost/opencost/pkg/util/timeutil"
  28. "github.com/opencost/opencost/pkg/util/watcher"
  29. "github.com/opencost/opencost/pkg/version"
  30. "github.com/spf13/viper"
  31. v1 "k8s.io/api/core/v1"
  32. "github.com/julienschmidt/httprouter"
  33. "github.com/getsentry/sentry-go"
  34. "github.com/opencost/opencost/pkg/cloud/azure"
  35. "github.com/opencost/opencost/pkg/cloud/models"
  36. "github.com/opencost/opencost/pkg/cloud/utils"
  37. "github.com/opencost/opencost/pkg/clustercache"
  38. "github.com/opencost/opencost/pkg/costmodel/clusters"
  39. "github.com/opencost/opencost/pkg/env"
  40. "github.com/opencost/opencost/pkg/errors"
  41. "github.com/opencost/opencost/pkg/kubecost"
  42. "github.com/opencost/opencost/pkg/log"
  43. "github.com/opencost/opencost/pkg/prom"
  44. "github.com/opencost/opencost/pkg/thanos"
  45. "github.com/opencost/opencost/pkg/util/json"
  46. prometheus "github.com/prometheus/client_golang/api"
  47. prometheusAPI "github.com/prometheus/client_golang/api/prometheus/v1"
  48. appsv1 "k8s.io/api/apps/v1"
  49. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  50. "github.com/patrickmn/go-cache"
  51. "k8s.io/client-go/kubernetes"
  52. )
  53. var sanitizePolicy = bluemonday.UGCPolicy()
  54. const (
  55. RFC3339Milli = "2006-01-02T15:04:05.000Z"
  56. maxCacheMinutes1d = 11
  57. maxCacheMinutes2d = 17
  58. maxCacheMinutes7d = 37
  59. maxCacheMinutes30d = 137
  60. CustomPricingSetting = "CustomPricing"
  61. DiscountSetting = "Discount"
  62. epRules = apiPrefix + "/rules"
  63. LogSeparator = "+-------------------------------------------------------------------------------------"
  64. )
  65. var (
  66. // gitCommit is set by the build system
  67. gitCommit string
  68. // ANSIRegex matches ANSI escape and colors https://en.wikipedia.org/wiki/ANSI_escape_code
  69. ANSIRegex = regexp.MustCompile("\x1b\\[[0-9;]*m")
  70. )
  71. // Accesses defines a singleton application instance, providing access to
  72. // Prometheus, Kubernetes, the cloud provider, and caches.
  73. type Accesses struct {
  74. Router *httprouter.Router
  75. PrometheusClient prometheus.Client
  76. ThanosClient prometheus.Client
  77. KubeClientSet kubernetes.Interface
  78. ClusterCache clustercache.ClusterCache
  79. ClusterMap clusters.ClusterMap
  80. CloudProvider models.Provider
  81. ConfigFileManager *config.ConfigFileManager
  82. CloudConfigController *cloudconfig.Controller
  83. CloudCostPipelineService *cloudcost.PipelineService
  84. CloudCostQueryService *cloudcost.QueryService
  85. ClusterInfoProvider clusters.ClusterInfoProvider
  86. Model *CostModel
  87. MetricsEmitter *CostModelMetricsEmitter
  88. OutOfClusterCache *cache.Cache
  89. AggregateCache *cache.Cache
  90. CostDataCache *cache.Cache
  91. ClusterCostsCache *cache.Cache
  92. CacheExpiration map[time.Duration]time.Duration
  93. AggAPI Aggregator
  94. // SettingsCache stores current state of app settings
  95. SettingsCache *cache.Cache
  96. // settingsSubscribers tracks channels through which changes to different
  97. // settings will be published in a pub/sub model
  98. settingsSubscribers map[string][]chan string
  99. settingsMutex sync.Mutex
  100. // registered http service instances
  101. httpServices services.HTTPServices
  102. }
  103. // GetPrometheusClient decides whether the default Prometheus client or the Thanos client
  104. // should be used.
  105. func (a *Accesses) GetPrometheusClient(remote bool) prometheus.Client {
  106. // Use Thanos Client if it exists (enabled) and remote flag set
  107. var pc prometheus.Client
  108. if remote && a.ThanosClient != nil {
  109. pc = a.ThanosClient
  110. } else {
  111. pc = a.PrometheusClient
  112. }
  113. return pc
  114. }
  115. // GetCacheExpiration looks up and returns custom cache expiration for the given duration.
  116. // If one does not exists, it returns the default cache expiration, which is defined by
  117. // the particular cache.
  118. func (a *Accesses) GetCacheExpiration(dur time.Duration) time.Duration {
  119. if expiration, ok := a.CacheExpiration[dur]; ok {
  120. return expiration
  121. }
  122. return cache.DefaultExpiration
  123. }
  124. // GetCacheRefresh determines how long to wait before refreshing the cache for the given duration,
  125. // which is done 1 minute before we expect the cache to expire, or 1 minute if expiration is
  126. // not found or is less than 2 minutes.
  127. func (a *Accesses) GetCacheRefresh(dur time.Duration) time.Duration {
  128. expiry := a.GetCacheExpiration(dur).Minutes()
  129. if expiry <= 2.0 {
  130. return time.Minute
  131. }
  132. mins := time.Duration(expiry/2.0) * time.Minute
  133. return mins
  134. }
  135. func (a *Accesses) ClusterCostsFromCacheHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  136. w.Header().Set("Content-Type", "application/json")
  137. duration := 24 * time.Hour
  138. offset := time.Minute
  139. durationHrs := "24h"
  140. fmtOffset := "1m"
  141. pClient := a.GetPrometheusClient(true)
  142. key := fmt.Sprintf("%s:%s", durationHrs, fmtOffset)
  143. if data, valid := a.ClusterCostsCache.Get(key); valid {
  144. clusterCosts := data.(map[string]*ClusterCosts)
  145. w.Write(WrapDataWithMessage(clusterCosts, nil, "clusterCosts cache hit"))
  146. } else {
  147. data, err := a.ComputeClusterCosts(pClient, a.CloudProvider, duration, offset, true)
  148. w.Write(WrapDataWithMessage(data, err, fmt.Sprintf("clusterCosts cache miss: %s", key)))
  149. }
  150. }
  151. type Response struct {
  152. Code int `json:"code"`
  153. Status string `json:"status"`
  154. Data interface{} `json:"data"`
  155. Message string `json:"message,omitempty"`
  156. Warning string `json:"warning,omitempty"`
  157. }
  158. // 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
  159. type FilterFunc func(*CostData) (bool, string)
  160. // FilterCostData allows through only CostData that matches all the given filter functions
  161. func FilterCostData(data map[string]*CostData, retains []FilterFunc, filters []FilterFunc) (map[string]*CostData, int, map[string]int) {
  162. result := make(map[string]*CostData)
  163. filteredEnvironments := make(map[string]int)
  164. filteredContainers := 0
  165. DataLoop:
  166. for key, datum := range data {
  167. for _, rf := range retains {
  168. if ok, _ := rf(datum); ok {
  169. result[key] = datum
  170. // if any retain function passes, the data is retained and move on
  171. continue DataLoop
  172. }
  173. }
  174. for _, ff := range filters {
  175. if ok, environment := ff(datum); !ok {
  176. if environment != "" {
  177. filteredEnvironments[environment]++
  178. }
  179. filteredContainers++
  180. // if any filter function check fails, move on to the next datum
  181. continue DataLoop
  182. }
  183. }
  184. result[key] = datum
  185. }
  186. return result, filteredContainers, filteredEnvironments
  187. }
  188. func filterFields(fields string, data map[string]*CostData) map[string]CostData {
  189. fs := strings.Split(fields, ",")
  190. fmap := make(map[string]bool)
  191. for _, f := range fs {
  192. fieldNameLower := strings.ToLower(f) // convert to go struct name by uppercasing first letter
  193. log.Debugf("to delete: %s", fieldNameLower)
  194. fmap[fieldNameLower] = true
  195. }
  196. filteredData := make(map[string]CostData)
  197. for cname, costdata := range data {
  198. s := reflect.TypeOf(*costdata)
  199. val := reflect.ValueOf(*costdata)
  200. costdata2 := CostData{}
  201. cd2 := reflect.New(reflect.Indirect(reflect.ValueOf(costdata2)).Type()).Elem()
  202. n := s.NumField()
  203. for i := 0; i < n; i++ {
  204. field := s.Field(i)
  205. value := val.Field(i)
  206. value2 := cd2.Field(i)
  207. if _, ok := fmap[strings.ToLower(field.Name)]; !ok {
  208. value2.Set(reflect.Value(value))
  209. }
  210. }
  211. filteredData[cname] = cd2.Interface().(CostData)
  212. }
  213. return filteredData
  214. }
  215. func normalizeTimeParam(param string) (string, error) {
  216. if param == "" {
  217. return "", fmt.Errorf("invalid time param")
  218. }
  219. // convert days to hours
  220. if param[len(param)-1:] == "d" {
  221. count := param[:len(param)-1]
  222. val, err := strconv.ParseInt(count, 10, 64)
  223. if err != nil {
  224. return "", err
  225. }
  226. val = val * 24
  227. param = fmt.Sprintf("%dh", val)
  228. }
  229. return param, nil
  230. }
  231. // ParsePercentString takes a string of expected format "N%" and returns a floating point 0.0N.
  232. // If the "%" symbol is missing, it just returns 0.0N. Empty string is interpreted as "0%" and
  233. // return 0.0.
  234. func ParsePercentString(percentStr string) (float64, error) {
  235. if len(percentStr) == 0 {
  236. return 0.0, nil
  237. }
  238. if percentStr[len(percentStr)-1:] == "%" {
  239. percentStr = percentStr[:len(percentStr)-1]
  240. }
  241. discount, err := strconv.ParseFloat(percentStr, 64)
  242. if err != nil {
  243. return 0.0, err
  244. }
  245. discount *= 0.01
  246. return discount, nil
  247. }
  248. func WrapData(data interface{}, err error) []byte {
  249. var resp []byte
  250. if err != nil {
  251. log.Errorf("Error returned to client: %s", err.Error())
  252. resp, _ = json.Marshal(&Response{
  253. Code: http.StatusInternalServerError,
  254. Status: "error",
  255. Message: err.Error(),
  256. Data: data,
  257. })
  258. } else {
  259. resp, err = json.Marshal(&Response{
  260. Code: http.StatusOK,
  261. Status: "success",
  262. Data: data,
  263. })
  264. if err != nil {
  265. log.Errorf("error marshaling response json: %s", err.Error())
  266. }
  267. }
  268. return resp
  269. }
  270. func WrapDataWithMessage(data interface{}, err error, message string) []byte {
  271. var resp []byte
  272. if err != nil {
  273. log.Errorf("Error returned to client: %s", err.Error())
  274. resp, _ = json.Marshal(&Response{
  275. Code: http.StatusInternalServerError,
  276. Status: "error",
  277. Message: err.Error(),
  278. Data: data,
  279. })
  280. } else {
  281. resp, _ = json.Marshal(&Response{
  282. Code: http.StatusOK,
  283. Status: "success",
  284. Data: data,
  285. Message: message,
  286. })
  287. }
  288. return resp
  289. }
  290. func WrapDataWithWarning(data interface{}, err error, warning string) []byte {
  291. var resp []byte
  292. if err != nil {
  293. log.Errorf("Error returned to client: %s", err.Error())
  294. resp, _ = json.Marshal(&Response{
  295. Code: http.StatusInternalServerError,
  296. Status: "error",
  297. Message: err.Error(),
  298. Warning: warning,
  299. Data: data,
  300. })
  301. } else {
  302. resp, _ = json.Marshal(&Response{
  303. Code: http.StatusOK,
  304. Status: "success",
  305. Data: data,
  306. Warning: warning,
  307. })
  308. }
  309. return resp
  310. }
  311. func WrapDataWithMessageAndWarning(data interface{}, err error, message, warning string) []byte {
  312. var resp []byte
  313. if err != nil {
  314. log.Errorf("Error returned to client: %s", err.Error())
  315. resp, _ = json.Marshal(&Response{
  316. Code: http.StatusInternalServerError,
  317. Status: "error",
  318. Message: err.Error(),
  319. Warning: warning,
  320. Data: data,
  321. })
  322. } else {
  323. resp, _ = json.Marshal(&Response{
  324. Code: http.StatusOK,
  325. Status: "success",
  326. Data: data,
  327. Message: message,
  328. Warning: warning,
  329. })
  330. }
  331. return resp
  332. }
  333. // wrapAsObjectItems wraps a slice of items into an object containing a single items list
  334. // allows our k8s proxy methods to emulate a List() request to k8s API
  335. func wrapAsObjectItems(items interface{}) map[string]interface{} {
  336. return map[string]interface{}{
  337. "items": items,
  338. }
  339. }
  340. // 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.
  341. func (a *Accesses) RefreshPricingData(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  342. w.Header().Set("Content-Type", "application/json")
  343. w.Header().Set("Access-Control-Allow-Origin", "*")
  344. err := a.CloudProvider.DownloadPricingData()
  345. if err != nil {
  346. log.Errorf("Error refreshing pricing data: %s", err.Error())
  347. }
  348. w.Write(WrapData(nil, err))
  349. }
  350. func (a *Accesses) CostDataModel(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  351. w.Header().Set("Content-Type", "application/json")
  352. w.Header().Set("Access-Control-Allow-Origin", "*")
  353. window := r.URL.Query().Get("timeWindow")
  354. offset := r.URL.Query().Get("offset")
  355. fields := r.URL.Query().Get("filterFields")
  356. namespace := r.URL.Query().Get("namespace")
  357. if offset != "" {
  358. offset = "offset " + offset
  359. }
  360. data, err := a.Model.ComputeCostData(a.PrometheusClient, a.CloudProvider, window, offset, namespace)
  361. if fields != "" {
  362. filteredData := filterFields(fields, data)
  363. w.Write(WrapData(filteredData, err))
  364. } else {
  365. w.Write(WrapData(data, err))
  366. }
  367. }
  368. func (a *Accesses) ClusterCosts(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  369. w.Header().Set("Content-Type", "application/json")
  370. w.Header().Set("Access-Control-Allow-Origin", "*")
  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 argument")))
  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. useThanos, _ := strconv.ParseBool(r.URL.Query().Get("multi"))
  392. if useThanos && !thanos.IsEnabled() {
  393. w.Write(WrapData(nil, fmt.Errorf("Multi=true while Thanos is not enabled.")))
  394. return
  395. }
  396. var client prometheus.Client
  397. if useThanos {
  398. client = a.ThanosClient
  399. offsetDur = thanos.OffsetDuration()
  400. } else {
  401. client = a.PrometheusClient
  402. }
  403. data, err := a.ComputeClusterCosts(client, a.CloudProvider, windowDur, offsetDur, true)
  404. w.Write(WrapData(data, err))
  405. }
  406. func (a *Accesses) ClusterCostsOverTime(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  407. w.Header().Set("Content-Type", "application/json")
  408. w.Header().Set("Access-Control-Allow-Origin", "*")
  409. start := r.URL.Query().Get("start")
  410. end := r.URL.Query().Get("end")
  411. window := r.URL.Query().Get("window")
  412. offset := r.URL.Query().Get("offset")
  413. if window == "" {
  414. w.Write(WrapData(nil, fmt.Errorf("missing window argument")))
  415. return
  416. }
  417. windowDur, err := timeutil.ParseDuration(window)
  418. if err != nil {
  419. w.Write(WrapData(nil, fmt.Errorf("error parsing window (%s): %s", window, err)))
  420. return
  421. }
  422. // offset is not a required parameter
  423. var offsetDur time.Duration
  424. if offset != "" {
  425. offsetDur, err = timeutil.ParseDuration(offset)
  426. if err != nil {
  427. w.Write(WrapData(nil, fmt.Errorf("error parsing offset (%s): %s", offset, err)))
  428. return
  429. }
  430. }
  431. data, err := ClusterCostsOverTime(a.PrometheusClient, a.CloudProvider, start, end, windowDur, offsetDur)
  432. w.Write(WrapData(data, err))
  433. }
  434. func (a *Accesses) CostDataModelRange(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  435. w.Header().Set("Content-Type", "application/json")
  436. w.Header().Set("Access-Control-Allow-Origin", "*")
  437. startStr := r.URL.Query().Get("start")
  438. endStr := r.URL.Query().Get("end")
  439. windowStr := r.URL.Query().Get("window")
  440. fields := r.URL.Query().Get("filterFields")
  441. namespace := r.URL.Query().Get("namespace")
  442. cluster := r.URL.Query().Get("cluster")
  443. remote := r.URL.Query().Get("remote")
  444. remoteEnabled := env.IsRemoteEnabled() && remote != "false"
  445. layout := "2006-01-02T15:04:05.000Z"
  446. start, err := time.Parse(layout, startStr)
  447. if err != nil {
  448. w.Write(WrapDataWithMessage(nil, fmt.Errorf("invalid start date: %s", startStr), fmt.Sprintf("invalid start date: %s", startStr)))
  449. return
  450. }
  451. end, err := time.Parse(layout, endStr)
  452. if err != nil {
  453. w.Write(WrapDataWithMessage(nil, fmt.Errorf("invalid end date: %s", endStr), fmt.Sprintf("invalid end date: %s", endStr)))
  454. return
  455. }
  456. window := kubecost.NewWindow(&start, &end)
  457. if window.IsOpen() || !window.HasDuration() || window.IsNegative() {
  458. w.Write(WrapDataWithMessage(nil, fmt.Errorf("invalid date range: %s", window), fmt.Sprintf("invalid date range: %s", window)))
  459. return
  460. }
  461. resolution := time.Hour
  462. if resDur, err := time.ParseDuration(windowStr); err == nil {
  463. resolution = resDur
  464. }
  465. // Use Thanos Client if it exists (enabled) and remote flag set
  466. var pClient prometheus.Client
  467. if remote != "false" && a.ThanosClient != nil {
  468. pClient = a.ThanosClient
  469. } else {
  470. pClient = a.PrometheusClient
  471. }
  472. data, err := a.Model.ComputeCostDataRange(pClient, a.CloudProvider, window, resolution, namespace, cluster, remoteEnabled)
  473. if err != nil {
  474. w.Write(WrapData(nil, err))
  475. }
  476. if fields != "" {
  477. filteredData := filterFields(fields, data)
  478. w.Write(WrapData(filteredData, err))
  479. } else {
  480. w.Write(WrapData(data, err))
  481. }
  482. }
  483. func parseAggregations(customAggregation, aggregator, filterType string) (string, []string, string) {
  484. var key string
  485. var filter string
  486. var val []string
  487. if customAggregation != "" {
  488. key = customAggregation
  489. filter = filterType
  490. val = strings.Split(customAggregation, ",")
  491. } else {
  492. aggregations := strings.Split(aggregator, ",")
  493. for i, agg := range aggregations {
  494. aggregations[i] = "kubernetes_" + agg
  495. }
  496. key = strings.Join(aggregations, ",")
  497. filter = "kubernetes_" + filterType
  498. val = aggregations
  499. }
  500. return key, val, filter
  501. }
  502. func (a *Accesses) GetAllNodePricing(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  503. w.Header().Set("Content-Type", "application/json")
  504. w.Header().Set("Access-Control-Allow-Origin", "*")
  505. data, err := a.CloudProvider.AllNodePricing()
  506. w.Write(WrapData(data, err))
  507. }
  508. func (a *Accesses) GetConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  509. w.Header().Set("Content-Type", "application/json")
  510. w.Header().Set("Access-Control-Allow-Origin", "*")
  511. data, err := a.CloudProvider.GetConfig()
  512. w.Write(WrapData(data, err))
  513. }
  514. func (a *Accesses) UpdateSpotInfoConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  515. w.Header().Set("Content-Type", "application/json")
  516. w.Header().Set("Access-Control-Allow-Origin", "*")
  517. data, err := a.CloudProvider.UpdateConfig(r.Body, aws.SpotInfoUpdateType)
  518. if err != nil {
  519. w.Write(WrapData(data, err))
  520. return
  521. }
  522. w.Write(WrapData(data, err))
  523. err = a.CloudProvider.DownloadPricingData()
  524. if err != nil {
  525. log.Errorf("Error redownloading data on config update: %s", err.Error())
  526. }
  527. return
  528. }
  529. func (a *Accesses) UpdateAthenaInfoConfigs(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, aws.AthenaInfoUpdateType)
  533. if err != nil {
  534. w.Write(WrapData(data, err))
  535. return
  536. }
  537. w.Write(WrapData(data, err))
  538. return
  539. }
  540. func (a *Accesses) UpdateBigQueryInfoConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  541. w.Header().Set("Content-Type", "application/json")
  542. w.Header().Set("Access-Control-Allow-Origin", "*")
  543. data, err := a.CloudProvider.UpdateConfig(r.Body, gcp.BigqueryUpdateType)
  544. if err != nil {
  545. w.Write(WrapData(data, err))
  546. return
  547. }
  548. w.Write(WrapData(data, err))
  549. return
  550. }
  551. func (a *Accesses) UpdateAzureStorageConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  552. w.Header().Set("Content-Type", "application/json")
  553. w.Header().Set("Access-Control-Allow-Origin", "*")
  554. data, err := a.CloudProvider.UpdateConfig(r.Body, azure.AzureStorageUpdateType)
  555. if err != nil {
  556. w.Write(WrapData(data, err))
  557. return
  558. }
  559. w.Write(WrapData(data, err))
  560. return
  561. }
  562. func (a *Accesses) UpdateConfigByKey(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  563. w.Header().Set("Content-Type", "application/json")
  564. w.Header().Set("Access-Control-Allow-Origin", "*")
  565. data, err := a.CloudProvider.UpdateConfig(r.Body, "")
  566. if err != nil {
  567. w.Write(WrapData(data, err))
  568. return
  569. }
  570. w.Write(WrapData(data, err))
  571. return
  572. }
  573. func (a *Accesses) ManagementPlatform(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  574. w.Header().Set("Content-Type", "application/json")
  575. w.Header().Set("Access-Control-Allow-Origin", "*")
  576. data, err := a.CloudProvider.GetManagementPlatform()
  577. if err != nil {
  578. w.Write(WrapData(data, err))
  579. return
  580. }
  581. w.Write(WrapData(data, err))
  582. return
  583. }
  584. func (a *Accesses) ClusterInfo(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  585. w.Header().Set("Content-Type", "application/json")
  586. w.Header().Set("Access-Control-Allow-Origin", "*")
  587. data := a.ClusterInfoProvider.GetClusterInfo()
  588. w.Write(WrapData(data, nil))
  589. }
  590. func (a *Accesses) GetClusterInfoMap(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  591. w.Header().Set("Content-Type", "application/json")
  592. w.Header().Set("Access-Control-Allow-Origin", "*")
  593. data := a.ClusterMap.AsMap()
  594. w.Write(WrapData(data, nil))
  595. }
  596. func (a *Accesses) GetServiceAccountStatus(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  597. w.Header().Set("Content-Type", "application/json")
  598. w.Header().Set("Access-Control-Allow-Origin", "*")
  599. w.Write(WrapData(a.CloudProvider.ServiceAccountStatus(), nil))
  600. }
  601. func (a *Accesses) GetPricingSourceStatus(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  602. w.Header().Set("Content-Type", "application/json")
  603. w.Header().Set("Access-Control-Allow-Origin", "*")
  604. w.Write(WrapData(a.CloudProvider.PricingSourceStatus(), nil))
  605. }
  606. func (a *Accesses) GetPricingSourceCounts(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  607. w.Header().Set("Content-Type", "application/json")
  608. w.Header().Set("Access-Control-Allow-Origin", "*")
  609. w.Write(WrapData(a.Model.GetPricingSourceCounts()))
  610. }
  611. func (a *Accesses) GetPricingSourceSummary(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
  612. w.Header().Set("Content-Type", "application/json")
  613. w.Header().Set("Access-Control-Allow-Origin", "*")
  614. data := a.CloudProvider.PricingSourceSummary()
  615. w.Write(WrapData(data, nil))
  616. }
  617. func (a *Accesses) GetPrometheusMetadata(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  618. w.Header().Set("Content-Type", "application/json")
  619. w.Header().Set("Access-Control-Allow-Origin", "*")
  620. w.Write(WrapData(prom.Validate(a.PrometheusClient)))
  621. }
  622. func (a *Accesses) PrometheusQuery(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  623. w.Header().Set("Content-Type", "application/json")
  624. w.Header().Set("Access-Control-Allow-Origin", "*")
  625. qp := httputil.NewQueryParams(r.URL.Query())
  626. query := qp.Get("query", "")
  627. if query == "" {
  628. w.Write(WrapData(nil, fmt.Errorf("Query Parameter 'query' is unset'")))
  629. return
  630. }
  631. // Attempt to parse time as either a unix timestamp or as an RFC3339 value
  632. var timeVal time.Time
  633. timeStr := qp.Get("time", "")
  634. if len(timeStr) > 0 {
  635. if t, err := strconv.ParseInt(timeStr, 10, 64); err == nil {
  636. timeVal = time.Unix(t, 0)
  637. } else if t, err := time.Parse(time.RFC3339, timeStr); err == nil {
  638. timeVal = t
  639. }
  640. // If time is given, but not parse-able, return an error
  641. if timeVal.IsZero() {
  642. http.Error(w, fmt.Sprintf("time must be a unix timestamp or RFC3339 value; illegal value given: %s", timeStr), http.StatusBadRequest)
  643. }
  644. }
  645. ctx := prom.NewNamedContext(a.PrometheusClient, prom.FrontendContextName)
  646. body, err := ctx.RawQuery(query, timeVal)
  647. if err != nil {
  648. w.Write(WrapData(nil, fmt.Errorf("Error running query %s. Error: %s", query, err)))
  649. return
  650. }
  651. w.Write(body)
  652. }
  653. func (a *Accesses) PrometheusQueryRange(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  654. w.Header().Set("Content-Type", "application/json")
  655. w.Header().Set("Access-Control-Allow-Origin", "*")
  656. qp := httputil.NewQueryParams(r.URL.Query())
  657. query := qp.Get("query", "")
  658. if query == "" {
  659. fmt.Fprintf(w, "Error parsing query from request parameters.")
  660. return
  661. }
  662. start, end, duration, err := toStartEndStep(qp)
  663. if err != nil {
  664. fmt.Fprintf(w, err.Error())
  665. return
  666. }
  667. ctx := prom.NewNamedContext(a.PrometheusClient, prom.FrontendContextName)
  668. body, err := ctx.RawQueryRange(query, start, end, duration)
  669. if err != nil {
  670. fmt.Fprintf(w, "Error running query %s. Error: %s", query, err)
  671. return
  672. }
  673. w.Write(body)
  674. }
  675. func (a *Accesses) ThanosQuery(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  676. w.Header().Set("Content-Type", "application/json")
  677. w.Header().Set("Access-Control-Allow-Origin", "*")
  678. if !thanos.IsEnabled() {
  679. w.Write(WrapData(nil, fmt.Errorf("ThanosDisabled")))
  680. return
  681. }
  682. qp := httputil.NewQueryParams(r.URL.Query())
  683. query := qp.Get("query", "")
  684. if query == "" {
  685. w.Write(WrapData(nil, fmt.Errorf("Query Parameter 'query' is unset'")))
  686. return
  687. }
  688. // Attempt to parse time as either a unix timestamp or as an RFC3339 value
  689. var timeVal time.Time
  690. timeStr := qp.Get("time", "")
  691. if len(timeStr) > 0 {
  692. if t, err := strconv.ParseInt(timeStr, 10, 64); err == nil {
  693. timeVal = time.Unix(t, 0)
  694. } else if t, err := time.Parse(time.RFC3339, timeStr); err == nil {
  695. timeVal = t
  696. }
  697. // If time is given, but not parse-able, return an error
  698. if timeVal.IsZero() {
  699. http.Error(w, fmt.Sprintf("time must be a unix timestamp or RFC3339 value; illegal value given: %s", timeStr), http.StatusBadRequest)
  700. }
  701. }
  702. ctx := prom.NewNamedContext(a.ThanosClient, prom.FrontendContextName)
  703. body, err := ctx.RawQuery(query, timeVal)
  704. if err != nil {
  705. w.Write(WrapData(nil, fmt.Errorf("Error running query %s. Error: %s", query, err)))
  706. return
  707. }
  708. w.Write(body)
  709. }
  710. func (a *Accesses) ThanosQueryRange(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  711. w.Header().Set("Content-Type", "application/json")
  712. w.Header().Set("Access-Control-Allow-Origin", "*")
  713. if !thanos.IsEnabled() {
  714. w.Write(WrapData(nil, fmt.Errorf("ThanosDisabled")))
  715. return
  716. }
  717. qp := httputil.NewQueryParams(r.URL.Query())
  718. query := qp.Get("query", "")
  719. if query == "" {
  720. fmt.Fprintf(w, "Error parsing query from request parameters.")
  721. return
  722. }
  723. start, end, duration, err := toStartEndStep(qp)
  724. if err != nil {
  725. fmt.Fprintf(w, err.Error())
  726. return
  727. }
  728. ctx := prom.NewNamedContext(a.ThanosClient, prom.FrontendContextName)
  729. body, err := ctx.RawQueryRange(query, start, end, duration)
  730. if err != nil {
  731. fmt.Fprintf(w, "Error running query %s. Error: %s", query, err)
  732. return
  733. }
  734. w.Write(body)
  735. }
  736. // helper for query range proxy requests
  737. func toStartEndStep(qp httputil.QueryParams) (start, end time.Time, step time.Duration, err error) {
  738. var e error
  739. ss := qp.Get("start", "")
  740. es := qp.Get("end", "")
  741. ds := qp.Get("duration", "")
  742. layout := "2006-01-02T15:04:05.000Z"
  743. start, e = time.Parse(layout, ss)
  744. if e != nil {
  745. err = fmt.Errorf("Error parsing time %s. Error: %s", ss, err)
  746. return
  747. }
  748. end, e = time.Parse(layout, es)
  749. if e != nil {
  750. err = fmt.Errorf("Error parsing time %s. Error: %s", es, err)
  751. return
  752. }
  753. step, e = time.ParseDuration(ds)
  754. if e != nil {
  755. err = fmt.Errorf("Error parsing duration %s. Error: %s", ds, err)
  756. return
  757. }
  758. err = nil
  759. return
  760. }
  761. func (a *Accesses) GetPrometheusQueueState(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  762. w.Header().Set("Content-Type", "application/json")
  763. w.Header().Set("Access-Control-Allow-Origin", "*")
  764. promQueueState, err := prom.GetPrometheusQueueState(a.PrometheusClient)
  765. if err != nil {
  766. w.Write(WrapData(nil, err))
  767. return
  768. }
  769. result := map[string]*prom.PrometheusQueueState{
  770. "prometheus": promQueueState,
  771. }
  772. if thanos.IsEnabled() {
  773. thanosQueueState, err := prom.GetPrometheusQueueState(a.ThanosClient)
  774. if err != nil {
  775. log.Warnf("Error getting Thanos queue state: %s", err)
  776. } else {
  777. result["thanos"] = thanosQueueState
  778. }
  779. }
  780. w.Write(WrapData(result, nil))
  781. }
  782. // GetPrometheusMetrics retrieves availability of Prometheus and Thanos metrics
  783. func (a *Accesses) GetPrometheusMetrics(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  784. w.Header().Set("Content-Type", "application/json")
  785. w.Header().Set("Access-Control-Allow-Origin", "*")
  786. promMetrics := prom.GetPrometheusMetrics(a.PrometheusClient, "")
  787. result := map[string][]*prom.PrometheusDiagnostic{
  788. "prometheus": promMetrics,
  789. }
  790. if thanos.IsEnabled() {
  791. thanosMetrics := prom.GetPrometheusMetrics(a.ThanosClient, thanos.QueryOffset())
  792. result["thanos"] = thanosMetrics
  793. }
  794. w.Write(WrapData(result, nil))
  795. }
  796. func (a *Accesses) GetAllPersistentVolumes(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  797. w.Header().Set("Content-Type", "application/json")
  798. w.Header().Set("Access-Control-Allow-Origin", "*")
  799. pvList := a.ClusterCache.GetAllPersistentVolumes()
  800. body, err := json.Marshal(wrapAsObjectItems(pvList))
  801. if err != nil {
  802. fmt.Fprintf(w, "Error decoding persistent volumes: "+err.Error())
  803. } else {
  804. w.Write(body)
  805. }
  806. }
  807. func (a *Accesses) GetAllDeployments(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  808. w.Header().Set("Content-Type", "application/json")
  809. w.Header().Set("Access-Control-Allow-Origin", "*")
  810. qp := httputil.NewQueryParams(r.URL.Query())
  811. namespace := qp.Get("namespace", "")
  812. deploymentsList := a.ClusterCache.GetAllDeployments()
  813. // filter for provided namespace
  814. var deployments []*appsv1.Deployment
  815. if namespace == "" {
  816. deployments = deploymentsList
  817. } else {
  818. deployments = []*appsv1.Deployment{}
  819. for _, d := range deploymentsList {
  820. if d.Namespace == namespace {
  821. deployments = append(deployments, d)
  822. }
  823. }
  824. }
  825. body, err := json.Marshal(wrapAsObjectItems(deployments))
  826. if err != nil {
  827. fmt.Fprintf(w, "Error decoding deployment: "+err.Error())
  828. } else {
  829. w.Write(body)
  830. }
  831. }
  832. func (a *Accesses) GetAllStorageClasses(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  833. w.Header().Set("Content-Type", "application/json")
  834. w.Header().Set("Access-Control-Allow-Origin", "*")
  835. scList := a.ClusterCache.GetAllStorageClasses()
  836. body, err := json.Marshal(wrapAsObjectItems(scList))
  837. if err != nil {
  838. fmt.Fprintf(w, "Error decoding storageclasses: "+err.Error())
  839. } else {
  840. w.Write(body)
  841. }
  842. }
  843. func (a *Accesses) GetAllStatefulSets(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  844. w.Header().Set("Content-Type", "application/json")
  845. w.Header().Set("Access-Control-Allow-Origin", "*")
  846. qp := httputil.NewQueryParams(r.URL.Query())
  847. namespace := qp.Get("namespace", "")
  848. statefulSetsList := a.ClusterCache.GetAllStatefulSets()
  849. // filter for provided namespace
  850. var statefulSets []*appsv1.StatefulSet
  851. if namespace == "" {
  852. statefulSets = statefulSetsList
  853. } else {
  854. statefulSets = []*appsv1.StatefulSet{}
  855. for _, ss := range statefulSetsList {
  856. if ss.Namespace == namespace {
  857. statefulSets = append(statefulSets, ss)
  858. }
  859. }
  860. }
  861. body, err := json.Marshal(wrapAsObjectItems(statefulSets))
  862. if err != nil {
  863. fmt.Fprintf(w, "Error decoding deployment: "+err.Error())
  864. } else {
  865. w.Write(body)
  866. }
  867. }
  868. func (a *Accesses) GetAllNodes(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  869. w.Header().Set("Content-Type", "application/json")
  870. w.Header().Set("Access-Control-Allow-Origin", "*")
  871. nodeList := a.ClusterCache.GetAllNodes()
  872. body, err := json.Marshal(wrapAsObjectItems(nodeList))
  873. if err != nil {
  874. fmt.Fprintf(w, "Error decoding nodes: "+err.Error())
  875. } else {
  876. w.Write(body)
  877. }
  878. }
  879. func (a *Accesses) GetAllPods(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  880. w.Header().Set("Content-Type", "application/json")
  881. w.Header().Set("Access-Control-Allow-Origin", "*")
  882. podlist := a.ClusterCache.GetAllPods()
  883. body, err := json.Marshal(wrapAsObjectItems(podlist))
  884. if err != nil {
  885. fmt.Fprintf(w, "Error decoding pods: "+err.Error())
  886. } else {
  887. w.Write(body)
  888. }
  889. }
  890. func (a *Accesses) GetAllNamespaces(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  891. w.Header().Set("Content-Type", "application/json")
  892. w.Header().Set("Access-Control-Allow-Origin", "*")
  893. namespaces := a.ClusterCache.GetAllNamespaces()
  894. body, err := json.Marshal(wrapAsObjectItems(namespaces))
  895. if err != nil {
  896. fmt.Fprintf(w, "Error decoding deployment: "+err.Error())
  897. } else {
  898. w.Write(body)
  899. }
  900. }
  901. func (a *Accesses) GetAllDaemonSets(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  902. w.Header().Set("Content-Type", "application/json")
  903. w.Header().Set("Access-Control-Allow-Origin", "*")
  904. daemonSets := a.ClusterCache.GetAllDaemonSets()
  905. body, err := json.Marshal(wrapAsObjectItems(daemonSets))
  906. if err != nil {
  907. fmt.Fprintf(w, "Error decoding daemon set: "+err.Error())
  908. } else {
  909. w.Write(body)
  910. }
  911. }
  912. func (a *Accesses) GetPod(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  913. w.Header().Set("Content-Type", "application/json")
  914. w.Header().Set("Access-Control-Allow-Origin", "*")
  915. podName := ps.ByName("name")
  916. podNamespace := ps.ByName("namespace")
  917. // TODO: ClusterCache API could probably afford to have some better filtering
  918. allPods := a.ClusterCache.GetAllPods()
  919. for _, pod := range allPods {
  920. for _, container := range pod.Spec.Containers {
  921. container.Env = make([]v1.EnvVar, 0)
  922. }
  923. if pod.Namespace == podNamespace && pod.Name == podName {
  924. body, err := json.Marshal(pod)
  925. if err != nil {
  926. fmt.Fprintf(w, "Error decoding pod: "+err.Error())
  927. } else {
  928. w.Write(body)
  929. }
  930. return
  931. }
  932. }
  933. fmt.Fprintf(w, "Pod not found\n")
  934. }
  935. func (a *Accesses) PrometheusRecordingRules(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  936. w.Header().Set("Content-Type", "application/json")
  937. w.Header().Set("Access-Control-Allow-Origin", "*")
  938. u := a.PrometheusClient.URL(epRules, nil)
  939. req, err := http.NewRequest(http.MethodGet, u.String(), nil)
  940. if err != nil {
  941. fmt.Fprintf(w, "Error creating Prometheus rule request: "+err.Error())
  942. }
  943. _, body, err := a.PrometheusClient.Do(r.Context(), req)
  944. if err != nil {
  945. fmt.Fprintf(w, "Error making Prometheus rule request: "+err.Error())
  946. } else {
  947. w.Write(body)
  948. }
  949. }
  950. func (a *Accesses) PrometheusConfig(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  951. w.Header().Set("Content-Type", "application/json")
  952. w.Header().Set("Access-Control-Allow-Origin", "*")
  953. pConfig := map[string]string{
  954. "address": env.GetPrometheusServerEndpoint(),
  955. }
  956. body, err := json.Marshal(pConfig)
  957. if err != nil {
  958. fmt.Fprintf(w, "Error marshalling prometheus config")
  959. } else {
  960. w.Write(body)
  961. }
  962. }
  963. func (a *Accesses) PrometheusTargets(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  964. w.Header().Set("Content-Type", "application/json")
  965. w.Header().Set("Access-Control-Allow-Origin", "*")
  966. u := a.PrometheusClient.URL(epTargets, nil)
  967. req, err := http.NewRequest(http.MethodGet, u.String(), nil)
  968. if err != nil {
  969. fmt.Fprintf(w, "Error creating Prometheus rule request: "+err.Error())
  970. }
  971. _, body, err := a.PrometheusClient.Do(r.Context(), req)
  972. if err != nil {
  973. fmt.Fprintf(w, "Error making Prometheus rule request: "+err.Error())
  974. } else {
  975. w.Write(body)
  976. }
  977. }
  978. func (a *Accesses) GetOrphanedPods(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  979. w.Header().Set("Content-Type", "application/json")
  980. w.Header().Set("Access-Control-Allow-Origin", "*")
  981. podlist := a.ClusterCache.GetAllPods()
  982. var lonePods []*v1.Pod
  983. for _, pod := range podlist {
  984. if len(pod.OwnerReferences) == 0 {
  985. lonePods = append(lonePods, pod)
  986. }
  987. }
  988. body, err := json.Marshal(lonePods)
  989. if err != nil {
  990. fmt.Fprintf(w, "Error decoding pod: "+err.Error())
  991. } else {
  992. w.Write(body)
  993. }
  994. }
  995. func (a *Accesses) GetInstallNamespace(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  996. w.Header().Set("Content-Type", "application/json")
  997. w.Header().Set("Access-Control-Allow-Origin", "*")
  998. ns := env.GetKubecostNamespace()
  999. w.Write([]byte(ns))
  1000. }
  1001. type InstallInfo struct {
  1002. Containers []ContainerInfo `json:"containers"`
  1003. ClusterInfo map[string]string `json:"clusterInfo"`
  1004. Version string `json:"version"`
  1005. }
  1006. type ContainerInfo struct {
  1007. ContainerName string `json:"containerName"`
  1008. Image string `json:"image"`
  1009. ImageID string `json:"imageID"`
  1010. StartTime string `json:"startTime"`
  1011. Restarts int32 `json:"restarts"`
  1012. }
  1013. func (a *Accesses) GetInstallInfo(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  1014. w.Header().Set("Content-Type", "application/json")
  1015. w.Header().Set("Access-Control-Allow-Origin", "*")
  1016. pods, err := a.KubeClientSet.CoreV1().Pods(env.GetKubecostNamespace()).List(context.Background(), metav1.ListOptions{
  1017. LabelSelector: "app=cost-analyzer",
  1018. FieldSelector: "status.phase=Running",
  1019. Limit: 1,
  1020. })
  1021. if err != nil {
  1022. writeErrorResponse(w, 500, fmt.Sprintf("Unable to list pods: %s", err.Error()))
  1023. return
  1024. }
  1025. info := InstallInfo{
  1026. ClusterInfo: make(map[string]string),
  1027. Version: version.FriendlyVersion(),
  1028. }
  1029. // If we have zero pods either something is weird with the install since the app selector is not exposed in the helm
  1030. // chart or more likely we are running locally - in either case Images field will return as null
  1031. if len(pods.Items) > 0 {
  1032. for _, pod := range pods.Items {
  1033. for _, container := range pod.Status.ContainerStatuses {
  1034. c := ContainerInfo{
  1035. ContainerName: container.Name,
  1036. Image: container.Image,
  1037. ImageID: container.ImageID,
  1038. StartTime: pod.Status.StartTime.String(),
  1039. Restarts: container.RestartCount,
  1040. }
  1041. info.Containers = append(info.Containers, c)
  1042. }
  1043. }
  1044. }
  1045. nodes := a.ClusterCache.GetAllNodes()
  1046. cachePods := a.ClusterCache.GetAllPods()
  1047. info.ClusterInfo["nodeCount"] = strconv.Itoa(len(nodes))
  1048. info.ClusterInfo["podCount"] = strconv.Itoa(len(cachePods))
  1049. body, err := json.Marshal(info)
  1050. if err != nil {
  1051. writeErrorResponse(w, 500, fmt.Sprintf("Error decoding pod: %s", err.Error()))
  1052. return
  1053. }
  1054. w.Write(body)
  1055. }
  1056. // logsFor pulls the logs for a specific pod, namespace, and container
  1057. func logsFor(c kubernetes.Interface, namespace string, pod string, container string, dur time.Duration, ctx context.Context) (string, error) {
  1058. since := time.Now().UTC().Add(-dur)
  1059. logOpts := v1.PodLogOptions{
  1060. SinceTime: &metav1.Time{Time: since},
  1061. }
  1062. if container != "" {
  1063. logOpts.Container = container
  1064. }
  1065. req := c.CoreV1().Pods(namespace).GetLogs(pod, &logOpts)
  1066. reader, err := req.Stream(ctx)
  1067. if err != nil {
  1068. return "", err
  1069. }
  1070. podLogs, err := io.ReadAll(reader)
  1071. if err != nil {
  1072. return "", err
  1073. }
  1074. // If color is already disabled then we don't need to process the logs
  1075. // to drop ANSI colors
  1076. if !viper.GetBool("disable-log-color") {
  1077. podLogs = ANSIRegex.ReplaceAll(podLogs, []byte{})
  1078. }
  1079. return string(podLogs), nil
  1080. }
  1081. func (a *Accesses) GetPodLogs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  1082. w.Header().Set("Content-Type", "application/json")
  1083. w.Header().Set("Access-Control-Allow-Origin", "*")
  1084. qp := httputil.NewQueryParams(r.URL.Query())
  1085. ns := qp.Get("namespace", env.GetKubecostNamespace())
  1086. pod := qp.Get("pod", "")
  1087. selector := qp.Get("selector", "")
  1088. container := qp.Get("container", "")
  1089. since := qp.Get("since", "24h")
  1090. sinceDuration, err := time.ParseDuration(since)
  1091. if err != nil {
  1092. fmt.Fprintf(w, "Invalid Duration String: "+err.Error())
  1093. return
  1094. }
  1095. var logResult string
  1096. appendLog := func(ns string, pod string, container string, l string) {
  1097. if l == "" {
  1098. return
  1099. }
  1100. logResult += fmt.Sprintf("%s\n| %s:%s:%s\n%s\n%s\n\n", LogSeparator, ns, pod, container, LogSeparator, l)
  1101. }
  1102. if pod != "" {
  1103. pd, err := a.KubeClientSet.CoreV1().Pods(ns).Get(r.Context(), pod, metav1.GetOptions{})
  1104. if err != nil {
  1105. fmt.Fprintf(w, "Error Finding Pod: "+err.Error())
  1106. return
  1107. }
  1108. if container != "" {
  1109. var foundContainer bool
  1110. for _, cont := range pd.Spec.Containers {
  1111. if strings.EqualFold(cont.Name, container) {
  1112. foundContainer = true
  1113. break
  1114. }
  1115. }
  1116. if !foundContainer {
  1117. fmt.Fprintf(w, "Could not find container: "+container)
  1118. return
  1119. }
  1120. }
  1121. logs, err := logsFor(a.KubeClientSet, ns, pod, container, sinceDuration, r.Context())
  1122. if err != nil {
  1123. fmt.Fprintf(w, "Error Getting Logs: "+err.Error())
  1124. return
  1125. }
  1126. appendLog(ns, pod, container, logs)
  1127. w.Write([]byte(logResult))
  1128. return
  1129. }
  1130. if selector != "" {
  1131. pods, err := a.KubeClientSet.CoreV1().Pods(ns).List(r.Context(), metav1.ListOptions{LabelSelector: selector})
  1132. if err != nil {
  1133. fmt.Fprintf(w, "Error Finding Pod: "+err.Error())
  1134. return
  1135. }
  1136. for _, pd := range pods.Items {
  1137. for _, cont := range pd.Spec.Containers {
  1138. logs, err := logsFor(a.KubeClientSet, ns, pd.Name, cont.Name, sinceDuration, r.Context())
  1139. if err != nil {
  1140. continue
  1141. }
  1142. appendLog(ns, pd.Name, cont.Name, logs)
  1143. }
  1144. }
  1145. }
  1146. w.Write([]byte(logResult))
  1147. }
  1148. func (a *Accesses) AddServiceKey(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  1149. w.Header().Set("Content-Type", "application/json")
  1150. w.Header().Set("Access-Control-Allow-Origin", "*")
  1151. r.ParseForm()
  1152. key := r.PostForm.Get("key")
  1153. k := []byte(key)
  1154. err := os.WriteFile(path.Join(env.GetConfigPathWithDefault(env.DefaultConfigMountPath), "key.json"), k, 0644)
  1155. if err != nil {
  1156. fmt.Fprintf(w, "Error writing service key: "+err.Error())
  1157. }
  1158. w.WriteHeader(http.StatusOK)
  1159. }
  1160. func (a *Accesses) GetHelmValues(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  1161. w.Header().Set("Content-Type", "application/json")
  1162. w.Header().Set("Access-Control-Allow-Origin", "*")
  1163. encodedValues := env.Get("HELM_VALUES", "")
  1164. if encodedValues == "" {
  1165. fmt.Fprintf(w, "Values reporting disabled")
  1166. return
  1167. }
  1168. result, err := base64.StdEncoding.DecodeString(encodedValues)
  1169. if err != nil {
  1170. fmt.Fprintf(w, "Failed to decode encoded values: %s", err)
  1171. return
  1172. }
  1173. w.Write(result)
  1174. }
  1175. func (a *Accesses) Status(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  1176. w.Header().Set("Content-Type", "application/json")
  1177. w.Header().Set("Access-Control-Allow-Origin", "*")
  1178. promServer := env.GetPrometheusServerEndpoint()
  1179. api := prometheusAPI.NewAPI(a.PrometheusClient)
  1180. result, err := api.Config(r.Context())
  1181. if err != nil {
  1182. fmt.Fprintf(w, "Using Prometheus at "+promServer+". Error: "+err.Error())
  1183. } else {
  1184. fmt.Fprintf(w, "Using Prometheus at "+promServer+". PrometheusConfig: "+result.YAML)
  1185. }
  1186. }
  1187. type LogLevelRequestResponse struct {
  1188. Level string `json:"level"`
  1189. }
  1190. func (a *Accesses) GetLogLevel(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  1191. w.Header().Set("Content-Type", "application/json")
  1192. w.Header().Set("Access-Control-Allow-Origin", "*")
  1193. level := log.GetLogLevel()
  1194. llrr := LogLevelRequestResponse{
  1195. Level: level,
  1196. }
  1197. body, err := json.Marshal(llrr)
  1198. if err != nil {
  1199. http.Error(w, fmt.Sprintf("unable to retrive log level"), http.StatusInternalServerError)
  1200. return
  1201. }
  1202. _, err = w.Write(body)
  1203. if err != nil {
  1204. http.Error(w, fmt.Sprintf("unable to write response: %s", body), http.StatusInternalServerError)
  1205. return
  1206. }
  1207. }
  1208. func (a *Accesses) SetLogLevel(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  1209. params := LogLevelRequestResponse{}
  1210. err := json.NewDecoder(r.Body).Decode(&params)
  1211. if err != nil {
  1212. http.Error(w, fmt.Sprintf("unable to decode request body, error: %s", err), http.StatusBadRequest)
  1213. return
  1214. }
  1215. err = log.SetLogLevel(params.Level)
  1216. if err != nil {
  1217. http.Error(w, fmt.Sprintf("level must be a valid log level according to zerolog; level given: %s, error: %s", params.Level, err), http.StatusBadRequest)
  1218. return
  1219. }
  1220. w.WriteHeader(http.StatusOK)
  1221. }
  1222. // captures the panic event in sentry
  1223. func capturePanicEvent(err string, stack string) {
  1224. msg := fmt.Sprintf("Panic: %s\nStackTrace: %s\n", err, stack)
  1225. log.Infof(msg)
  1226. sentry.CurrentHub().CaptureEvent(&sentry.Event{
  1227. Level: sentry.LevelError,
  1228. Message: msg,
  1229. })
  1230. sentry.Flush(5 * time.Second)
  1231. }
  1232. // handle any panics reported by the errors package
  1233. func handlePanic(p errors.Panic) bool {
  1234. err := p.Error
  1235. if err != nil {
  1236. if err, ok := err.(error); ok {
  1237. capturePanicEvent(err.Error(), p.Stack)
  1238. }
  1239. if err, ok := err.(string); ok {
  1240. capturePanicEvent(err, p.Stack)
  1241. }
  1242. }
  1243. // Return true to recover iff the type is http, otherwise allow kubernetes
  1244. // to recover.
  1245. return p.Type == errors.PanicTypeHTTP
  1246. }
  1247. func Initialize(additionalConfigWatchers ...*watcher.ConfigMapWatcher) *Accesses {
  1248. configWatchers := watcher.NewConfigMapWatchers(additionalConfigWatchers...)
  1249. var err error
  1250. if errorReportingEnabled {
  1251. err = sentry.Init(sentry.ClientOptions{Release: version.FriendlyVersion()})
  1252. if err != nil {
  1253. log.Infof("Failed to initialize sentry for error reporting")
  1254. } else {
  1255. err = errors.SetPanicHandler(handlePanic)
  1256. if err != nil {
  1257. log.Infof("Failed to set panic handler: %s", err)
  1258. }
  1259. }
  1260. }
  1261. address := env.GetPrometheusServerEndpoint()
  1262. if address == "" {
  1263. log.Fatalf("No address for prometheus set in $%s. Aborting.", env.PrometheusServerEndpointEnvVar)
  1264. }
  1265. queryConcurrency := env.GetMaxQueryConcurrency()
  1266. log.Infof("Prometheus/Thanos Client Max Concurrency set to %d", queryConcurrency)
  1267. timeout := 120 * time.Second
  1268. keepAlive := 120 * time.Second
  1269. tlsHandshakeTimeout := 10 * time.Second
  1270. scrapeInterval := time.Minute
  1271. var rateLimitRetryOpts *prom.RateLimitRetryOpts = nil
  1272. if env.IsPrometheusRetryOnRateLimitResponse() {
  1273. rateLimitRetryOpts = &prom.RateLimitRetryOpts{
  1274. MaxRetries: env.GetPrometheusRetryOnRateLimitMaxRetries(),
  1275. DefaultRetryWait: env.GetPrometheusRetryOnRateLimitDefaultWait(),
  1276. }
  1277. }
  1278. promCli, err := prom.NewPrometheusClient(address, &prom.PrometheusClientConfig{
  1279. Timeout: timeout,
  1280. KeepAlive: keepAlive,
  1281. TLSHandshakeTimeout: tlsHandshakeTimeout,
  1282. TLSInsecureSkipVerify: env.GetInsecureSkipVerify(),
  1283. RateLimitRetryOpts: rateLimitRetryOpts,
  1284. Auth: &prom.ClientAuth{
  1285. Username: env.GetDBBasicAuthUsername(),
  1286. Password: env.GetDBBasicAuthUserPassword(),
  1287. BearerToken: env.GetDBBearerToken(),
  1288. },
  1289. QueryConcurrency: queryConcurrency,
  1290. QueryLogFile: "",
  1291. HeaderXScopeOrgId: env.GetPrometheusHeaderXScopeOrgId(),
  1292. })
  1293. if err != nil {
  1294. log.Fatalf("Failed to create prometheus client, Error: %v", err)
  1295. }
  1296. m, err := prom.Validate(promCli)
  1297. if err != nil || !m.Running {
  1298. if err != nil {
  1299. log.Errorf("Failed to query prometheus at %s. Error: %s . Troubleshooting help available at: %s", address, err.Error(), prom.PrometheusTroubleshootingURL)
  1300. } else if !m.Running {
  1301. log.Errorf("Prometheus at %s is not running. Troubleshooting help available at: %s", address, prom.PrometheusTroubleshootingURL)
  1302. }
  1303. } else {
  1304. log.Infof("Success: retrieved the 'up' query against prometheus at: " + address)
  1305. }
  1306. api := prometheusAPI.NewAPI(promCli)
  1307. _, err = api.Config(context.Background())
  1308. if err != nil {
  1309. log.Infof("No valid prometheus config file at %s. Error: %s . Troubleshooting help available at: %s. Ignore if using cortex/mimir/thanos here.", address, err.Error(), prom.PrometheusTroubleshootingURL)
  1310. } else {
  1311. log.Infof("Retrieved a prometheus config file from: %s", address)
  1312. }
  1313. // Lookup scrape interval for kubecost job, update if found
  1314. si, err := prom.ScrapeIntervalFor(promCli, env.GetKubecostJobName())
  1315. if err == nil {
  1316. scrapeInterval = si
  1317. }
  1318. log.Infof("Using scrape interval of %f", scrapeInterval.Seconds())
  1319. // Kubernetes API setup
  1320. kubeClientset, err := kubeconfig.LoadKubeClient("")
  1321. if err != nil {
  1322. log.Fatalf("Failed to build Kubernetes client: %s", err.Error())
  1323. }
  1324. // Create ConfigFileManager for synchronization of shared configuration
  1325. confManager := config.NewConfigFileManager(&config.ConfigFileManagerOpts{
  1326. BucketStoreConfig: env.GetKubecostConfigBucket(),
  1327. LocalConfigPath: "/",
  1328. })
  1329. configPrefix := env.GetConfigPathWithDefault("/var/configs/")
  1330. // Create Kubernetes Cluster Cache + Watchers
  1331. var k8sCache clustercache.ClusterCache
  1332. if env.IsClusterCacheFileEnabled() {
  1333. importLocation := confManager.ConfigFileAt(path.Join(configPrefix, "cluster-cache.json"))
  1334. k8sCache = clustercache.NewClusterImporter(importLocation)
  1335. } else {
  1336. k8sCache = clustercache.NewKubernetesClusterCache(kubeClientset)
  1337. }
  1338. k8sCache.Run()
  1339. cloudProviderKey := env.GetCloudProviderAPIKey()
  1340. cloudProvider, err := provider.NewProvider(k8sCache, cloudProviderKey, confManager)
  1341. if err != nil {
  1342. panic(err.Error())
  1343. }
  1344. // Append the pricing config watcher
  1345. configWatchers.AddWatcher(provider.ConfigWatcherFor(cloudProvider))
  1346. configWatchers.AddWatcher(metrics.GetMetricsConfigWatcher())
  1347. watchConfigFunc := configWatchers.ToWatchFunc()
  1348. watchedConfigs := configWatchers.GetWatchedConfigs()
  1349. kubecostNamespace := env.GetKubecostNamespace()
  1350. // We need an initial invocation because the init of the cache has happened before we had access to the provider.
  1351. for _, cw := range watchedConfigs {
  1352. configs, err := kubeClientset.CoreV1().ConfigMaps(kubecostNamespace).Get(context.Background(), cw, metav1.GetOptions{})
  1353. if err != nil {
  1354. log.Infof("No %s configmap found at install time, using existing configs: %s", cw, err.Error())
  1355. } else {
  1356. log.Infof("Found configmap %s, watching...", configs.Name)
  1357. watchConfigFunc(configs)
  1358. }
  1359. }
  1360. k8sCache.SetConfigMapUpdateFunc(watchConfigFunc)
  1361. remoteEnabled := env.IsRemoteEnabled()
  1362. if remoteEnabled {
  1363. info, err := cloudProvider.ClusterInfo()
  1364. log.Infof("Saving cluster with id:'%s', and name:'%s' to durable storage", info["id"], info["name"])
  1365. if err != nil {
  1366. log.Infof("Error saving cluster id %s", err.Error())
  1367. }
  1368. _, _, err = utils.GetOrCreateClusterMeta(info["id"], info["name"])
  1369. if err != nil {
  1370. log.Infof("Unable to set cluster id '%s' for cluster '%s', %s", info["id"], info["name"], err.Error())
  1371. }
  1372. }
  1373. // Thanos Client
  1374. var thanosClient prometheus.Client
  1375. if thanos.IsEnabled() {
  1376. thanosAddress := thanos.QueryURL()
  1377. if thanosAddress != "" {
  1378. thanosCli, _ := thanos.NewThanosClient(thanosAddress, &prom.PrometheusClientConfig{
  1379. Timeout: timeout,
  1380. KeepAlive: keepAlive,
  1381. TLSHandshakeTimeout: tlsHandshakeTimeout,
  1382. TLSInsecureSkipVerify: env.GetInsecureSkipVerify(),
  1383. RateLimitRetryOpts: rateLimitRetryOpts,
  1384. Auth: &prom.ClientAuth{
  1385. Username: env.GetMultiClusterBasicAuthUsername(),
  1386. Password: env.GetMultiClusterBasicAuthPassword(),
  1387. BearerToken: env.GetMultiClusterBearerToken(),
  1388. },
  1389. QueryConcurrency: queryConcurrency,
  1390. QueryLogFile: env.GetQueryLoggingFile(),
  1391. })
  1392. _, err = prom.Validate(thanosCli)
  1393. if err != nil {
  1394. log.Warnf("Failed to query Thanos at %s. Error: %s.", thanosAddress, err.Error())
  1395. thanosClient = thanosCli
  1396. } else {
  1397. log.Infof("Success: retrieved the 'up' query against Thanos at: " + thanosAddress)
  1398. thanosClient = thanosCli
  1399. }
  1400. } else {
  1401. log.Infof("Error resolving environment variable: $%s", env.ThanosQueryUrlEnvVar)
  1402. }
  1403. }
  1404. // ClusterInfo Provider to provide the cluster map with local and remote cluster data
  1405. var clusterInfoProvider clusters.ClusterInfoProvider
  1406. if env.IsClusterInfoFileEnabled() {
  1407. clusterInfoFile := confManager.ConfigFileAt(path.Join(configPrefix, "cluster-info.json"))
  1408. clusterInfoProvider = NewConfiguredClusterInfoProvider(clusterInfoFile)
  1409. } else {
  1410. clusterInfoProvider = NewLocalClusterInfoProvider(kubeClientset, cloudProvider)
  1411. }
  1412. // Initialize ClusterMap for maintaining ClusterInfo by ClusterID
  1413. var clusterMap clusters.ClusterMap
  1414. if thanosClient != nil {
  1415. clusterMap = clusters.NewClusterMap(thanosClient, clusterInfoProvider, 10*time.Minute)
  1416. } else {
  1417. clusterMap = clusters.NewClusterMap(promCli, clusterInfoProvider, 5*time.Minute)
  1418. }
  1419. // cache responses from model and aggregation for a default of 10 minutes;
  1420. // clear expired responses every 20 minutes
  1421. aggregateCache := cache.New(time.Minute*10, time.Minute*20)
  1422. costDataCache := cache.New(time.Minute*10, time.Minute*20)
  1423. clusterCostsCache := cache.New(cache.NoExpiration, cache.NoExpiration)
  1424. outOfClusterCache := cache.New(time.Minute*5, time.Minute*10)
  1425. settingsCache := cache.New(cache.NoExpiration, cache.NoExpiration)
  1426. // query durations that should be cached longer should be registered here
  1427. // use relatively prime numbers to minimize likelihood of synchronized
  1428. // attempts at cache warming
  1429. day := 24 * time.Hour
  1430. cacheExpiration := map[time.Duration]time.Duration{
  1431. day: maxCacheMinutes1d * time.Minute,
  1432. 2 * day: maxCacheMinutes2d * time.Minute,
  1433. 7 * day: maxCacheMinutes7d * time.Minute,
  1434. 30 * day: maxCacheMinutes30d * time.Minute,
  1435. }
  1436. var pc prometheus.Client
  1437. if thanosClient != nil {
  1438. pc = thanosClient
  1439. } else {
  1440. pc = promCli
  1441. }
  1442. costModel := NewCostModel(pc, cloudProvider, k8sCache, clusterMap, scrapeInterval)
  1443. metricsEmitter := NewCostModelMetricsEmitter(promCli, k8sCache, cloudProvider, clusterInfoProvider, costModel)
  1444. a := &Accesses{
  1445. Router: httprouter.New(),
  1446. PrometheusClient: promCli,
  1447. ThanosClient: thanosClient,
  1448. KubeClientSet: kubeClientset,
  1449. ClusterCache: k8sCache,
  1450. ClusterMap: clusterMap,
  1451. CloudProvider: cloudProvider,
  1452. CloudConfigController: cloudconfig.NewController(cloudProvider),
  1453. ConfigFileManager: confManager,
  1454. ClusterInfoProvider: clusterInfoProvider,
  1455. Model: costModel,
  1456. MetricsEmitter: metricsEmitter,
  1457. AggregateCache: aggregateCache,
  1458. CostDataCache: costDataCache,
  1459. ClusterCostsCache: clusterCostsCache,
  1460. OutOfClusterCache: outOfClusterCache,
  1461. SettingsCache: settingsCache,
  1462. CacheExpiration: cacheExpiration,
  1463. httpServices: services.NewCostModelServices(),
  1464. }
  1465. // Use the Accesses instance, itself, as the CostModelAggregator. This is
  1466. // confusing and unconventional, but necessary so that we can swap it
  1467. // out for the ETL-adapted version elsewhere.
  1468. // TODO clean this up once ETL is open-sourced.
  1469. a.AggAPI = a
  1470. // Initialize mechanism for subscribing to settings changes
  1471. a.InitializeSettingsPubSub()
  1472. err = a.CloudProvider.DownloadPricingData()
  1473. if err != nil {
  1474. log.Infof("Failed to download pricing data: " + err.Error())
  1475. }
  1476. // Warm the aggregate cache unless explicitly set to false
  1477. if env.IsCacheWarmingEnabled() {
  1478. log.Infof("Init: AggregateCostModel cache warming enabled")
  1479. a.warmAggregateCostModelCache()
  1480. } else {
  1481. log.Infof("Init: AggregateCostModel cache warming disabled")
  1482. }
  1483. if !env.IsKubecostMetricsPodEnabled() {
  1484. a.MetricsEmitter.Start()
  1485. }
  1486. a.Router.GET("/costDataModel", a.CostDataModel)
  1487. a.Router.GET("/costDataModelRange", a.CostDataModelRange)
  1488. a.Router.GET("/aggregatedCostModel", a.AggregateCostModelHandler)
  1489. a.Router.GET("/allocation/compute", a.ComputeAllocationHandler)
  1490. a.Router.GET("/allocation/compute/summary", a.ComputeAllocationHandlerSummary)
  1491. a.Router.GET("/allNodePricing", a.GetAllNodePricing)
  1492. a.Router.POST("/refreshPricing", a.RefreshPricingData)
  1493. a.Router.GET("/clusterCostsOverTime", a.ClusterCostsOverTime)
  1494. a.Router.GET("/clusterCosts", a.ClusterCosts)
  1495. a.Router.GET("/clusterCostsFromCache", a.ClusterCostsFromCacheHandler)
  1496. a.Router.GET("/validatePrometheus", a.GetPrometheusMetadata)
  1497. a.Router.GET("/managementPlatform", a.ManagementPlatform)
  1498. a.Router.GET("/clusterInfo", a.ClusterInfo)
  1499. a.Router.GET("/clusterInfoMap", a.GetClusterInfoMap)
  1500. a.Router.GET("/serviceAccountStatus", a.GetServiceAccountStatus)
  1501. a.Router.GET("/pricingSourceStatus", a.GetPricingSourceStatus)
  1502. a.Router.GET("/pricingSourceSummary", a.GetPricingSourceSummary)
  1503. a.Router.GET("/pricingSourceCounts", a.GetPricingSourceCounts)
  1504. // endpoints migrated from server
  1505. a.Router.GET("/allPersistentVolumes", a.GetAllPersistentVolumes)
  1506. a.Router.GET("/allDeployments", a.GetAllDeployments)
  1507. a.Router.GET("/allStorageClasses", a.GetAllStorageClasses)
  1508. a.Router.GET("/allStatefulSets", a.GetAllStatefulSets)
  1509. a.Router.GET("/allNodes", a.GetAllNodes)
  1510. a.Router.GET("/allPods", a.GetAllPods)
  1511. a.Router.GET("/allNamespaces", a.GetAllNamespaces)
  1512. a.Router.GET("/allDaemonSets", a.GetAllDaemonSets)
  1513. a.Router.GET("/pod/:namespace/:name", a.GetPod)
  1514. a.Router.GET("/prometheusRecordingRules", a.PrometheusRecordingRules)
  1515. a.Router.GET("/prometheusConfig", a.PrometheusConfig)
  1516. a.Router.GET("/prometheusTargets", a.PrometheusTargets)
  1517. a.Router.GET("/orphanedPods", a.GetOrphanedPods)
  1518. a.Router.GET("/installNamespace", a.GetInstallNamespace)
  1519. a.Router.GET("/installInfo", a.GetInstallInfo)
  1520. a.Router.GET("/podLogs", a.GetPodLogs)
  1521. a.Router.POST("/serviceKey", a.AddServiceKey)
  1522. a.Router.GET("/helmValues", a.GetHelmValues)
  1523. a.Router.GET("/status", a.Status)
  1524. // prom query proxies
  1525. a.Router.GET("/prometheusQuery", a.PrometheusQuery)
  1526. a.Router.GET("/prometheusQueryRange", a.PrometheusQueryRange)
  1527. a.Router.GET("/thanosQuery", a.ThanosQuery)
  1528. a.Router.GET("/thanosQueryRange", a.ThanosQueryRange)
  1529. // diagnostics
  1530. a.Router.GET("/diagnostics/requestQueue", a.GetPrometheusQueueState)
  1531. a.Router.GET("/diagnostics/prometheusMetrics", a.GetPrometheusMetrics)
  1532. a.Router.GET("/logs/level", a.GetLogLevel)
  1533. a.Router.POST("/logs/level", a.SetLogLevel)
  1534. a.Router.GET("/cloud/config/export", a.CloudConfigController.GetExportConfigHandler())
  1535. a.Router.GET("/cloud/config/enable", a.CloudConfigController.GetEnableConfigHandler())
  1536. a.Router.GET("/cloud/config/disable", a.CloudConfigController.GetDisableConfigHandler())
  1537. a.Router.GET("/cloud/config/delete", a.CloudConfigController.GetDeleteConfigHandler())
  1538. a.httpServices.RegisterAll(a.Router)
  1539. return a
  1540. }
  1541. func writeErrorResponse(w http.ResponseWriter, code int, message string) {
  1542. out := map[string]string{
  1543. "message": message,
  1544. }
  1545. bytes, err := json.Marshal(out)
  1546. if err != nil {
  1547. w.Header().Set("Content-Type", "text/plain")
  1548. w.WriteHeader(500)
  1549. fmt.Fprint(w, "unable to marshall json for error")
  1550. log.Warnf("Failed to marshall JSON for error response: %s", err.Error())
  1551. return
  1552. }
  1553. w.WriteHeader(code)
  1554. fmt.Fprint(w, string(bytes))
  1555. }