2
0

router.go 57 KB

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